diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..8906da2
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,4 @@
+# Environment variables for LibreBooking API integration
+REACT_APP_LIBREBOOKING_API_URL=http://localhost:8080
+# Optional: API key if authentication is required
+# REACT_APP_LIBREBOOKING_API_KEY=your-api-key-here
\ No newline at end of file
diff --git a/README.md b/README.md
index b87cb00..5dd8f04 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,136 @@
-# Getting Started with Create React App
+# LibreBooking UI
-This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+A modern, user-friendly web interface for LibreBooking that provides an improved booking experience while maintaining compatibility with the LibreBooking API.
-## Available Scripts
+## Features
-In the project directory, you can run:
+### For Users
+- **Resource Browsing**: View available resources with detailed information including location, capacity, and amenities
+- **Easy Booking**: Intuitive reservation creation with real-time availability checking
+- **Calendar View**: Visual weekly calendar showing resource availability and existing bookings
+- **Responsive Design**: Works seamlessly on desktop, tablet, and mobile devices
-### `npm start`
+### For Administrators
+- **Admin Dashboard**: Centralized management interface for all reservations
+- **Reservation Management**: Confirm, cancel, and review booking requests
+- **Status Tracking**: Monitor pending, confirmed, and cancelled reservations
+- **Quick Actions**: One-click confirmation and cancellation of bookings
-Runs the app in the development mode.\
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+## Technology Stack
-The page will reload if you make edits.\
-You will also see any lint errors in the console.
+- **Frontend**: React 18 with TypeScript
+- **Routing**: React Router for navigation
+- **Date Handling**: date-fns for date manipulation
+- **Styling**: CSS3 with responsive design
+- **API Integration**: Structured client for LibreBooking API compatibility
-### `npm test`
+## Project Structure
-Launches the test runner in the interactive watch mode.\
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+```
+src/
+├── components/ # Reusable UI components
+│ ├── Layout.tsx # Main navigation and layout
+│ └── Layout.css
+├── pages/ # Page components
+│ ├── ResourcesList.tsx # Resource listing page
+│ ├── CreateReservation.tsx # Booking form
+│ ├── ResourceCalendar.tsx # Weekly calendar view
+│ └── AdminDashboard.tsx # Admin management interface
+├── services/ # API and business logic
+│ ├── api.ts # Mock API for development
+│ └── librebooking-api.ts # LibreBooking API client
+├── types/ # TypeScript interfaces
+│ └── index.ts # Core data types
+└── App.tsx # Main application component
+```
-### `npm run build`
+## Getting Started
-Builds the app for production to the `build` folder.\
-It correctly bundles React in production mode and optimizes the build for the best performance.
+### Prerequisites
+- Node.js 16+
+- npm or yarn
-The build is minified and the filenames include the hashes.\
-Your app is ready to be deployed!
+### Installation
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+1. Install dependencies:
+```bash
+npm install
+```
-### `npm run eject`
+2. Start the development server:
+```bash
+npm start
+```
-**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+3. Open [http://localhost:3000](http://localhost:3000) in your browser
-If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+## API Integration
-Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+### Development Mode
+The application runs with mock data by default for development and testing.
-You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+### Production Integration
-## Learn More
+1. Copy the environment template:
+```bash
+cp .env.example .env
+```
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+2. Configure your LibreBooking API endpoint:
+```env
+REACT_APP_LIBREBOOKING_API_URL=https://your-librebooking-instance.com
+```
-To learn React, check out the [React documentation](https://reactjs.org/).
+3. Update the API client in `src/services/api.ts` to use the real LibreBooking API:
+```typescript
+import { createLibreBookingClient } from './librebooking-api';
+
+export const api = createLibreBookingClient();
+```
+
+## Available Pages
+
+- **/resources** - Browse available resources
+- **/resources/:id/calendar** - View resource availability calendar
+- **/reservations/new** - Create a new reservation
+- **/admin** - Admin dashboard for managing reservations
+
+## Building for Production
+
+```bash
+npm run build
+```
+
+This creates a `build` folder with the production-ready application.
+
+## Features in Detail
+
+### Resource Management
+- List all available resources with filtering options
+- Display resource details: location, capacity, amenities
+- Real-time availability status
+- Quick booking shortcuts
+
+### Reservation System
+- Step-by-step booking process
+- Date and time selection with validation
+- Resource-specific booking forms
+- Confirmation and status tracking
+
+### Calendar View
+- Weekly calendar layout
+- Visual reservation blocks
+- Navigation between weeks
+- Today highlighting
+- Color-coded reservation status
+
+### Admin Dashboard
+- Overview statistics
+- Pending reservation alerts
+- Quick action buttons
+- Detailed reservation information
+- Status management
+
+## Contributing
+
+This UI is designed to be an independent frontend that communicates with the LibreBooking backend via REST API. The mock data service makes it easy to develop and test without a backend connection.
diff --git a/package-lock.json b/package-lock.json
index 3fc4c7c..3bd7d4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,8 +16,11 @@
"@types/node": "^16.18.126",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
+ "@types/react-router-dom": "^5.3.3",
+ "date-fns": "^4.1.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
+ "react-router-dom": "^7.13.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@@ -3595,6 +3598,12 @@
"@types/node": "*"
}
},
+ "node_modules/@types/history": {
+ "version": "4.7.11",
+ "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
+ "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
+ "license": "MIT"
+ },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -3731,6 +3740,27 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/react-router": {
+ "version": "5.1.20",
+ "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
+ "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-router-dom": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
+ "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/history": "^4.7.11",
+ "@types/react": "*",
+ "@types/react-router": "*"
+ }
+ },
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -6384,6 +6414,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -13732,6 +13772,57 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
+ "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
+ "integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-router/node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14610,6 +14701,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
diff --git a/package.json b/package.json
index 6aecf75..e852d6e 100644
--- a/package.json
+++ b/package.json
@@ -11,8 +11,11 @@
"@types/node": "^16.18.126",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
+ "@types/react-router-dom": "^5.3.3",
+ "date-fns": "^4.1.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
+ "react-router-dom": "^7.13.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
diff --git a/src/App.css b/src/App.css
index 74b5e05..0393278 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,38 +1,20 @@
-.App {
- text-align: center;
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background-color: #f5f6fa;
+ color: #2c3e50;
+ line-height: 1.6;
}
-.App-logo {
- height: 40vmin;
- pointer-events: none;
+* {
+ box-sizing: border-box;
}
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ monospace;
}
diff --git a/src/App.tsx b/src/App.tsx
index a53698a..dbc31bc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,25 +1,30 @@
import React from 'react';
-import logo from './logo.svg';
+import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
+import Layout from './components/Layout';
+import ErrorBoundary from './components/ErrorBoundary';
+import ResourcesList from './pages/ResourcesList';
+import ResourceCalendar from './pages/ResourceCalendar';
+import CreateReservation from './pages/CreateReservation';
+import UserDashboard from './pages/UserDashboard';
+import AdminDashboard from './pages/AdminDashboard';
import './App.css';
function App() {
return (
-
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
);
}
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..fec51a7
--- /dev/null
+++ b/src/components/ErrorBoundary.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+
+interface ErrorBoundaryState {
+ hasError: boolean;
+ error?: Error;
+}
+
+class ErrorBoundary extends React.Component<
+ React.PropsWithChildren<{}>,
+ ErrorBoundaryState
+> {
+ constructor(props: React.PropsWithChildren<{}>) {
+ super(props);
+ this.state = { hasError: false };
+ }
+
+ static getDerivedStateFromError(error: Error): ErrorBoundaryState {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+ console.error('Error caught by boundary:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
Something went wrong
+
The application encountered an error. Please try refreshing the page.
+
+ Error details
+
+ {this.state.error?.stack}
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
+export default ErrorBoundary;
\ No newline at end of file
diff --git a/src/components/Layout.css b/src/components/Layout.css
new file mode 100644
index 0000000..8314fed
--- /dev/null
+++ b/src/components/Layout.css
@@ -0,0 +1,218 @@
+.layout {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.navbar {
+ background-color: #2c3e50;
+ color: white;
+ padding: 1rem 1.5rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+ position: relative;
+}
+
+.nav-brand {
+ text-decoration: none;
+ color: white;
+ transition: background-color 0.2s;
+ border-radius: 4px;
+ padding: 0.25rem 0.5rem;
+}
+
+.nav-brand:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.nav-brand h1 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+/* Desktop Navigation */
+.nav-links-desktop {
+ display: flex;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ gap: 2rem;
+}
+
+/* Mobile Menu Button */
+.mobile-menu-btn {
+ display: none;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 32px;
+ height: 32px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 4px;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+}
+
+.mobile-menu-btn:hover {
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.hamburger-line {
+ width: 20px;
+ height: 2px;
+ background-color: white;
+ margin: 2px 0;
+ transition: all 0.3s ease;
+ transform-origin: center;
+}
+
+.mobile-menu-btn[aria-expanded="true"] .hamburger-line:nth-child(1) {
+ transform: rotate(45deg) translate(5px, 5px);
+}
+
+.mobile-menu-btn[aria-expanded="true"] .hamburger-line:nth-child(2) {
+ opacity: 0;
+ transform: translateX(10px);
+}
+
+.mobile-menu-btn[aria-expanded="true"] .hamburger-line:nth-child(3) {
+ transform: rotate(-45deg) translate(5px, -5px);
+}
+
+/* Mobile Menu */
+.mobile-menu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 1000;
+}
+
+.mobile-menu-backdrop {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.nav-links-mobile {
+ position: absolute;
+ top: 0;
+ right: 0;
+ background-color: #2c3e50;
+ width: 280px;
+ height: 100vh;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ transform: translateX(100%);
+ transition: transform 0.3s ease;
+ box-shadow: -2px 0 10px rgba(0,0,0,0.1);
+}
+
+.mobile-menu.open .nav-links-mobile {
+ transform: translateX(0);
+}
+
+.nav-links-mobile li {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.nav-links-mobile li:last-child {
+ border-bottom: none;
+}
+
+/* Common Navigation Styles */
+.nav-links a {
+ color: white;
+ text-decoration: none;
+ padding: 1rem 1.5rem;
+ border-radius: 4px;
+ transition: background-color 0.2s;
+ display: block;
+ font-size: 1rem;
+}
+
+.nav-links-desktop a {
+ padding: 0.5rem 1rem;
+}
+
+.nav-links a:hover,
+.nav-links a.active {
+ background-color: #34495e;
+}
+
+.nav-brand:hover {
+ background-color: #34495e;
+}
+
+.main-content {
+ flex: 1;
+ padding: 2rem;
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .navbar {
+ padding: 1rem;
+ }
+
+ .nav-brand h1 {
+ font-size: 1.1rem;
+ }
+
+ .nav-links-desktop {
+ display: none;
+ }
+
+ .mobile-menu-btn {
+ display: flex;
+ }
+
+ .mobile-menu {
+ display: block;
+ }
+
+ .main-content {
+ padding: 1rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .navbar {
+ padding: 0.75rem 1rem;
+ }
+
+ .nav-brand h1 {
+ font-size: 1rem;
+ }
+
+ .mobile-menu-btn {
+ width: 28px;
+ height: 28px;
+ }
+
+ .hamburger-line {
+ width: 18px;
+ height: 2px;
+ }
+
+ .nav-links-mobile {
+ width: 250px;
+ }
+
+ .main-content {
+ padding: 0.75rem;
+ }
+}
\ No newline at end of file
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
new file mode 100644
index 0000000..cf1efa2
--- /dev/null
+++ b/src/components/Layout.tsx
@@ -0,0 +1,112 @@
+import React, { useState } from 'react';
+import { Link, useLocation } from 'react-router-dom';
+import './Layout.css';
+
+interface LayoutProps {
+ children: React.ReactNode;
+}
+
+const Layout: React.FC = ({ children }) => {
+ const location = useLocation();
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
+
+ const toggleMobileMenu = () => {
+ try {
+ setIsMobileMenuOpen(!isMobileMenuOpen);
+ } catch (error) {
+ console.error('Error toggling mobile menu:', error);
+ }
+ };
+
+ const closeMobileMenu = () => {
+ try {
+ setIsMobileMenuOpen(false);
+ } catch (error) {
+ console.error('Error closing mobile menu:', error);
+ }
+ };
+
+ const navLinks = [
+ {
+ to: '/dashboard',
+ label: 'My Reservations',
+ isActive: location.pathname === '/dashboard'
+ },
+ {
+ to: '/resources',
+ label: 'Resources',
+ isActive: location.pathname.startsWith('/resources')
+ },
+ {
+ to: '/reservations/new',
+ label: 'Book Resource',
+ isActive: location.pathname === '/reservations/new'
+ },
+ {
+ to: '/admin',
+ label: 'Admin',
+ isActive: location.pathname === '/admin'
+ }
+ ];
+
+ return (
+
+
+
+ {children}
+
+
+ );
+};
+
+export default Layout;
\ No newline at end of file
diff --git a/src/pages/AdminDashboard.css b/src/pages/AdminDashboard.css
new file mode 100644
index 0000000..f15fd42
--- /dev/null
+++ b/src/pages/AdminDashboard.css
@@ -0,0 +1,217 @@
+.admin-dashboard {
+ width: 100%;
+}
+
+.page-header {
+ margin-bottom: 2rem;
+}
+
+.page-header h1 {
+ margin: 0 0 0.5rem 0;
+ color: #2c3e50;
+}
+
+.page-header p {
+ margin: 0;
+ color: #7f8c8d;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.stat-card {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ text-align: center;
+}
+
+.stat-card h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 2rem;
+ color: #3498db;
+}
+
+.stat-card p {
+ margin: 0;
+ color: #7f8c8d;
+ font-weight: 500;
+}
+
+.reservations-sections {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.reservations-section {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ overflow: hidden;
+}
+
+.reservations-section h2 {
+ margin: 0;
+ padding: 1.5rem;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+ color: #2c3e50;
+}
+
+.reservations-list {
+ padding: 1rem;
+}
+
+.reservation-card {
+ border: 1px solid #dee2e6;
+ border-radius: 6px;
+ margin-bottom: 1rem;
+ padding: 1.5rem;
+ background: white;
+ transition: box-shadow 0.2s;
+}
+
+.reservation-card:hover {
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.reservation-card:last-child {
+ margin-bottom: 0;
+}
+
+.reservation-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+
+.reservation-header h3 {
+ margin: 0;
+ color: #2c3e50;
+ font-size: 1.2rem;
+}
+
+.status-badge {
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ color: white;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.reservation-details {
+ display: grid;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.detail-row {
+ display: flex;
+ gap: 0.5rem;
+ color: #555;
+ font-size: 0.9rem;
+}
+
+.detail-row strong {
+ color: #2c3e50;
+ min-width: 80px;
+}
+
+.reservation-actions {
+ display: flex;
+ gap: 0.5rem;
+ padding-top: 1rem;
+ border-top: 1px solid #eee;
+}
+
+.btn {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ font-weight: 500;
+ text-decoration: none;
+ text-align: center;
+ display: inline-block;
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-confirm {
+ background-color: #27ae60;
+ color: white;
+}
+
+.btn-confirm:hover:not(:disabled) {
+ background-color: #229954;
+}
+
+.btn-cancel {
+ background-color: #e74c3c;
+ color: white;
+}
+
+.btn-cancel:hover:not(:disabled) {
+ background-color: #c0392b;
+}
+
+.no-reservations {
+ text-align: center;
+ color: #7f8c8d;
+ padding: 2rem;
+ font-style: italic;
+}
+
+.loading,
+.error {
+ text-align: center;
+ padding: 2rem;
+ font-size: 1.1rem;
+}
+
+.error {
+ color: #e74c3c;
+ background-color: #f8d7da;
+ border: 1px solid #f5c6cb;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
+
+@media (max-width: 768px) {
+ .stats-grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .reservation-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+
+ .reservation-actions {
+ flex-direction: column;
+ }
+
+ .detail-row {
+ flex-direction: column;
+ gap: 0.25rem;
+ }
+
+ .detail-row strong {
+ min-width: auto;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx
new file mode 100644
index 0000000..de709e9
--- /dev/null
+++ b/src/pages/AdminDashboard.tsx
@@ -0,0 +1,226 @@
+import React, { useState, useEffect } from 'react';
+import { format } from 'date-fns';
+import { api } from '../services/api';
+import { Reservation } from '../types';
+import './AdminDashboard.css';
+
+const AdminDashboard: React.FC = () => {
+ const [reservations, setReservations] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [processing, setProcessing] = useState(null);
+
+ useEffect(() => {
+ loadReservations();
+ }, []);
+
+ const loadReservations = async () => {
+ try {
+ setLoading(true);
+ const data = await api.getReservations();
+ // Sort by creation date, newest first
+ const sortedData = data.sort((a, b) =>
+ new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
+ );
+ setReservations(sortedData);
+ } catch (err) {
+ setError('Failed to load reservations');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleConfirmReservation = async (reservationId: string) => {
+ try {
+ setProcessing(reservationId);
+ await api.updateReservationStatus(reservationId, 'confirmed');
+
+ // Update local state
+ setReservations(prev =>
+ prev.map(r =>
+ r.id === reservationId
+ ? { ...r, status: 'confirmed' as const }
+ : r
+ )
+ );
+ } catch (err) {
+ setError('Failed to confirm reservation');
+ } finally {
+ setProcessing(null);
+ }
+ };
+
+ const handleCancelReservation = async (reservationId: string) => {
+ try {
+ setProcessing(reservationId);
+ await api.updateReservationStatus(reservationId, 'cancelled');
+
+ // Update local state
+ setReservations(prev =>
+ prev.map(r =>
+ r.id === reservationId
+ ? { ...r, status: 'cancelled' as const }
+ : r
+ )
+ );
+ } catch (err) {
+ setError('Failed to cancel reservation');
+ } finally {
+ setProcessing(null);
+ }
+ };
+
+ const pendingReservations = reservations.filter(r => r.status === 'pending');
+ const confirmedReservations = reservations.filter(r => r.status === 'confirmed');
+ const cancelledReservations = reservations.filter(r => r.status === 'cancelled');
+
+ if (loading) {
+ return Loading admin dashboard...
;
+ }
+
+ return (
+
+
+
Admin Dashboard
+
Manage resource reservations
+
+
+ {error &&
{error}
}
+
+
+
+
{pendingReservations.length}
+
Pending Reservations
+
+
+
{confirmedReservations.length}
+
Confirmed Reservations
+
+
+
{cancelledReservations.length}
+
Cancelled Reservations
+
+
+
+
+ {pendingReservations.length > 0 && (
+
+ Pending Reservations
+
+ {pendingReservations.map(reservation => (
+ handleConfirmReservation(reservation.id)}
+ onCancel={() => handleCancelReservation(reservation.id)}
+ processing={processing === reservation.id}
+ />
+ ))}
+
+
+ )}
+
+
+ All Reservations
+
+ {reservations.length === 0 ? (
+
No reservations found
+ ) : (
+ reservations.map(reservation => (
+
handleConfirmReservation(reservation.id) : undefined}
+ onCancel={reservation.status === 'pending' ? () => handleCancelReservation(reservation.id) : undefined}
+ processing={processing === reservation.id}
+ />
+ ))
+ )}
+
+
+
+
+ );
+};
+
+interface ReservationCardProps {
+ reservation: Reservation;
+ onConfirm?: () => void;
+ onCancel?: () => void;
+ processing: boolean;
+}
+
+const ReservationCard: React.FC = ({
+ reservation,
+ onConfirm,
+ onCancel,
+ processing
+}) => {
+ const statusColors = {
+ pending: '#f39c12',
+ confirmed: '#27ae60',
+ cancelled: '#e74c3c'
+ };
+
+ return (
+
+
+
{reservation.title}
+
+ {reservation.status}
+
+
+
+
+
+ Resource: {reservation.resource.name}
+
+
+ Location: {reservation.resource.location}
+
+
+ Date: {format(new Date(reservation.startTime), 'MMMM d, yyyy')}
+
+
+ Time: {format(new Date(reservation.startTime), 'h:mm a')} - {format(new Date(reservation.endTime), 'h:mm a')}
+
+ {reservation.description && (
+
+ Description: {reservation.description}
+
+ )}
+
+ Created: {format(new Date(reservation.createdAt), 'MMM d, yyyy h:mm a')}
+
+
+
+ {(onConfirm || onCancel) && (
+
+ {onConfirm && (
+
+ )}
+ {onCancel && (
+
+ )}
+
+ )}
+
+ );
+};
+
+export default AdminDashboard;
\ No newline at end of file
diff --git a/src/pages/CreateReservation.css b/src/pages/CreateReservation.css
new file mode 100644
index 0000000..676b63a
--- /dev/null
+++ b/src/pages/CreateReservation.css
@@ -0,0 +1,142 @@
+.create-reservation {
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.page-header {
+ margin-bottom: 2rem;
+}
+
+.page-header h1 {
+ margin: 0;
+ color: #2c3e50;
+}
+
+.reservation-form {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+}
+
+.form-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+ color: #2c3e50;
+}
+
+.form-group input,
+.form-group select,
+.form-group textarea {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 1rem;
+ transition: border-color 0.2s;
+}
+
+.form-group input:focus,
+.form-group select:focus,
+.form-group textarea:focus {
+ outline: none;
+ border-color: #3498db;
+}
+
+.form-group textarea {
+ resize: vertical;
+ min-height: 100px;
+}
+
+.form-actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+ margin-top: 2rem;
+ padding-top: 1.5rem;
+ border-top: 1px solid #eee;
+}
+
+.btn {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 4px;
+ text-decoration: none;
+ font-size: 1rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ font-weight: 500;
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-primary {
+ background-color: #3498db;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: #2980b9;
+}
+
+.btn-secondary {
+ background-color: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: #7f8c8d;
+}
+
+.error {
+ background-color: #f8d7da;
+ color: #721c24;
+ padding: 1rem;
+ border-radius: 4px;
+ margin-bottom: 1.5rem;
+ border: 1px solid #f5c6cb;
+}
+
+.loading,
+.no-resources {
+ text-align: center;
+ padding: 2rem;
+ color: #7f8c8d;
+}
+
+.no-resources h2 {
+ color: #2c3e50;
+ margin-bottom: 1rem;
+}
+
+@media (max-width: 768px) {
+ .create-reservation {
+ max-width: none;
+ }
+
+ .form-row {
+ grid-template-columns: 1fr;
+ }
+
+ .form-actions {
+ flex-direction: column;
+ }
+
+ .reservation-form {
+ padding: 1.5rem;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/CreateReservation.tsx b/src/pages/CreateReservation.tsx
new file mode 100644
index 0000000..81e7945
--- /dev/null
+++ b/src/pages/CreateReservation.tsx
@@ -0,0 +1,189 @@
+import React, { useState, useEffect } from 'react';
+import { useSearchParams, useNavigate } from 'react-router-dom';
+import { api } from '../services/api';
+import { Resource, CreateReservationRequest } from '../types';
+import './CreateReservation.css';
+
+const CreateReservation: React.FC = () => {
+ const [searchParams] = useSearchParams();
+ const navigate = useNavigate();
+ const [resources, setResources] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+
+ const preselectedResourceId = searchParams.get('resourceId');
+
+ const [formData, setFormData] = useState({
+ resourceId: preselectedResourceId || '',
+ startTime: new Date(),
+ endTime: new Date(),
+ title: '',
+ description: '',
+ });
+
+ useEffect(() => {
+ loadResources();
+ }, []);
+
+ const loadResources = async () => {
+ try {
+ setLoading(true);
+ const data = await api.getResources();
+ const availableResources = data.filter(r => r.isAvailable);
+ setResources(availableResources);
+
+ // If no preselected resource but we have available ones, select the first
+ if (!preselectedResourceId && availableResources.length > 0) {
+ setFormData(prev => ({ ...prev, resourceId: availableResources[0].id }));
+ }
+ } catch (err) {
+ setError('Failed to load resources');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!formData.resourceId || !formData.title) {
+ setError('Please fill in all required fields');
+ return;
+ }
+
+ if (formData.startTime >= formData.endTime) {
+ setError('End time must be after start time');
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ setError(null);
+
+ await api.createReservation(formData);
+
+ // Navigate to resources list on success
+ navigate('/resources', {
+ state: { message: 'Reservation created successfully!' }
+ });
+ } catch (err) {
+ setError('Failed to create reservation');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleInputChange = (field: keyof CreateReservationRequest, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ };
+
+ if (loading) {
+ return Loading resources...
;
+ }
+
+ if (resources.length === 0) {
+ return (
+
+
No Available Resources
+
There are currently no available resources to book.
+
+ );
+ }
+
+ return (
+
+
+
Create New Reservation
+
+
+ {error &&
{error}
}
+
+
+
+ );
+};
+
+export default CreateReservation;
\ No newline at end of file
diff --git a/src/pages/ResourceCalendar.css b/src/pages/ResourceCalendar.css
new file mode 100644
index 0000000..4990742
--- /dev/null
+++ b/src/pages/ResourceCalendar.css
@@ -0,0 +1,213 @@
+.resource-calendar {
+ width: 100%;
+}
+
+.calendar-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 2rem;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.header-info h1 {
+ margin: 0 0 0.5rem 0;
+ color: #2c3e50;
+}
+
+.resource-details {
+ margin: 0;
+ color: #7f8c8d;
+ font-size: 0.9rem;
+}
+
+.header-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ align-items: flex-end;
+}
+
+.week-navigation {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.current-week {
+ font-weight: 600;
+ color: #2c3e50;
+ min-width: 150px;
+ text-align: center;
+}
+
+.calendar-container {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ overflow: auto;
+}
+
+.calendar-grid {
+ display: grid;
+ grid-template-columns: 100px repeat(7, 1fr);
+ min-width: 800px;
+}
+
+.time-header {
+ padding: 1rem;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+ border-right: 1px solid #dee2e6;
+}
+
+.day-header {
+ padding: 1rem;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+ border-right: 1px solid #dee2e6;
+ text-align: center;
+}
+
+.day-name {
+ font-weight: 600;
+ color: #2c3e50;
+ font-size: 0.9rem;
+}
+
+.day-date {
+ color: #7f8c8d;
+ font-size: 0.8rem;
+}
+
+.time-label {
+ padding: 1rem;
+ background: #f8f9fa;
+ border-right: 1px solid #dee2e6;
+ border-bottom: 1px solid #dee2e6;
+ font-size: 0.8rem;
+ color: #6c757d;
+ text-align: center;
+}
+
+.time-slot {
+ min-height: 60px;
+ padding: 0.5rem;
+ border-right: 1px solid #dee2e6;
+ border-bottom: 1px solid #dee2e6;
+ background: white;
+}
+
+.time-slot.today {
+ background: #fff3cd;
+}
+
+.reservation-item {
+ background: #3498db;
+ color: white;
+ padding: 0.5rem;
+ border-radius: 4px;
+ margin-bottom: 0.25rem;
+ font-size: 0.8rem;
+}
+
+.reservation-item:last-child {
+ margin-bottom: 0;
+}
+
+.reservation-item.pending {
+ background: #f39c12;
+}
+
+.reservation-item.cancelled {
+ background: #e74c3c;
+}
+
+.reservation-title {
+ font-weight: 600;
+ margin-bottom: 0.25rem;
+}
+
+.reservation-time {
+ font-size: 0.7rem;
+ opacity: 0.9;
+}
+
+.btn {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ text-decoration: none;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ text-align: center;
+ white-space: nowrap;
+}
+
+.btn-primary {
+ background-color: #3498db;
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: #2980b9;
+}
+
+.btn-secondary {
+ background-color: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background-color: #7f8c8d;
+}
+
+.loading,
+.error {
+ text-align: center;
+ padding: 2rem;
+ font-size: 1.1rem;
+}
+
+.error {
+ color: #e74c3c;
+}
+
+@media (max-width: 1200px) {
+ .calendar-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .header-actions {
+ align-items: stretch;
+ }
+
+ .week-navigation {
+ justify-content: center;
+ }
+}
+
+@media (max-width: 768px) {
+ .calendar-grid {
+ grid-template-columns: 80px repeat(7, 1fr);
+ min-width: 600px;
+ }
+
+ .day-header,
+ .time-label,
+ .time-slot {
+ padding: 0.5rem;
+ }
+
+ .time-slot {
+ min-height: 50px;
+ }
+
+ .reservation-item {
+ font-size: 0.7rem;
+ padding: 0.25rem;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/ResourceCalendar.tsx b/src/pages/ResourceCalendar.tsx
new file mode 100644
index 0000000..4e9fe7b
--- /dev/null
+++ b/src/pages/ResourceCalendar.tsx
@@ -0,0 +1,195 @@
+import React, { useState, useEffect } from 'react';
+import { useParams, Link } from 'react-router-dom';
+import { format, startOfWeek, addDays, startOfDay, addHours, isSameDay, isWithinInterval } from 'date-fns';
+import { api } from '../services/api';
+import { Resource, Reservation } from '../types';
+import './ResourceCalendar.css';
+
+const ResourceCalendar: React.FC = () => {
+ const { id } = useParams<{ id: string }>();
+ const [resource, setResource] = useState(null);
+ const [reservations, setReservations] = useState([]);
+ const [currentWeek, setCurrentWeek] = useState(new Date());
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (id) {
+ loadData();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [id, currentWeek]);
+
+ const loadData = async () => {
+ if (!id) {
+ setError('Invalid resource ID');
+ return;
+ }
+
+ try {
+ setLoading(true);
+ setError(null);
+
+ const [resourceData, reservationsData] = await Promise.all([
+ api.getResource(id),
+ api.getReservations(id)
+ ]);
+
+ if (!resourceData) {
+ setError('Resource not found');
+ return;
+ }
+
+ console.log('Calendar - Resource:', resourceData);
+ console.log('Calendar - Reservations:', reservationsData);
+
+ setResource(resourceData);
+ setReservations(Array.isArray(reservationsData) ? reservationsData : []);
+ } catch (err) {
+ console.error('Calendar load error:', err);
+ setError('Failed to load resource calendar');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const weekStart = startOfWeek(currentWeek, { weekStartsOn: 1 }); // Start on Monday
+ const weekDays = Array.from({ length: 7 }, (_, i) => addDays(weekStart, i));
+
+ // Generate time slots from 8 AM to 8 PM
+ const timeSlots = Array.from({ length: 13 }, (_, i) => addHours(startOfDay(weekStart), 8 + i));
+
+ const getReservationsForSlot = (date: Date, timeSlot: Date): Reservation[] => {
+ if (!date || !timeSlot) return [];
+
+ const slotStart = timeSlot;
+ const slotEnd = addHours(timeSlot, 1);
+
+ const filtered = reservations.filter(reservation => {
+ if (!reservation?.startTime) return false;
+
+ const reservationDate = new Date(reservation.startTime);
+ if (isNaN(reservationDate.getTime())) return false;
+
+ const matches = isSameDay(reservationDate, date) &&
+ isWithinInterval(reservationDate, { start: slotStart, end: slotEnd });
+
+ if (matches) {
+ console.log('Found reservation for slot:', {
+ title: reservation.title,
+ date: reservationDate,
+ slotStart,
+ slotEnd,
+ matches
+ });
+ }
+
+ return matches;
+ });
+
+ return filtered;
+ };
+
+ const navigateWeek = (direction: 'prev' | 'next') => {
+ setCurrentWeek(prev => addDays(prev, direction === 'next' ? 7 : -7));
+ };
+
+ const goToToday = () => {
+ setCurrentWeek(new Date());
+ };
+
+ if (loading) {
+ return Loading calendar...
;
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (!resource) {
+ return Resource not found
;
+ }
+
+ return (
+
+
+
+
{resource.name} - Calendar
+
+ {resource.location} • Capacity: {resource.capacity}
+
+
+
+
+
+
+
+ {format(weekStart, 'MMM d')} - {format(addDays(weekStart, 6), 'MMM d, yyyy')}
+
+
+
+
+ Book Resource
+
+
+
+
+
+
+ {/* Time column header */}
+
+
+ {/* Day headers */}
+ {weekDays.map(day => (
+
+
{format(day, 'EEE')}
+
{format(day, 'd')}
+
+ ))}
+
+ {/* Time slots */}
+ {timeSlots.map(timeSlot => (
+
+
+ {format(timeSlot, 'h:mm a')}
+
+
+ {weekDays.map(day => {
+ const slotReservations = getReservationsForSlot(day, timeSlot);
+ const isToday = isSameDay(day, new Date());
+
+ return (
+
+ {slotReservations.map(reservation => (
+
+
{reservation.title}
+
+ {format(new Date(reservation.startTime), 'h:mm a')} -
+ {format(new Date(reservation.endTime), 'h:mm a')}
+
+
+ ))}
+
+ );
+ })}
+
+ ))}
+
+
+
+ );
+};
+
+export default ResourceCalendar;
\ No newline at end of file
diff --git a/src/pages/ResourcesList.css b/src/pages/ResourcesList.css
new file mode 100644
index 0000000..44e5b39
--- /dev/null
+++ b/src/pages/ResourcesList.css
@@ -0,0 +1,189 @@
+.resources-list {
+ width: 100%;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2rem;
+ gap: 1rem;
+}
+
+.page-header h1 {
+ margin: 0;
+ color: #2c3e50;
+}
+
+.resources-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
+ gap: 2rem;
+}
+
+.resource-card {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ overflow: hidden;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.resource-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 20px rgba(0,0,0,0.15);
+}
+
+.resource-image {
+ height: 200px;
+ background: #ecf0f1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.resource-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.placeholder-image {
+ font-size: 3rem;
+ color: #95a5a6;
+}
+
+.resource-content {
+ padding: 1.5rem;
+}
+
+.resource-content h3 {
+ margin: 0 0 0.5rem 0;
+ color: #2c3e50;
+ font-size: 1.25rem;
+}
+
+.resource-description {
+ color: #7f8c8d;
+ margin: 0 0 1rem 0;
+ line-height: 1.5;
+}
+
+.resource-location,
+.resource-capacity {
+ margin: 0.5rem 0;
+ color: #34495e;
+ font-size: 0.9rem;
+}
+
+.resource-amenities {
+ margin: 1rem 0;
+}
+
+.resource-amenities strong {
+ color: #2c3e50;
+ font-size: 0.9rem;
+}
+
+.resource-amenities ul {
+ margin: 0.5rem 0 0 0;
+ padding-left: 1.5rem;
+ color: #7f8c8d;
+ font-size: 0.85rem;
+}
+
+.resource-amenities li {
+ margin: 0.25rem 0;
+}
+
+.resource-status {
+ margin: 1rem 0;
+}
+
+.status-badge {
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.status-badge.available {
+ background-color: #d4edda;
+ color: #155724;
+}
+
+.status-badge.unavailable {
+ background-color: #f8d7da;
+ color: #721c24;
+}
+
+.resource-actions {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 1rem;
+}
+
+.btn {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 4px;
+ text-decoration: none;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ text-align: center;
+ flex: 1;
+}
+
+.btn:disabled,
+.btn.disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ text-decoration: none;
+}
+
+.btn-primary {
+ background-color: #3498db;
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: #2980b9;
+}
+
+.btn-secondary {
+ background-color: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: #7f8c8d;
+}
+
+.loading, .error {
+ text-align: center;
+ padding: 2rem;
+ font-size: 1.1rem;
+}
+
+.error {
+ color: #e74c3c;
+}
+
+@media (max-width: 768px) {
+ .page-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .resources-grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+
+ .resource-actions {
+ flex-direction: column;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/ResourcesList.tsx b/src/pages/ResourcesList.tsx
new file mode 100644
index 0000000..b6d5d88
--- /dev/null
+++ b/src/pages/ResourcesList.tsx
@@ -0,0 +1,113 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { api } from '../services/api';
+import { Resource } from '../types';
+import './ResourcesList.css';
+
+const ResourcesList: React.FC = () => {
+ const [resources, setResources] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ loadResources();
+ }, []);
+
+ const loadResources = async () => {
+ try {
+ setLoading(true);
+ const data = await api.getResources();
+ setResources(data);
+ } catch (err) {
+ setError('Failed to load resources');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return Loading resources...
;
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ return (
+
+
+
Available Resources
+
+ New Reservation
+
+
+
+
+ {resources.map(resource => (
+
+
+ {resource.imageUrl ? (
+

+ ) : (
+
🏢
+ )}
+
+
+
+
{resource.name}
+
{resource.description}
+
+ {resource.location && (
+
📍 {resource.location}
+ )}
+
+ {resource.capacity && (
+
👥 Capacity: {resource.capacity}
+ )}
+
+ {resource.amenities && resource.amenities.length > 0 && (
+
+
Amenities:
+
+ {resource.amenities.map((amenity, index) => (
+ - {amenity}
+ ))}
+
+
+ )}
+
+
+
+ {resource.isAvailable ? 'Available' : 'Unavailable'}
+
+
+
+
+
+ View Calendar
+
+ {resource.isAvailable ? (
+
+ Book Now
+
+ ) : (
+
+ Book Now
+
+ )}
+
+
+
+ ))}
+
+
+ );
+};
+
+export default ResourcesList;
\ No newline at end of file
diff --git a/src/pages/UserDashboard.css b/src/pages/UserDashboard.css
new file mode 100644
index 0000000..c4bcd4a
--- /dev/null
+++ b/src/pages/UserDashboard.css
@@ -0,0 +1,361 @@
+.user-dashboard {
+ width: 100%;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 2rem;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.header-content h1 {
+ margin: 0 0 0.5rem 0;
+ color: #2c3e50;
+}
+
+.header-content p {
+ margin: 0;
+ color: #7f8c8d;
+}
+
+.header-actions {
+ display: flex;
+ gap: 1rem;
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+.stat-card {
+ background: white;
+ padding: 1.5rem;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ text-align: center;
+}
+
+.stat-card h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 2rem;
+ color: #3498db;
+}
+
+.stat-card p {
+ margin: 0;
+ color: #7f8c8d;
+ font-weight: 500;
+}
+
+.filters-section {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1.5rem;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.filter-tabs {
+ display: flex;
+ gap: 0.5rem;
+ background: white;
+ padding: 0.25rem;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.filter-tab {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ background: transparent;
+ color: #7f8c8d;
+ cursor: pointer;
+ border-radius: 4px;
+ font-weight: 500;
+ transition: all 0.2s;
+}
+
+.filter-tab:hover {
+ background: #f8f9fa;
+}
+
+.filter-tab.active {
+ background: #3498db;
+ color: white;
+}
+
+.date-filter {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ background: white;
+ padding: 0.5rem;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+.date-input {
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 0.5rem;
+ font-size: 0.9rem;
+}
+
+.clear-date-btn {
+ background: none;
+ border: none;
+ color: #7f8c8d;
+ cursor: pointer;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 1rem;
+}
+
+.clear-date-btn:hover {
+ background: #f8f9fa;
+ color: #2c3e50;
+}
+
+.reservations-section {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ overflow: hidden;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1.5rem;
+ background: #f8f9fa;
+ border-bottom: 1px solid #dee2e6;
+}
+
+.section-header h2 {
+ margin: 0;
+ color: #2c3e50;
+ font-size: 1.25rem;
+}
+
+.result-count {
+ color: #7f8c8d;
+ font-size: 0.9rem;
+}
+
+.reservations-list {
+ padding: 1rem;
+}
+
+.reservation-card {
+ border: 1px solid #dee2e6;
+ border-radius: 6px;
+ margin-bottom: 1rem;
+ padding: 1.5rem;
+ background: white;
+ transition: box-shadow 0.2s;
+}
+
+.reservation-card:hover {
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.reservation-card:last-child {
+ margin-bottom: 0;
+}
+
+.reservation-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 1rem;
+ gap: 1rem;
+}
+
+.reservation-info h3 {
+ margin: 0 0 0.25rem 0;
+ color: #2c3e50;
+ font-size: 1.2rem;
+}
+
+.resource-name {
+ margin: 0;
+ color: #7f8c8d;
+ font-size: 0.9rem;
+}
+
+.status-badge {
+ padding: 0.25rem 0.75rem;
+ border-radius: 20px;
+ color: white;
+ font-size: 0.8rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.reservation-details {
+ display: grid;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.detail-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+}
+
+.detail-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #555;
+ font-size: 0.9rem;
+}
+
+.detail-icon {
+ font-size: 1rem;
+}
+
+.detail-item.description {
+ width: 100%;
+ align-items: flex-start;
+}
+
+.reservation-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 1rem;
+ border-top: 1px solid #eee;
+ gap: 1rem;
+}
+
+.pending-note {
+ color: #f39c12;
+ font-size: 0.9rem;
+ font-weight: 500;
+}
+
+.no-reservations {
+ text-align: center;
+ padding: 3rem 2rem;
+ color: #7f8c8d;
+}
+
+.no-reservations-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ opacity: 0.5;
+}
+
+.no-reservations h3 {
+ margin: 0 0 0.5rem 0;
+ color: #2c3e50;
+}
+
+.no-reservations p {
+ margin: 0 0 1.5rem 0;
+ font-size: 0.95rem;
+}
+
+.btn {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 4px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ font-weight: 500;
+ text-decoration: none;
+ text-align: center;
+ display: inline-block;
+}
+
+.btn-primary {
+ background-color: #3498db;
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: #2980b9;
+}
+
+.btn-secondary {
+ background-color: #95a5a6;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background-color: #7f8c8d;
+}
+
+.loading,
+.error {
+ text-align: center;
+ padding: 2rem;
+ font-size: 1.1rem;
+}
+
+.error {
+ color: #e74c3c;
+ background-color: #f8d7da;
+ border: 1px solid #f5c6cb;
+ border-radius: 4px;
+ margin-bottom: 1rem;
+}
+
+@media (max-width: 768px) {
+ .page-header {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .filters-section {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .filter-tabs {
+ justify-content: center;
+ }
+
+ .date-filter {
+ justify-content: center;
+ }
+
+ .section-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+
+ .reservation-header {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .detail-group {
+ flex-direction: column;
+ gap: 0.75rem;
+ }
+
+ .reservation-actions {
+ flex-direction: column;
+ align-items: stretch;
+ text-align: center;
+ }
+
+ .stats-grid {
+ grid-template-columns: 1fr;
+ gap: 1rem;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/UserDashboard.tsx b/src/pages/UserDashboard.tsx
new file mode 100644
index 0000000..3b56d2a
--- /dev/null
+++ b/src/pages/UserDashboard.tsx
@@ -0,0 +1,282 @@
+import React, { useState, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { format, startOfDay, isAfter, isBefore } from 'date-fns';
+import { api } from '../services/api';
+import { Reservation } from '../types';
+import './UserDashboard.css';
+
+const UserDashboard: React.FC = () => {
+ const [reservations, setReservations] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [filter, setFilter] = useState<'all' | 'upcoming' | 'past'>('upcoming');
+ const [selectedDate, setSelectedDate] = useState(null);
+
+ useEffect(() => {
+ loadReservations();
+ }, []);
+
+ const loadReservations = async () => {
+ try {
+ setLoading(true);
+ const data = await api.getReservations();
+ // Sort by start date, upcoming first
+ const sortedData = data.sort((a, b) =>
+ new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
+ );
+ setReservations(sortedData);
+ } catch (err) {
+ setError('Failed to load reservations');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const filterReservations = (reservations: Reservation[]) => {
+ const now = new Date();
+ const today = startOfDay(now);
+
+ switch (filter) {
+ case 'upcoming':
+ return reservations.filter(r =>
+ isAfter(new Date(r.startTime), today) ||
+ isSameDay(new Date(r.startTime), today)
+ );
+ case 'past':
+ return reservations.filter(r =>
+ isBefore(new Date(r.startTime), today)
+ );
+ default:
+ return reservations;
+ }
+ };
+
+ const filterByDate = (reservations: Reservation[]) => {
+ if (!selectedDate) return reservations;
+ return reservations.filter(r =>
+ isSameDay(new Date(r.startTime), selectedDate)
+ );
+ };
+
+ const getFilteredReservations = () => {
+ let filtered = filterReservations(reservations);
+ filtered = filterByDate(filtered);
+ return filtered;
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status) {
+ case 'confirmed':
+ return '#27ae60';
+ case 'pending':
+ return '#f39c12';
+ case 'cancelled':
+ return '#e74c3c';
+ default:
+ return '#95a5a6';
+ }
+ };
+
+ const isSameDay = (date1: Date, date2: Date) => {
+ return format(date1, 'yyyy-MM-dd') === format(date2, 'yyyy-MM-dd');
+ };
+
+ const clearDateFilter = () => {
+ setSelectedDate(null);
+ };
+
+ const upcomingReservations = filterReservations(reservations).filter(r =>
+ isAfter(new Date(r.startTime), new Date()) ||
+ isSameDay(new Date(r.startTime), new Date())
+ );
+
+ if (loading) {
+ return Loading reservations...
;
+ }
+
+ const filteredReservations = getFilteredReservations();
+
+ return (
+
+
+
+
My Reservations
+
View and manage all your resource bookings
+
+
+
+ New Reservation
+
+
+
+
+ {error &&
{error}
}
+
+ {/* Quick Stats */}
+
+
+
{upcomingReservations.length}
+
Upcoming Reservations
+
+
+
{upcomingReservations.filter(r => r.status === 'confirmed').length}
+
Confirmed Bookings
+
+
+
{upcomingReservations.filter(r => r.status === 'pending').length}
+
Pending Approval
+
+
+
+ {/* Filters */}
+
+
+
+
+
+
+
+
+ setSelectedDate(e.target.value ? new Date(e.target.value) : null)}
+ className="date-input"
+ />
+ {selectedDate && (
+
+ )}
+
+
+
+ {/* Reservations List */}
+
+
+
+ {filter === 'upcoming' && 'Upcoming Reservations'}
+ {filter === 'past' && 'Past Reservations'}
+ {filter === 'all' && 'All Reservations'}
+ {selectedDate && ` - ${format(selectedDate, 'MMM d, yyyy')}`}
+
+
+ {filteredReservations.length} {filteredReservations.length === 1 ? 'reservation' : 'reservations'}
+
+
+
+ {filteredReservations.length === 0 ? (
+
+
📅
+
No reservations found
+
+ {selectedDate
+ ? `No reservations found for ${format(selectedDate, 'MMMM d, yyyy')}`
+ : filter === 'upcoming'
+ ? 'You have no upcoming reservations'
+ : filter === 'past'
+ ? 'You have no past reservations'
+ : 'No reservations found'
+ }
+
+ {!selectedDate && filter === 'upcoming' && (
+
+ Create Your First Reservation
+
+ )}
+
+ ) : (
+
+ {filteredReservations.map(reservation => (
+
+
+
+
{reservation.title}
+
{reservation.resource.name}
+
+
+
+ {reservation.status}
+
+
+
+
+
+
+
+ 📅
+ {format(new Date(reservation.startTime), 'EEEE, MMMM d, yyyy')}
+
+
+ 🕐
+
+ {format(new Date(reservation.startTime), 'h:mm a')} -
+ {format(new Date(reservation.endTime), 'h:mm a')}
+
+
+
+
+
+
+ 📍
+ {reservation.resource.location}
+
+ {reservation.resource.capacity && (
+
+ 👥
+ Capacity: {reservation.resource.capacity}
+
+ )}
+
+
+ {reservation.description && (
+
+
+ 📝
+ {reservation.description}
+
+
+ )}
+
+
+
+
+ View Calendar
+
+ {reservation.status === 'pending' && (
+
+ ⏳ Awaiting approval
+
+ )}
+
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default UserDashboard;
\ No newline at end of file
diff --git a/src/services/api.ts b/src/services/api.ts
new file mode 100644
index 0000000..3e568ae
--- /dev/null
+++ b/src/services/api.ts
@@ -0,0 +1,131 @@
+import { Resource, Reservation, CreateReservationRequest } from '../types';
+
+// Mock data for development - will be replaced with real API calls
+const mockResources: Resource[] = [
+ {
+ id: '1',
+ name: 'Conference Room A',
+ description: 'Large conference room with projector and whiteboard',
+ location: 'Building 1, Floor 2',
+ capacity: 20,
+ imageUrl: '/images/conference-room.jpg',
+ amenities: ['Projector', 'Whiteboard', 'Video Conference'],
+ isAvailable: true,
+ },
+ {
+ id: '2',
+ name: 'Meeting Room B',
+ description: 'Small meeting room for team discussions',
+ location: 'Building 1, Floor 1',
+ capacity: 6,
+ amenities: ['TV', 'Whiteboard'],
+ isAvailable: true,
+ },
+ {
+ id: '3',
+ name: 'Training Room',
+ description: 'Spacious training room with multiple screens',
+ location: 'Building 2, Floor 3',
+ capacity: 30,
+ amenities: ['Projector', 'Sound System', 'Recording Equipment'],
+ isAvailable: false,
+ },
+];
+
+const mockReservations: Reservation[] = [
+ {
+ id: '1',
+ resourceId: '1',
+ resource: mockResources[0],
+ userId: 'user1',
+ startTime: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}T10:00:00`),
+ endTime: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}T12:00:00`),
+ title: 'Team Meeting',
+ description: 'Weekly team sync',
+ status: 'confirmed',
+ createdAt: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate() - 2).padStart(2, '0')}T09:00:00`),
+ },
+ {
+ id: '2',
+ resourceId: '2',
+ resource: mockResources[1],
+ userId: 'user2',
+ startTime: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}T14:00:00`),
+ endTime: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate()).padStart(2, '0')}T15:30:00`),
+ title: 'Client Presentation',
+ status: 'pending',
+ createdAt: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate() - 1).padStart(2, '0')}T11:00:00`),
+ },
+ {
+ id: '3',
+ resourceId: '1',
+ resource: mockResources[0],
+ userId: 'user3',
+ startTime: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate() + 1).padStart(2, '0')}T09:00:00`),
+ endTime: new Date(`${new Date().getFullYear()}-${String(new Date().getMonth() + 1).padStart(2, '0')}-${String(new Date().getDate() + 1).padStart(2, '0')}T11:00:00`),
+ title: 'Project Planning',
+ description: 'Q1 planning session',
+ status: 'pending',
+ createdAt: new Date(),
+ },
+];
+
+export const api = {
+ // Resources
+ getResources: async (): Promise => {
+ // Simulate API delay
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return mockResources;
+ },
+
+ getResource: async (id: string): Promise => {
+ await new Promise(resolve => setTimeout(resolve, 300));
+ return mockResources.find(r => r.id === id) || null;
+ },
+
+ // Reservations
+ getReservations: async (resourceId?: string): Promise => {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ if (resourceId) {
+ return mockReservations.filter(r => r.resourceId === resourceId);
+ }
+ return mockReservations;
+ },
+
+ createReservation: async (data: CreateReservationRequest): Promise => {
+ await new Promise(resolve => setTimeout(resolve, 800));
+ const resource = mockResources.find(r => r.id === data.resourceId);
+ if (!resource) {
+ throw new Error('Resource not found');
+ }
+
+ const newReservation: Reservation = {
+ id: Date.now().toString(),
+ resourceId: data.resourceId,
+ resource,
+ userId: 'current-user', // Will come from authentication
+ startTime: data.startTime,
+ endTime: data.endTime,
+ title: data.title,
+ description: data.description,
+ status: 'pending',
+ createdAt: new Date(),
+ };
+
+ mockReservations.push(newReservation);
+ console.log('New reservation created:', newReservation);
+ console.log('All reservations now:', mockReservations);
+ return newReservation;
+ },
+
+ updateReservationStatus: async (id: string, status: 'confirmed' | 'cancelled'): Promise => {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ const reservation = mockReservations.find(r => r.id === id);
+ if (!reservation) {
+ throw new Error('Reservation not found');
+ }
+
+ reservation.status = status;
+ return reservation;
+ },
+};
\ No newline at end of file
diff --git a/src/services/librebooking-api.ts b/src/services/librebooking-api.ts
new file mode 100644
index 0000000..0f21471
--- /dev/null
+++ b/src/services/librebooking-api.ts
@@ -0,0 +1,133 @@
+import { Resource, Reservation, CreateReservationRequest } from '../types';
+
+export interface LibreBookingAPIConfig {
+ baseURL: string;
+ apiKey?: string;
+ timeout?: number;
+}
+
+class LibreBookingAPIClient {
+ private config: LibreBookingAPIConfig;
+ private baseUrl: string;
+
+ constructor(config: LibreBookingAPIConfig) {
+ this.config = config;
+ this.baseUrl = config.baseURL.replace(/\/$/, ''); // Remove trailing slash
+ }
+
+ private async makeRequest(
+ endpoint: string,
+ options: RequestInit = {}
+ ): Promise {
+ const url = `${this.baseUrl}${endpoint}`;
+
+ const headers: Record = {
+ 'Content-Type': 'application/json',
+ ...(options.headers as Record || {}),
+ };
+
+ if (this.config.apiKey) {
+ headers['Authorization'] = `Bearer ${this.config.apiKey}`;
+ }
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ headers,
+ signal: AbortSignal.timeout(this.config.timeout || 10000),
+ });
+
+ if (!response.ok) {
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
+ }
+
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ return await response.json();
+ } else {
+ return response.text() as unknown as T;
+ }
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(`Network error: ${error.message}`);
+ }
+ throw error;
+ }
+ }
+
+ // Resources
+ async getResources(): Promise {
+ return this.makeRequest('/api/resources');
+ }
+
+ async getResource(id: string): Promise {
+ return this.makeRequest(`/api/resources/${id}`);
+ }
+
+ // Reservations
+ async getReservations(resourceId?: string): Promise {
+ const params = resourceId ? `?resourceId=${resourceId}` : '';
+ return this.makeRequest(`/api/reservations${params}`);
+ }
+
+ async getReservation(id: string): Promise {
+ return this.makeRequest(`/api/reservations/${id}`);
+ }
+
+ async createReservation(data: CreateReservationRequest): Promise {
+ return this.makeRequest('/api/reservations', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ });
+ }
+
+ async updateReservationStatus(
+ id: string,
+ status: 'confirmed' | 'cancelled'
+ ): Promise {
+ return this.makeRequest(`/api/reservations/${id}/status`, {
+ method: 'PATCH',
+ body: JSON.stringify({ status }),
+ });
+ }
+
+ async cancelReservation(id: string): Promise {
+ return this.makeRequest(`/api/reservations/${id}/cancel`, {
+ method: 'POST',
+ });
+ }
+
+ // Users (if needed for admin functions)
+ async getUsers(): Promise {
+ return this.makeRequest('/api/users');
+ }
+
+ async getUserReservations(userId: string): Promise {
+ return this.makeRequest(`/api/users/${userId}/reservations`);
+ }
+}
+
+// Factory function to create API client with environment variables
+export function createLibreBookingClient(
+ config?: Partial
+): LibreBookingAPIClient {
+ const defaultConfig: LibreBookingAPIConfig = {
+ baseURL: process.env.REACT_APP_LIBREBOOKING_API_URL || 'http://localhost:8080',
+ apiKey: process.env.REACT_APP_LIBREBOOKING_API_KEY,
+ timeout: 10000,
+ };
+
+ const finalConfig = { ...defaultConfig, ...config };
+ return new LibreBookingAPIClient(finalConfig);
+}
+
+// Example of how to integrate with the existing mock API
+export function integrateWithMockAPI(mockAPI: any) {
+ return {
+ getResources: mockAPI.getResources,
+ getResource: mockAPI.getResource,
+ getReservations: mockAPI.getReservations,
+ createReservation: mockAPI.createReservation,
+ updateReservationStatus: mockAPI.updateReservationStatus,
+ };
+}
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..452af51
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,38 @@
+export interface Resource {
+ id: string;
+ name: string;
+ description?: string;
+ location?: string;
+ capacity?: number;
+ imageUrl?: string;
+ amenities?: string[];
+ isAvailable: boolean;
+}
+
+export interface Reservation {
+ id: string;
+ resourceId: string;
+ resource: Resource;
+ userId: string;
+ startTime: Date;
+ endTime: Date;
+ title: string;
+ description?: string;
+ status: 'pending' | 'confirmed' | 'cancelled';
+ createdAt: Date;
+}
+
+export interface User {
+ id: string;
+ name: string;
+ email: string;
+ role: 'user' | 'admin';
+}
+
+export interface CreateReservationRequest {
+ resourceId: string;
+ startTime: Date;
+ endTime: Date;
+ title: string;
+ description?: string;
+}
\ No newline at end of file