Phoenix21 commited on
Commit
235037f
·
verified ·
1 Parent(s): 9c9f87a

🗑️ Remove old frontend folder

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. frontend/.dockerignore +0 -75
  2. frontend/.env +0 -15
  3. frontend/.env.example +0 -18
  4. frontend/.env.production +0 -18
  5. frontend/.gitignore +0 -30
  6. frontend/DOCKER-SUMMARY.md +0 -119
  7. frontend/DOCKER.md +0 -219
  8. frontend/Dockerfile +0 -44
  9. frontend/Dockerfile.dev +0 -21
  10. frontend/ENDPOINT_FIX_SUMMARY.md +0 -184
  11. frontend/FIELD_NAME_FIX.md +0 -58
  12. frontend/FRONTEND_ENV_FIXED.md +0 -260
  13. frontend/FRONTEND_INTEGRATION_GUIDE.txt +0 -1510
  14. frontend/FRONTEND_INTEGRATION_GUIDE_MARK2.md +0 -945
  15. frontend/JSON_FIX_SUMMARY.md +0 -79
  16. frontend/README-LOCALLEND.md +0 -323
  17. frontend/README.md +0 -73
  18. frontend/SETUP.md +0 -166
  19. frontend/STATUS_TOGGLE_DEBUG.md +0 -238
  20. frontend/TOGGLE_FIX_SUMMARY.md +0 -96
  21. frontend/TOGGLE_SIMPLIFIED.md +0 -95
  22. frontend/VALIDATION_FIXES.md +0 -112
  23. frontend/eslint.config.js +0 -23
  24. frontend/healthcheck.sh +0 -11
  25. frontend/index.html +0 -13
  26. frontend/nginx.conf +0 -52
  27. frontend/package-lock.json +0 -3289
  28. frontend/package.json +0 -50
  29. frontend/public/vite.svg +0 -1
  30. frontend/src/App.css +0 -42
  31. frontend/src/App.tsx +0 -232
  32. frontend/src/App.tsx.backup +0 -2048
  33. frontend/src/assets/react.svg +0 -1
  34. frontend/src/components/Navbar.tsx +0 -217
  35. frontend/src/context/AuthContext.tsx +0 -103
  36. frontend/src/index.css +0 -74
  37. frontend/src/main.tsx +0 -13
  38. frontend/src/pages/AddItemPage.tsx +0 -651
  39. frontend/src/pages/BookItemPage.tsx +0 -348
  40. frontend/src/pages/BrowsePage.tsx +0 -238
  41. frontend/src/pages/DashboardPage.tsx +0 -1685
  42. frontend/src/pages/HomePage.tsx +0 -297
  43. frontend/src/pages/LoginPage.tsx +0 -254
  44. frontend/src/pages/MyBookingsPage.tsx +0 -15
  45. frontend/src/pages/MyItemsPage.tsx +0 -15
  46. frontend/src/pages/RegisterPage.tsx +0 -419
  47. frontend/src/pages/SearchResultsPage.tsx +0 -445
  48. frontend/src/services/api.ts +0 -121
  49. frontend/src/services/authService.ts +0 -48
  50. frontend/src/services/bookingService.ts +0 -82
frontend/.dockerignore DELETED
@@ -1,75 +0,0 @@
1
- # Docker ignore file for LocalLend Frontend
2
- # This prevents unnecessary files from being copied to Docker build context
3
-
4
- # Dependencies
5
- node_modules/
6
- npm-debug.log*
7
- yarn-debug.log*
8
- yarn-error.log*
9
-
10
- # Development files
11
- .env.local
12
- .env.development.local
13
- .env.test.local
14
- .env.production.local
15
-
16
- # Build outputs (these will be generated inside the container)
17
- dist/
18
- build/
19
-
20
- # IDE and editor files
21
- .vscode/
22
- .idea/
23
- *.swp
24
- *.swo
25
- *~
26
-
27
- # OS generated files
28
- .DS_Store
29
- .DS_Store?
30
- ._*
31
- .Spotlight-V100
32
- .Trashes
33
- ehthumbs.db
34
- Thumbs.db
35
-
36
- # Git
37
- .git/
38
- .gitignore
39
-
40
- # Documentation (optional - comment out if you want docs in container)
41
- README.md
42
- README-LOCALLEND.md
43
- SETUP.md
44
- *.md
45
-
46
- # Development configuration
47
- .eslintrc*
48
- .prettierrc*
49
-
50
- # Testing
51
- coverage/
52
- .nyc_output/
53
-
54
- # Docker files themselves (to avoid recursion)
55
- Dockerfile
56
- docker-compose*.yml
57
- .dockerignore
58
-
59
- # Logs
60
- logs/
61
- *.log
62
-
63
- # Runtime data
64
- pids/
65
- *.pid
66
- *.seed
67
- *.pid.lock
68
-
69
- # Coverage directory used by tools like istanbul
70
- coverage/
71
- .nyc_output/
72
-
73
- # Dependencies lock files (package-lock.json will be copied separately)
74
- package-lock.json
75
- yarn.lock
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.env DELETED
@@ -1,15 +0,0 @@
1
- # Environment Variables for LocalLend Frontend
2
-
3
- # API Configuration
4
- VITE_API_BASE_URL=http://localhost:8080
5
-
6
- # Image/Upload Configuration - DEPRECATED
7
- # Images are now stored in Cloudinary (cloud storage) and return full URLs
8
- # This local upload endpoint is no longer used
9
- # VITE_IMAGE_BASE_URL=http://localhost:8080/uploads
10
-
11
- # App Configuration
12
- VITE_APP_TITLE=LocalLend
13
-
14
- # Development Configuration
15
- VITE_NODE_ENV=development
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.env.example DELETED
@@ -1,18 +0,0 @@
1
- # Environment Variables for LocalLend Frontend
2
-
3
- # API Configuration
4
- VITE_API_BASE_URL=http://localhost:8080
5
-
6
- # Image/Upload Configuration - DEPRECATED
7
- # Images are now stored in Cloudinary (cloud storage) and return full URLs
8
- # This local upload endpoint is no longer used
9
- # VITE_IMAGE_BASE_URL=http://localhost:8080/uploads
10
-
11
- # App Configuration
12
- VITE_APP_TITLE=LocalLend
13
-
14
- # Development Settings
15
- VITE_DEV_MODE=true
16
-
17
- # Note: All environment variables in Vite must be prefixed with VITE_
18
- # These variables will be available in the application via import.meta.env
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.env.production DELETED
@@ -1,18 +0,0 @@
1
- # Production Environment Variables for Docker Container
2
-
3
- # API Configuration - Update this to point to your backend service
4
- VITE_API_BASE_URL=http://localhost:8080
5
-
6
- # Image/Upload Configuration - DEPRECATED
7
- # Images are now stored in Cloudinary (cloud storage) and return full URLs
8
- # This local upload endpoint is no longer used
9
- # VITE_IMAGE_BASE_URL=http://localhost:8080/uploads
10
-
11
- # App Configuration
12
- VITE_APP_TITLE=LocalLend
13
-
14
- # Production Settings
15
- NODE_ENV=production
16
-
17
- # Note: In Docker Compose, you might want to use service names:
18
- # VITE_API_BASE_URL=http://locallend-backend:8080
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/.gitignore DELETED
@@ -1,30 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
-
10
- node_modules
11
- dist
12
- dist-ssr
13
- *.local
14
-
15
- # Environment files
16
- .env
17
- .env.local
18
- .env.*.local
19
-
20
-
21
- # Editor directories and files
22
- .vscode/*
23
- !.vscode/extensions.json
24
- .idea
25
- .DS_Store
26
- *.suo
27
- *.ntvs*
28
- *.njsproj
29
- *.sln
30
- *.sw?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/DOCKER-SUMMARY.md DELETED
@@ -1,119 +0,0 @@
1
- # 🐳 LocalLend Frontend - Docker Setup Complete!
2
-
3
- ## 📋 What's Been Created
4
-
5
- Your LocalLend frontend is now fully containerized with a complete Docker setup:
6
-
7
- ### 🔧 Docker Configuration Files
8
-
9
- - **`Dockerfile`** - Production build with Nginx serving
10
- - **`Dockerfile.dev`** - Development build with hot reload
11
- - **`docker-compose.yml`** - Production orchestration
12
- - **`docker-compose.dev.yml`** - Development orchestration
13
- - **`nginx.conf`** - Nginx configuration with React Router support
14
- - **`.dockerignore`** - Optimized build context
15
- - **`.env.production`** - Production environment variables
16
- - **`healthcheck.sh`** - Container health monitoring
17
-
18
- ### 🚀 Quick Commands
19
-
20
- ```bash
21
- # Development (with hot reload)
22
- npm run docker:dev
23
-
24
- # Production build and run
25
- npm run docker:compose:build
26
-
27
- # View development logs
28
- npm run docker:dev:logs
29
-
30
- # Stop all containers
31
- npm run docker:stop
32
- ```
33
-
34
- ## 🏗 Architecture
35
-
36
- ### Production Container
37
- - **Multi-stage build**: Node.js build → Nginx serve
38
- - **Size optimized**: ~50MB final image
39
- - **Performance**: Gzip compression, asset caching
40
- - **Security**: Security headers, non-root user
41
- - **Health checks**: Automatic container monitoring
42
-
43
- ### Development Container
44
- - **Hot reload**: Live code changes
45
- - **Volume mounting**: Source code sync
46
- - **Port mapping**: 5173 (Vite dev server)
47
- - **Fast rebuilds**: Cached node_modules
48
-
49
- ## 🌐 Usage Options
50
-
51
- ### Option 1: Development with Docker
52
- ```bash
53
- # Start development container with hot reload
54
- npm run docker:dev
55
-
56
- # Access at: http://localhost:5173
57
- # Code changes will auto-reload
58
- ```
59
-
60
- ### Option 2: Production with Docker
61
- ```bash
62
- # Build and run production container
63
- npm run docker:compose:build
64
-
65
- # Access at: http://localhost:3000
66
- # Served by Nginx with optimizations
67
- ```
68
-
69
- ### Option 3: Backend Integration
70
- Update `.env.production` for your backend:
71
- ```env
72
- # Local backend
73
- VITE_API_BASE_URL=http://localhost:8080
74
-
75
- # Docker backend (same network)
76
- VITE_API_BASE_URL=http://locallend-backend:8080
77
- ```
78
-
79
- ## 🔗 Full Stack Docker (Optional)
80
-
81
- To run backend + frontend + database together, uncomment the backend services in `docker-compose.yml`:
82
-
83
- ```bash
84
- # Edit docker-compose.yml and uncomment backend/mongodb
85
- # Then run:
86
- docker-compose up --build -d
87
-
88
- # Services available:
89
- # Frontend: http://localhost:3000
90
- # Backend: http://localhost:8080
91
- # MongoDB: localhost:27017
92
- ```
93
-
94
- ## ✅ Benefits of This Docker Setup
95
-
96
- 1. **Consistent Environment**: Same runtime everywhere
97
- 2. **Easy Deployment**: Single container deployment
98
- 3. **Development Parity**: Dev/prod environment matching
99
- 4. **Scalability**: Ready for orchestration (Kubernetes, etc.)
100
- 5. **Isolation**: No dependency conflicts
101
- 6. **Performance**: Optimized for production serving
102
- 7. **Monitoring**: Built-in health checks
103
-
104
- ## 📚 Documentation
105
-
106
- - **`DOCKER.md`** - Complete Docker setup guide
107
- - **`README-LOCALLEND.md`** - Full project documentation
108
- - **`SETUP.md`** - Installation troubleshooting
109
-
110
- ---
111
-
112
- ## 🚀 Next Steps
113
-
114
- 1. **Start Development**: `npm run docker:dev`
115
- 2. **Configure Backend**: Update API URLs in environment files
116
- 3. **Test Integration**: Verify frontend → backend communication
117
- 4. **Deploy**: Use production Docker setup for deployment
118
-
119
- Your LocalLend frontend is now production-ready with Docker! 🎉
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/DOCKER.md DELETED
@@ -1,219 +0,0 @@
1
- # LocalLend Frontend - Docker Setup Guide
2
-
3
- ## 🐳 Docker Containerization
4
-
5
- This guide explains how to run the LocalLend frontend application using Docker.
6
-
7
- ## 📋 Prerequisites
8
-
9
- - **Docker Desktop** installed and running
10
- - **Docker Compose** (included with Docker Desktop)
11
- - **Backend API** running (either locally or in Docker)
12
-
13
- ## 🚀 Quick Start with Docker
14
-
15
- ### Option 1: Using Docker Compose (Recommended)
16
-
17
- ```bash
18
- # Build and start the container
19
- npm run docker:compose:build
20
-
21
- # Or use docker-compose directly
22
- docker-compose up --build -d
23
- ```
24
-
25
- The frontend will be available at: `http://localhost:3000`
26
-
27
- ### Option 2: Using Docker directly
28
-
29
- ```bash
30
- # Build the Docker image
31
- npm run docker:build
32
-
33
- # Run the container
34
- npm run docker:run
35
-
36
- # Or use docker commands directly
37
- docker build -t locallend-frontend .
38
- docker run -p 3000:80 locallend-frontend
39
- ```
40
-
41
- ## 🔧 Docker Configuration Files
42
-
43
- ### Dockerfile (Multi-stage build)
44
- - **Stage 1 (Builder):** Installs dependencies and builds the React app
45
- - **Stage 2 (Production):** Serves the built app using Nginx
46
-
47
- ### docker-compose.yml
48
- - Orchestrates the frontend container
49
- - Includes optional backend and MongoDB services (commented out)
50
- - Sets up networking between services
51
-
52
- ### nginx.conf
53
- - Handles client-side routing for React Router
54
- - Serves static assets with proper caching
55
- - Includes security headers and gzip compression
56
-
57
- ## 🌐 Environment Configuration
58
-
59
- ### Production Environment (.env.production)
60
- ```env
61
- VITE_API_BASE_URL=http://localhost:8080
62
- VITE_IMAGE_BASE_URL=http://localhost:8080/uploads
63
- VITE_APP_TITLE=LocalLend
64
- ```
65
-
66
- ### Docker Compose Environment
67
- If using Docker Compose with backend services, update the API URL:
68
- ```env
69
- VITE_API_BASE_URL=http://locallend-backend:8080
70
- ```
71
-
72
- ## 📦 Container Details
73
-
74
- - **Base Image:** node:18-alpine (for building), nginx:alpine (for serving)
75
- - **Port:** 80 inside container, mapped to 3000 on host
76
- - **Size:** Optimized multi-stage build (~50MB final image)
77
- - **Restart Policy:** unless-stopped
78
-
79
- ## 🛠 Docker Commands
80
-
81
- ```bash
82
- # Build and run with compose
83
- docker-compose up --build -d
84
-
85
- # View logs
86
- docker-compose logs -f locallend-frontend
87
-
88
- # Stop containers
89
- docker-compose down
90
-
91
- # Rebuild only frontend
92
- docker-compose build locallend-frontend
93
-
94
- # Access container shell (for debugging)
95
- docker exec -it locallend-frontend_locallend-frontend_1 sh
96
-
97
- # View Nginx config inside container
98
- docker exec locallend-frontend_locallend-frontend_1 cat /etc/nginx/conf.d/default.conf
99
- ```
100
-
101
- ## 🔗 Backend Integration
102
-
103
- ### Option 1: Backend running locally
104
- ```env
105
- # In .env.production
106
- VITE_API_BASE_URL=http://localhost:8080
107
- ```
108
-
109
- ### Option 2: Backend in Docker (same network)
110
- ```env
111
- # In .env.production
112
- VITE_API_BASE_URL=http://locallend-backend:8080
113
- ```
114
-
115
- ### Option 3: Nginx proxy (recommended for production)
116
- Uncomment the API proxy section in `nginx.conf`:
117
- ```nginx
118
- location /api/ {
119
- proxy_pass http://backend:8080;
120
- proxy_set_header Host $host;
121
- proxy_set_header X-Real-IP $remote_addr;
122
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
123
- proxy_set_header X-Forwarded-Proto $scheme;
124
- }
125
- ```
126
-
127
- ## 🏗 Full Stack Docker Setup
128
-
129
- To run the entire LocalLend application with Docker:
130
-
131
- 1. **Uncomment backend services** in `docker-compose.yml`
132
- 2. **Build/obtain backend Docker image**
133
- 3. **Update environment variables** for service communication
134
- 4. **Run the full stack:**
135
- ```bash
136
- docker-compose up --build -d
137
- ```
138
-
139
- Services will be available at:
140
- - Frontend: `http://localhost:3000`
141
- - Backend API: `http://localhost:8080`
142
- - MongoDB: `localhost:27017`
143
-
144
- ## 🔍 Troubleshooting
145
-
146
- ### Build Issues
147
- ```bash
148
- # Clear Docker cache
149
- docker system prune -a
150
-
151
- # Build without cache
152
- docker-compose build --no-cache
153
-
154
- # Check build logs
155
- docker-compose logs locallend-frontend
156
- ```
157
-
158
- ### Network Issues
159
- ```bash
160
- # Check container networks
161
- docker network ls
162
-
163
- # Inspect network
164
- docker network inspect locallend-frontend_locallend-network
165
- ```
166
-
167
- ### Container Access
168
- ```bash
169
- # Check running containers
170
- docker ps
171
-
172
- # Access container logs
173
- docker logs <container-id>
174
-
175
- # Shell into container
176
- docker exec -it <container-id> sh
177
- ```
178
-
179
- ## 📊 Performance Optimization
180
-
181
- The Docker setup includes:
182
- - **Multi-stage builds** to minimize image size
183
- - **Nginx gzip compression** for faster loading
184
- - **Static asset caching** with proper headers
185
- - **Production build optimization** via Vite
186
-
187
- ## 🔒 Security Features
188
-
189
- - Non-root user in container
190
- - Security headers (X-Frame-Options, X-Content-Type-Options, etc.)
191
- - Minimal attack surface with Alpine Linux
192
- - No sensitive data in container layers
193
-
194
- ---
195
-
196
- ## 🚀 Production Deployment
197
-
198
- For production deployment:
199
-
200
- 1. **Update environment variables** for production URLs
201
- 2. **Configure HTTPS** (add SSL certificates to nginx.conf)
202
- 3. **Set up proper logging** and monitoring
203
- 4. **Use Docker secrets** for sensitive data
204
- 5. **Implement health checks** in docker-compose.yml
205
-
206
- Example production docker-compose:
207
- ```yaml
208
- services:
209
- locallend-frontend:
210
- image: locallend-frontend:latest
211
- restart: always
212
- environment:
213
- - VITE_API_BASE_URL=https://api.yourdomain.com
214
- healthcheck:
215
- test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80/"]
216
- interval: 30s
217
- timeout: 10s
218
- retries: 3
219
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/Dockerfile DELETED
@@ -1,44 +0,0 @@
1
- # Use the official Node.js runtime as the base image
2
- FROM node:18-alpine AS builder
3
-
4
- # Set the working directory in the container
5
- WORKDIR /app
6
-
7
- # Copy package.json and package-lock.json (if available)
8
- COPY package*.json ./
9
-
10
- # Install dependencies (using npm install since package-lock.json may not exist)
11
- RUN npm install --legacy-peer-deps
12
-
13
- # Copy the rest of the application code
14
- COPY . .
15
-
16
- # Accept build argument for API URL
17
- ARG VITE_API_BASE_URL=http://localhost:8080
18
- ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
19
-
20
- # Build the application for production
21
- RUN npm run build
22
-
23
- # Production stage - Use Nginx to serve the built app
24
- FROM nginx:alpine AS production
25
-
26
- # Copy the built app from the builder stage
27
- COPY --from=builder /app/dist /usr/share/nginx/html
28
-
29
- # Copy custom nginx configuration
30
- COPY nginx.conf /etc/nginx/conf.d/default.conf
31
-
32
- # Copy health check script
33
- COPY healthcheck.sh /usr/local/bin/healthcheck.sh
34
- RUN chmod +x /usr/local/bin/healthcheck.sh
35
-
36
- # Add health check
37
- HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
38
- CMD /usr/local/bin/healthcheck.sh
39
-
40
- # Expose port 80
41
- EXPOSE 80
42
-
43
- # Start Nginx
44
- CMD ["nginx", "-g", "daemon off;"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/Dockerfile.dev DELETED
@@ -1,21 +0,0 @@
1
- # Development Dockerfile - Hot reload enabled
2
- FROM node:20-alpine
3
-
4
- # Set working directory
5
- WORKDIR /app
6
-
7
- # Install patch-package globally to fix the missing dependency
8
- RUN npm install -g patch-package
9
-
10
- # Install dependencies
11
- COPY package*.json ./
12
- RUN npm install --legacy-peer-deps
13
-
14
- # Copy source code
15
- COPY . .
16
-
17
- # Expose development port
18
- EXPOSE 5173
19
-
20
- # Start development server
21
- CMD ["npm", "run", "dev", "--", "--host"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/ENDPOINT_FIX_SUMMARY.md DELETED
@@ -1,184 +0,0 @@
1
- # FIXED: Item Status Toggle - Wrong Endpoint Issue
2
-
3
- ## 🎯 **Root Cause Identified**
4
-
5
- **The frontend was calling the wrong API endpoint!**
6
-
7
- ### **Frontend was calling:**
8
- ```
9
- PATCH /api/items/{id}/status
10
- ```
11
-
12
- ### **Backend actually has:**
13
- ```
14
- PATCH /api/items/{id}/toggle-availability
15
- PATCH /api/items/{id}/availability
16
- ```
17
-
18
- This mismatch caused the "No static resource" error because the backend doesn't have a `/status` endpoint.
19
-
20
- ---
21
-
22
- ## 🔧 **Frontend Fixes Applied**
23
-
24
- ### **1. Updated DashboardPage.tsx**
25
- ```typescript
26
- // OLD (wrong endpoint)
27
- await fetch(`/api/items/${itemId}/status`, {
28
- method: 'PATCH',
29
- body: JSON.stringify({ status: newStatus })
30
- });
31
-
32
- // NEW (correct endpoints with fallback)
33
- // Try toggle endpoint first
34
- let response = await fetch(`/api/items/${itemId}/toggle-availability`, {
35
- method: 'PATCH' // No body needed for toggle
36
- });
37
-
38
- // If 404, try availability endpoint
39
- if (!response.ok && response.status === 404) {
40
- response = await fetch(`/api/items/${itemId}/availability`, {
41
- method: 'PATCH',
42
- body: JSON.stringify({ status: newStatus })
43
- });
44
- }
45
- ```
46
-
47
- ### **2. Updated itemService.ts**
48
- ```typescript
49
- // OLD (wrong endpoint)
50
- updateItemStatus: async (itemId: string, status: ItemStatus, userId: string) => {
51
- return await api.patch(`/api/items/${itemId}/status`, { status }, {
52
- headers: addUserIdHeader(userId)
53
- });
54
- }
55
-
56
- // NEW (correct endpoints with fallback)
57
- updateItemStatus: async (itemId: string, status: ItemStatus, userId: string) => {
58
- try {
59
- // Try toggle endpoint first (simpler)
60
- return await api.patch(`/api/items/${itemId}/toggle-availability`, null, {
61
- headers: addUserIdHeader(userId)
62
- });
63
- } catch (error) {
64
- // Fallback to availability endpoint
65
- if (error?.response?.status === 404) {
66
- return await api.patch(`/api/items/${itemId}/availability`, { status }, {
67
- headers: addUserIdHeader(userId)
68
- });
69
- }
70
- throw error;
71
- }
72
- }
73
- ```
74
-
75
- ---
76
-
77
- ## 🧪 **Backend Testing**
78
-
79
- ### **Test Toggle Endpoint:**
80
- ```bash
81
- curl -X PATCH http://localhost:8080/api/items/test123/toggle-availability \
82
- -H "Content-Type: application/json" \
83
- -H "Authorization: Bearer {TOKEN}" \
84
- -H "X-User-Id: {USER_ID}"
85
- ```
86
-
87
- ### **Test Availability Endpoint:**
88
- ```bash
89
- curl -X PATCH http://localhost:8080/api/items/test123/availability \
90
- -H "Content-Type: application/json" \
91
- -H "Authorization: Bearer {TOKEN}" \
92
- -H "X-User-Id: {USER_ID}" \
93
- -d '{"status":"UNAVAILABLE"}'
94
- ```
95
-
96
- ### **Expected Responses:**
97
- - ✅ **200 OK** with updated item JSON = Working correctly
98
- - ✅ **401 Unauthorized** = Auth required (expected without valid token)
99
- - ✅ **403 Forbidden** = Permission issue (expected if not owner)
100
- - ❌ **404 Not Found** = Endpoint doesn't exist
101
- - ❌ **"No static resource"** = API server not running
102
-
103
- ---
104
-
105
- ## 🎯 **What Should Happen Now**
106
-
107
- ### **With Toggle Endpoint (`/toggle-availability`):**
108
- 1. User clicks "Make Unavailable"
109
- 2. Frontend calls PATCH `/api/items/{id}/toggle-availability`
110
- 3. Backend toggles status (AVAILABLE → UNAVAILABLE)
111
- 4. Returns updated item with new status
112
- 5. Frontend updates UI to show new status
113
-
114
- ### **With Availability Endpoint (`/availability`):**
115
- 1. User clicks "Make Unavailable"
116
- 2. Frontend calls PATCH `/api/items/{id}/availability` with `{"status":"UNAVAILABLE"}`
117
- 3. Backend sets specific status
118
- 4. Returns updated item with new status
119
- 5. Frontend updates UI to show new status
120
-
121
- ---
122
-
123
- ## 🔍 **Debugging Steps**
124
-
125
- ### **1. Test which endpoint your backend has:**
126
- ```bash
127
- # Test toggle
128
- curl -X PATCH http://localhost:8080/api/items/test/toggle-availability
129
-
130
- # Test availability
131
- curl -X PATCH http://localhost:8080/api/items/test/availability
132
- ```
133
-
134
- ### **2. Check frontend logs:**
135
- - Look for "📡 Used endpoint:" in console
136
- - Should show either `/toggle-availability` or `/availability`
137
- - Should NOT show `/status` anymore
138
-
139
- ### **3. Verify success:**
140
- - Click "Make Unavailable" button
141
- - Should see "✅ Item status updated successfully"
142
- - Button should change to "Make Available"
143
- - Status badge should update color
144
-
145
- ---
146
-
147
- ## 💡 **Backend Colleague Notes**
148
-
149
- ### **If using toggle endpoint:**
150
- ```java
151
- @PatchMapping("/api/items/{itemId}/toggle-availability")
152
- public ResponseEntity<Item> toggleAvailability(@PathVariable String itemId, @RequestHeader("X-User-Id") String userId) {
153
- // Toggle between AVAILABLE ↔ UNAVAILABLE
154
- // Don't change if status is BORROWED
155
- }
156
- ```
157
-
158
- ### **If using availability endpoint:**
159
- ```java
160
- @PatchMapping("/api/items/{itemId}/availability")
161
- public ResponseEntity<Item> setAvailability(@PathVariable String itemId, @RequestBody Map<String, String> body, @RequestHeader("X-User-Id") String userId) {
162
- String status = body.get("status");
163
- // Set to specific status (AVAILABLE/UNAVAILABLE)
164
- }
165
- ```
166
-
167
- ### **Both should:**
168
- - ✅ Validate user owns the item
169
- - ✅ Return updated item JSON
170
- - ✅ Handle authentication/authorization
171
- - ✅ Prevent changing BORROWED items
172
-
173
- ---
174
-
175
- ## 🎉 **Expected Result**
176
-
177
- The "Make Unavailable" button should now work correctly because:
178
-
179
- 1. ✅ **Correct API endpoints** - Using actual backend endpoints
180
- 2. ✅ **Fallback logic** - Tries both endpoints to find which works
181
- 3. ✅ **Proper error handling** - Shows specific error messages
182
- 4. ✅ **Enhanced debugging** - Logs which endpoint was used
183
-
184
- **The static resource error should be gone!** The frontend now calls the endpoints that actually exist on your backend.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/FIELD_NAME_FIX.md DELETED
@@ -1,58 +0,0 @@
1
- # FIXED: Field Name Error - "isAvailable" Required
2
-
3
- ## 🎯 **Backend Error Message**
4
- ```
5
- "isAvailable field is required"
6
- ```
7
-
8
- **Issue:** Backend expects field name `isAvailable` (not `available`)
9
-
10
- ---
11
-
12
- ## 🔧 **Quick Fix Applied**
13
-
14
- ### **BEFORE (Wrong field name):**
15
- ```json
16
- {
17
- "available": true // ❌ Wrong field name
18
- }
19
- ```
20
-
21
- ### **AFTER (Correct field name):**
22
- ```json
23
- {
24
- "isAvailable": true // ✅ Correct field name
25
- }
26
- ```
27
-
28
- ---
29
-
30
- ## 📁 **Files Updated:**
31
-
32
- ### **1. itemService.ts**
33
- ```typescript
34
- // Changed from:
35
- { available: isAvailable }
36
-
37
- // To:
38
- { isAvailable } // ES6 shorthand for { isAvailable: isAvailable }
39
- ```
40
-
41
- ### **2. DashboardPage.tsx**
42
- ```typescript
43
- // Changed from:
44
- body: JSON.stringify({ available: isAvailable })
45
-
46
- // To:
47
- body: JSON.stringify({ isAvailable })
48
- ```
49
-
50
- ---
51
-
52
- ## ✅ **Should Work Now:**
53
-
54
- - **"Make Unavailable"** → Sends `{"isAvailable": false}`
55
- - **"Make Available"** → Sends `{"isAvailable": true}`
56
- - **No more field validation errors** from backend
57
-
58
- The backend should now accept the requests with the correct `isAvailable` field name!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/FRONTEND_ENV_FIXED.md DELETED
@@ -1,260 +0,0 @@
1
- # Frontend Environment Variables - Fixed! 🔧
2
-
3
- ## 🚨 Issue Identified
4
-
5
- The frontend `.env` file had a **conflict** with the new Cloudinary implementation:
6
-
7
- 1. ❌ `.env` was **NOT gitignored** (could leak configuration)
8
- 2. ❌ `VITE_IMAGE_BASE_URL=http://localhost:8080/uploads` pointed to non-existent local upload endpoint
9
- 3. ❌ Suggested local file storage (incompatible with Cloudinary cloud storage)
10
-
11
- ## ✅ Solution Applied
12
-
13
- ### 1. Added `.env` to `.gitignore`
14
-
15
- **File: `locallend-frontend/.gitignore`**
16
-
17
- Added:
18
- ```gitignore
19
- # Environment files
20
- .env
21
- .env.local
22
- .env.*.local
23
- ```
24
-
25
- Now the `.env` file won't be accidentally committed!
26
-
27
- ### 2. Deprecated `VITE_IMAGE_BASE_URL`
28
-
29
- **Files Updated:**
30
- - `locallend-frontend/.env`
31
- - `locallend-frontend/.env.example`
32
- - `locallend-frontend/.env.production`
33
-
34
- **Before:**
35
- ```bash
36
- VITE_IMAGE_BASE_URL=http://localhost:8080/uploads
37
- ```
38
-
39
- **After:**
40
- ```bash
41
- # Image/Upload Configuration - DEPRECATED
42
- # Images are now stored in Cloudinary (cloud storage) and return full URLs
43
- # This local upload endpoint is no longer used
44
- # VITE_IMAGE_BASE_URL=http://localhost:8080/uploads
45
- ```
46
-
47
- ### 3. Updated TypeScript Definitions
48
-
49
- **File: `src/vite-env.d.ts`**
50
-
51
- Made `VITE_IMAGE_BASE_URL` optional:
52
- ```typescript
53
- interface ImportMetaEnv {
54
- readonly VITE_API_BASE_URL: string;
55
- readonly VITE_IMAGE_BASE_URL?: string; // DEPRECATED: Images use Cloudinary full URLs
56
- readonly VITE_APP_TITLE: string;
57
- }
58
- ```
59
-
60
- ### 4. Enhanced `getImageUrl` Helper
61
-
62
- **File: `src/utils/helpers.ts`**
63
-
64
- Updated to properly handle Cloudinary URLs:
65
-
66
- ```typescript
67
- export const getImageUrl = (imagePath: string): string => {
68
- if (!imagePath) return '/placeholder-image.jpg';
69
-
70
- // If already a full URL (Cloudinary or other), return as-is
71
- if (imagePath.startsWith('http://') || imagePath.startsWith('https://')) {
72
- return imagePath;
73
- }
74
-
75
- // Legacy support for relative paths (backward compatibility)
76
- if (import.meta.env.VITE_IMAGE_BASE_URL) {
77
- return `${import.meta.env.VITE_IMAGE_BASE_URL}${imagePath}`;
78
- }
79
-
80
- return imagePath;
81
- };
82
- ```
83
-
84
- **Benefits:**
85
- - ✅ Works with Cloudinary full URLs (primary use case)
86
- - ✅ Backward compatible with relative paths
87
- - ✅ Proper fallback to placeholder
88
- - ✅ Well-documented with JSDoc
89
-
90
- ## 📊 Comparison: Local vs Cloudinary Storage
91
-
92
- ### Local File Storage (Old Approach - Deprecated)
93
-
94
- ```
95
- Frontend Upload Flow:
96
- 1. User selects image
97
- 2. Upload to /api/uploads endpoint
98
- 3. Server saves to disk: /uploads/image123.jpg
99
- 4. Server returns: "/image123.jpg"
100
- 5. Frontend combines: VITE_IMAGE_BASE_URL + "/image123.jpg"
101
- 6. Result: "http://localhost:8080/uploads/image123.jpg"
102
-
103
- Problems:
104
- ❌ Files stored on server disk
105
- ❌ Not Docker-friendly
106
- ❌ No CDN/optimization
107
- ❌ Manual cleanup needed
108
- ❌ Doesn't scale
109
- ```
110
-
111
- ### Cloudinary Storage (New Approach - Implemented)
112
-
113
- ```
114
- Frontend Upload Flow:
115
- 1. User selects image
116
- 2. Upload to /api/images/upload endpoint
117
- 3. Server uploads to Cloudinary cloud
118
- 4. Cloudinary returns full URL
119
- 5. Server returns: "https://res.cloudinary.com/demo/image/upload/v1234/locallend/items/abc123.jpg"
120
- 6. Frontend uses URL directly (no base URL needed)
121
-
122
- Benefits:
123
- ✅ Cloud storage (infinite capacity)
124
- ✅ Works everywhere (Docker, cloud, local)
125
- ✅ CDN delivery (fast worldwide)
126
- ✅ Automatic optimization
127
- ✅ Image transformations
128
- ✅ Scales automatically
129
- ✅ No base URL concatenation needed
130
- ```
131
-
132
- ## 🎯 How Images Work Now
133
-
134
- ### Backend (Spring Boot)
135
-
136
- 1. **Upload Endpoint:** `POST /api/images/upload`
137
- - Accepts `multipart/form-data` with file
138
- - Uploads to Cloudinary
139
- - Returns **full Cloudinary URL**
140
-
141
- 2. **Item Creation:** `POST /api/items`
142
- - Accepts array of image URLs
143
- - Stores full URLs in MongoDB
144
-
145
- ### Frontend (React)
146
-
147
- 1. **Upload (AddItemPage):**
148
- ```typescript
149
- // User selects files
150
- const formData = new FormData();
151
- formData.append('file', file);
152
-
153
- // Upload to backend
154
- const response = await fetch('/api/images/upload', {
155
- method: 'POST',
156
- body: formData
157
- });
158
-
159
- const result = await response.json();
160
- // result.url = "https://res.cloudinary.com/.../image.jpg"
161
- ```
162
-
163
- 2. **Display (DashboardPage, SearchPage, etc):**
164
- ```typescript
165
- // Item has full Cloudinary URLs
166
- <img src={item.images[0]} alt={item.name} />
167
- // Renders: <img src="https://res.cloudinary.com/.../image.jpg" />
168
- ```
169
-
170
- 3. **Using Helper (Optional):**
171
- ```typescript
172
- import { getImageUrl } from '@/utils/helpers';
173
-
174
- <img src={getImageUrl(item.images[0])} alt={item.name} />
175
- // Helper passes through full URLs unchanged
176
- ```
177
-
178
- ## 🔍 No More Conflicts!
179
-
180
- ### Before (Conflicting Approaches)
181
-
182
- ```
183
- .env says: Use /uploads endpoint
184
- Backend implements: Cloudinary cloud storage
185
- Frontend expects: Relative paths + base URL
186
- Result: CONFLICT! 😱
187
- ```
188
-
189
- ### After (Unified Approach)
190
-
191
- ```
192
- .env says: Use Cloudinary (deprecated old config)
193
- Backend implements: Cloudinary cloud storage ✅
194
- Frontend uses: Full Cloudinary URLs ✅
195
- Result: WORKS PERFECTLY! 🎉
196
- ```
197
-
198
- ## 📝 What to Do Next
199
-
200
- ### For Development:
201
-
202
- 1. **Pull latest changes:**
203
- ```bash
204
- git pull origin prishiv_dev
205
- ```
206
-
207
- 2. **The `.env` file is now gitignored**, so you need to:
208
- ```bash
209
- cd locallend-frontend
210
- cp .env.example .env
211
- ```
212
-
213
- 3. **No configuration needed!**
214
- - `VITE_API_BASE_URL` still points to `http://localhost:8080`
215
- - `VITE_IMAGE_BASE_URL` is deprecated (not needed)
216
- - Images work automatically with Cloudinary
217
-
218
- 4. **Start frontend:**
219
- ```bash
220
- npm run dev
221
- ```
222
-
223
- ### For Production:
224
-
225
- Update `VITE_API_BASE_URL` to point to your production backend:
226
-
227
- ```bash
228
- # .env.production
229
- VITE_API_BASE_URL=https://your-production-api.com
230
- ```
231
-
232
- Images will work automatically since Cloudinary provides full URLs.
233
-
234
- ## ✅ Verification Checklist
235
-
236
- - [x] `.env` added to `.gitignore`
237
- - [x] `VITE_IMAGE_BASE_URL` deprecated (commented out)
238
- - [x] TypeScript types updated (optional property)
239
- - [x] `getImageUrl` helper enhanced for Cloudinary
240
- - [x] Backward compatibility maintained
241
- - [x] Documentation updated
242
- - [x] No breaking changes for existing code
243
-
244
- ## 🎓 Key Takeaways
245
-
246
- 1. **Cloudinary URLs are complete** - No base URL needed
247
- 2. **`.env` files should be gitignored** - Prevents config leaks
248
- 3. **Helper functions still work** - Pass-through for full URLs
249
- 4. **Backward compatible** - Old code won't break
250
- 5. **Future-proof** - Can switch storage providers if needed
251
-
252
- ## 📚 Related Documentation
253
-
254
- - **Backend:** `ENV_SETUP.md` - Environment variable setup
255
- - **Images:** `IMAGE_UPLOAD_SETUP.md` - Complete image upload guide
256
- - **Quick Start:** `IMAGE_UPLOAD_QUICKSTART.md` - 5-minute setup
257
-
258
- ---
259
-
260
- **No more conflicts!** The frontend and backend now work together seamlessly with Cloudinary. 🚀
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/FRONTEND_INTEGRATION_GUIDE.txt DELETED
@@ -1,1510 +0,0 @@
1
- ================================================================================
2
- LOCALLEND BACKEND API INTEGRATION GUIDE
3
- Frontend Development Reference
4
- ================================================================================
5
-
6
- TABLE OF CONTENTS
7
- -----------------
8
- 1. System Overview
9
- 2. Authentication & Authorization
10
- 3. API Endpoints Reference
11
- 4. Data Models & DTOs
12
- 5. Frontend Pages/Views Required
13
- 6. Common Workflows
14
- 7. Error Handling
15
- 8. Best Practices
16
-
17
-
18
- ================================================================================
19
- 1. SYSTEM OVERVIEW
20
- ================================================================================
21
-
22
- LocalLend is a peer-to-peer item sharing platform where users can:
23
- - List items they own for others to borrow
24
- - Browse and search items available in their community
25
- - Request to borrow items from other users
26
- - Manage bookings (approve, track, complete)
27
- - Rate users and items after completed transactions
28
- - Build trust scores through positive interactions
29
-
30
- TECHNOLOGY STACK:
31
- - Backend: Spring Boot 3.5.6 with MongoDB
32
- - Authentication: JWT (JSON Web Tokens)
33
- - API Style: RESTful JSON APIs
34
- - Security: Spring Security with role-based access
35
-
36
-
37
- ================================================================================
38
- 2. AUTHENTICATION & AUTHORIZATION
39
- ================================================================================
40
-
41
- 2.1 AUTHENTICATION FLOW
42
- -----------------------
43
- 1. User registers via POST /api/auth/register
44
- 2. User logs in via POST /api/auth/login → receives JWT token
45
- 3. Include token in all subsequent requests:
46
- Header: Authorization: Bearer <jwt_token>
47
-
48
- 2.2 REQUIRED HEADERS
49
- --------------------
50
- For ALL authenticated requests:
51
- - Authorization: Bearer <jwt_token>
52
-
53
- For item/booking operations (owner/borrower specific):
54
- - Authorization: Bearer <jwt_token>
55
- - X-User-Id: <user_id>
56
-
57
- 2.3 PUBLIC ENDPOINTS (No Authentication Required)
58
- -------------------------------------------------
59
- - POST /api/auth/register
60
- - POST /api/auth/login
61
- - GET /api/categories/**
62
- - GET /api/items/**
63
- - GET /api/users/{userId}/public
64
- - GET /api/users/search
65
-
66
- 2.4 USER ROLES
67
- --------------
68
- - USER: Standard user (default role)
69
- - ADMIN: Administrative access (future feature)
70
-
71
- All users have the same basic permissions but operations are restricted based on
72
- ownership (e.g., only item owner can delete their items).
73
-
74
-
75
- ================================================================================
76
- 3. API ENDPOINTS REFERENCE
77
- ================================================================================
78
-
79
- BASE URL: http://localhost:8080
80
-
81
- ------------------------------------------------------------------------------
82
- 3.1 AUTHENTICATION ENDPOINTS
83
- ------------------------------------------------------------------------------
84
-
85
- POST /api/auth/register
86
- ------------------------
87
- Register a new user account
88
-
89
- Request Body:
90
- {
91
- "username": "string (3-50 chars, required)",
92
- "name": "string (2-100 chars, required)",
93
- "email": "string (valid email, required)",
94
- "password": "string (min 8 chars, required)",
95
- "phoneNumber": "string (optional)"
96
- }
97
-
98
- Response (200):
99
- {
100
- "id": "string",
101
- "username": "string",
102
- "name": "string",
103
- "email": "string",
104
- "phoneNumber": "string",
105
- "profileImageUrl": "string",
106
- "role": "USER",
107
- "isActive": true,
108
- "createdDate": "timestamp",
109
- "trustScore": 5.0,
110
- "itemCount": 0,
111
- "memberSince": "string",
112
- "totalItemsShared": 0,
113
- "totalBookings": 0
114
- }
115
-
116
-
117
- POST /api/auth/login
118
- --------------------
119
- Login and receive JWT token
120
-
121
- Request Body:
122
- {
123
- "usernameOrEmail": "string (required)",
124
- "password": "string (required)"
125
- }
126
-
127
- Response (200):
128
- {
129
- "token": "eyJhbGc...",
130
- "type": "Bearer",
131
- "user": {
132
- "id": "string",
133
- "username": "string",
134
- "name": "string",
135
- "email": "string",
136
- "role": "USER",
137
- "trustScore": 5.0
138
- }
139
- }
140
-
141
-
142
- ------------------------------------------------------------------------------
143
- 3.2 USER ENDPOINTS
144
- ------------------------------------------------------------------------------
145
-
146
- GET /api/users/{userId}/public
147
- -------------------------------
148
- Get public user profile (no authentication required)
149
-
150
- Response (200):
151
- {
152
- "id": "string",
153
- "username": "string",
154
- "name": "string",
155
- "profileImageUrl": "string",
156
- "trustScore": 5.0,
157
- "itemCount": 3,
158
- "memberSince": "Member since November 2025"
159
- }
160
-
161
-
162
- GET /api/users/search?query={searchTerm}
163
- -----------------------------------------
164
- Search users by username or name
165
-
166
- Query Parameters:
167
- - query: string (search term)
168
-
169
- Response (200):
170
- {
171
- "data": [
172
- {
173
- "id": "string",
174
- "username": "string",
175
- "name": "string",
176
- "profileImageUrl": "string",
177
- "trustScore": 5.0,
178
- "itemCount": 3
179
- }
180
- ],
181
- "count": 1
182
- }
183
-
184
-
185
- ------------------------------------------------------------------------------
186
- 3.3 CATEGORY ENDPOINTS
187
- ------------------------------------------------------------------------------
188
-
189
- GET /api/categories
190
- -------------------
191
- Get all categories (public endpoint)
192
-
193
- Query Parameters:
194
- - sort: string (optional, default: "name")
195
-
196
- Response (200):
197
- {
198
- "data": [
199
- {
200
- "id": "string",
201
- "name": "string",
202
- "description": "string",
203
- "parent_category_id": "string",
204
- "parent_category_name": "string",
205
- "is_active": true,
206
- "item_count": 5,
207
- "has_subcategories": false,
208
- "created_at": "timestamp",
209
- "updated_at": "timestamp"
210
- }
211
- ],
212
- "success": true,
213
- "count": 2,
214
- "message": "Categories retrieved successfully"
215
- }
216
-
217
-
218
- POST /api/categories
219
- --------------------
220
- Create a new category (authenticated)
221
-
222
- Headers:
223
- - Authorization: Bearer <token>
224
-
225
- Request Body:
226
- {
227
- "name": "string (required)",
228
- "description": "string (optional)",
229
- "parentCategoryId": "string (optional)"
230
- }
231
-
232
- Response (201): Returns created category object
233
-
234
-
235
- GET /api/categories/{categoryId}
236
- ---------------------------------
237
- Get single category details
238
-
239
- Response (200): Returns category object with subcategories
240
-
241
-
242
- GET /api/categories/search?term={searchTerm}
243
- --------------------------------------------
244
- Search categories by name or description
245
-
246
- Response (200): Returns matching categories
247
-
248
-
249
- ------------------------------------------------------------------------------
250
- 3.4 ITEM ENDPOINTS
251
- ------------------------------------------------------------------------------
252
-
253
- GET /api/items
254
- --------------
255
- Get all items with pagination (public endpoint)
256
-
257
- Query Parameters:
258
- - page: integer (default: 0)
259
- - size: integer (default: 10)
260
- - sort: string (default: "createdAt,desc")
261
- - categoryId: string (optional, filter by category)
262
- - ownerId: string (optional, filter by owner)
263
- - condition: string (optional, NEW|EXCELLENT|GOOD|FAIR|POOR)
264
- - status: string (optional, AVAILABLE|UNAVAILABLE|BORROWED)
265
-
266
- Response (200):
267
- {
268
- "content": [
269
- {
270
- "id": "string",
271
- "name": "string",
272
- "description": "string",
273
- "condition": "EXCELLENT",
274
- "status": "AVAILABLE",
275
- "deposit": 50.0,
276
- "images": ["url1", "url2"],
277
- "averageRating": 4.5,
278
- "ownerId": "string",
279
- "ownerName": "string",
280
- "categoryId": "string",
281
- "categoryName": "string",
282
- "canBeBorrowed": true,
283
- "createdAt": "timestamp",
284
- "updatedAt": "timestamp"
285
- }
286
- ],
287
- "pageable": {...},
288
- "totalElements": 6,
289
- "totalPages": 1,
290
- "first": true,
291
- "last": true
292
- }
293
-
294
-
295
- POST /api/items
296
- ---------------
297
- Create a new item listing (authenticated)
298
-
299
- Headers:
300
- - Authorization: Bearer <token>
301
- - X-User-Id: <user_id>
302
-
303
- Request Body:
304
- {
305
- "name": "string (required)",
306
- "description": "string (required)",
307
- "categoryId": "string (required)",
308
- "deposit": 0.0,
309
- "images": ["url1", "url2"],
310
- "condition": "EXCELLENT" // NEW|EXCELLENT|GOOD|FAIR|POOR
311
- }
312
-
313
- Response (201): Returns created item object
314
-
315
-
316
- GET /api/items/{itemId}
317
- -----------------------
318
- Get single item details
319
-
320
- Response (200): Returns detailed item object
321
-
322
-
323
- PUT /api/items/{itemId}
324
- -----------------------
325
- Update item details (owner only)
326
-
327
- Headers:
328
- - Authorization: Bearer <token>
329
- - X-User-Id: <user_id>
330
-
331
- Request Body:
332
- {
333
- "name": "string",
334
- "description": "string",
335
- "categoryId": "string",
336
- "deposit": 50.0,
337
- "condition": "GOOD"
338
- }
339
-
340
- Response (200): Returns updated item object
341
-
342
-
343
- DELETE /api/items/{itemId}
344
- --------------------------
345
- Delete item (owner only, must have no active bookings)
346
-
347
- Headers:
348
- - Authorization: Bearer <token>
349
- - X-User-Id: <user_id>
350
-
351
- Response (204): No content on success
352
-
353
-
354
- PATCH /api/items/{itemId}/status
355
- ---------------------------------
356
- Update item availability status (owner only)
357
-
358
- Headers:
359
- - Authorization: Bearer <token>
360
- - X-User-Id: <user_id>
361
-
362
- Request Body:
363
- {
364
- "status": "AVAILABLE" // AVAILABLE|UNAVAILABLE|BORROWED
365
- }
366
-
367
- Response (200): Returns updated item object
368
-
369
-
370
- GET /api/items/search?query={searchTerm}
371
- ----------------------------------------
372
- Search items by name or description
373
-
374
- Query Parameters:
375
- - query: string (required)
376
- - categoryId: string (optional)
377
- - condition: string (optional)
378
-
379
- Response (200): Returns matching items
380
-
381
-
382
- ------------------------------------------------------------------------------
383
- 3.5 BOOKING ENDPOINTS
384
- ------------------------------------------------------------------------------
385
-
386
- POST /api/bookings
387
- ------------------
388
- Create a new booking request
389
-
390
- Headers:
391
- - Authorization: Bearer <token>
392
- - X-User-Id: <borrower_id>
393
-
394
- Request Body:
395
- {
396
- "itemId": "string (required)",
397
- "startDate": "2025-11-10T10:00:00 (required, future date)",
398
- "endDate": "2025-11-13T10:00:00 (required, after start date)",
399
- "bookingNotes": "string (max 500 chars, optional)",
400
- "depositAmount": 100.0,
401
- "requestedDurationDays": 3,
402
- "acceptTerms": true (required, must be true)
403
- }
404
-
405
- Response (201):
406
- {
407
- "id": "string",
408
- "itemId": "string",
409
- "itemName": "string",
410
- "borrowerId": "string",
411
- "borrowerName": "string",
412
- "ownerId": "string",
413
- "ownerName": "string",
414
- "status": "PENDING",
415
- "startDate": "timestamp",
416
- "endDate": "timestamp",
417
- "bookingNotes": "string",
418
- "depositAmount": 100.0,
419
- "createdDate": "timestamp",
420
- "isRated": false,
421
- "durationDays": 3,
422
- "statusDescription": "string",
423
- "timeAgo": "string",
424
- "canBeCancelled": true,
425
- "canBeConfirmed": false
426
- }
427
-
428
-
429
- GET /api/bookings/my-bookings
430
- -----------------------------
431
- Get bookings where current user is borrower
432
-
433
- Headers:
434
- - Authorization: Bearer <token>
435
- - X-User-Id: <user_id>
436
-
437
- Response (200): Array of booking objects
438
-
439
-
440
- GET /api/bookings/my-owned-bookings
441
- -----------------------------------
442
- Get bookings for items owned by current user
443
-
444
- Headers:
445
- - Authorization: Bearer <token>
446
- - X-User-Id: <user_id>
447
-
448
- Response (200): Array of booking objects
449
-
450
-
451
- GET /api/bookings/pending-approvals
452
- ------------------------------------
453
- Get bookings awaiting owner approval
454
-
455
- Headers:
456
- - Authorization: Bearer <token>
457
- - X-User-Id: <owner_id>
458
-
459
- Response (200): Array of pending booking objects
460
-
461
-
462
- GET /api/bookings/{bookingId}
463
- ------------------------------
464
- Get single booking details
465
-
466
- Headers:
467
- - Authorization: Bearer <token>
468
- - X-User-Id: <user_id>
469
-
470
- Response (200): Returns booking object
471
-
472
-
473
- PATCH /api/bookings/{bookingId}/approve
474
- ----------------------------------------
475
- Approve a booking request (owner only)
476
-
477
- Headers:
478
- - Authorization: Bearer <token>
479
- - X-User-Id: <owner_id>
480
-
481
- Response (200): Returns updated booking with status CONFIRMED
482
-
483
-
484
- PATCH /api/bookings/{bookingId}/reject
485
- ---------------------------------------
486
- Reject a booking request (owner only)
487
-
488
- Headers:
489
- - Authorization: Bearer <token>
490
- - X-User-Id: <owner_id>
491
-
492
- Request Body (optional):
493
- {
494
- "rejectionReason": "string (max 500 chars)"
495
- }
496
-
497
- Response (200): Returns updated booking with status REJECTED
498
-
499
-
500
- PATCH /api/bookings/{bookingId}/start
501
- --------------------------------------
502
- Start a confirmed booking (borrower picks up item)
503
-
504
- Headers:
505
- - Authorization: Bearer <token>
506
- - X-User-Id: <borrower_id>
507
-
508
- Response (200): Returns updated booking with status ACTIVE
509
-
510
-
511
- PATCH /api/bookings/{bookingId}/complete
512
- -----------------------------------------
513
- Complete an active booking (borrower returns item)
514
-
515
- Headers:
516
- - Authorization: Bearer <token>
517
- - X-User-Id: <borrower_id>
518
-
519
- Response (200): Returns updated booking with status COMPLETED
520
-
521
-
522
- PATCH /api/bookings/{bookingId}/cancel
523
- ---------------------------------------
524
- Cancel a booking (borrower only, before it starts)
525
-
526
- Headers:
527
- - Authorization: Bearer <token>
528
- - X-User-Id: <borrower_id>
529
-
530
- Request Body (optional):
531
- {
532
- "cancellationReason": "string (max 500 chars)"
533
- }
534
-
535
- Response (200): Returns updated booking with status CANCELLED
536
-
537
-
538
- BOOKING STATUS FLOW:
539
- PENDING → CONFIRMED → ACTIVE → COMPLETED
540
- ↓ ↓ ↓
541
- REJECTED CANCELLED CANCELLED
542
-
543
-
544
- ------------------------------------------------------------------------------
545
- 3.6 RATING ENDPOINTS
546
- ------------------------------------------------------------------------------
547
-
548
- POST /api/ratings
549
- -----------------
550
- Create a new rating (requires completed booking for user ratings)
551
-
552
- Headers:
553
- - Authorization: Bearer <token>
554
-
555
- Request Body:
556
- {
557
- "rateeId": "string (user being rated, required)",
558
- "ratingType": "USER_TO_USER|USER_TO_ITEM|BORROWER_TO_OWNER|OWNER_TO_BORROWER",
559
- "ratingValue": 5, // 1-5 (required)
560
- "comment": "string (max 1000 chars, optional)",
561
- "bookingId": "string (required for user ratings)",
562
- "itemId": "string (required for USER_TO_ITEM)",
563
- "isAnonymous": false
564
- }
565
-
566
- Response (201):
567
- {
568
- "success": true,
569
- "message": "Rating created successfully",
570
- "data": {
571
- "id": "string",
572
- "rater_id": "string",
573
- "rater_name": "string",
574
- "ratee_id": "string",
575
- "ratee_name": "string",
576
- "rating_type": "BORROWER_TO_OWNER",
577
- "rating_value": 5,
578
- "comment": "string",
579
- "booking_id": "string",
580
- "created_at": "timestamp",
581
- "is_anonymous": false
582
- }
583
- }
584
-
585
- RATING VALIDATION (Issue #26):
586
- - Booking must exist and be COMPLETED
587
- - Only borrower can rate the booking
588
- - Each booking can only be rated once
589
- - Trust scores automatically updated after rating
590
-
591
-
592
- GET /api/ratings/can-rate/{bookingId}
593
- --------------------------------------
594
- Check if current user can rate a booking (Issue #26 feature)
595
-
596
- Headers:
597
- - Authorization: Bearer <token>
598
-
599
- Response (200):
600
- {
601
- "success": true,
602
- "can_rate": true,
603
- "booking_id": "string"
604
- }
605
-
606
- Returns false if:
607
- - Booking doesn't exist
608
- - Booking status is not COMPLETED
609
- - User is not the borrower
610
- - Booking has already been rated
611
-
612
-
613
- GET /api/ratings/user/{userId}/received
614
- ----------------------------------------
615
- Get ratings received by a user
616
-
617
- Headers:
618
- - Authorization: Bearer <token>
619
-
620
- Query Parameters:
621
- - page: integer (default: 0)
622
- - size: integer (default: 10)
623
- - ratingType: string (optional filter)
624
-
625
- Response (200):
626
- {
627
- "data": [
628
- {
629
- "id": "string",
630
- "rater_name": "string",
631
- "rating_value": 5,
632
- "comment": "string",
633
- "rating_type": "BORROWER_TO_OWNER",
634
- "created_at": "timestamp"
635
- }
636
- ],
637
- "pageable": {...},
638
- "totalElements": 10
639
- }
640
-
641
-
642
- GET /api/ratings/user/{userId}/given
643
- -------------------------------------
644
- Get ratings given by a user
645
-
646
- Headers:
647
- - Authorization: Bearer <token>
648
-
649
- Response (200): Similar structure to received ratings
650
-
651
-
652
- GET /api/ratings/item/{itemId}
653
- -------------------------------
654
- Get all ratings for an item
655
-
656
- Headers:
657
- - Authorization: Bearer <token>
658
-
659
- Response (200): Array of rating objects
660
-
661
-
662
- GET /api/ratings/user/{userId}/statistics
663
- ------------------------------------------
664
- Get rating statistics for a user
665
-
666
- Headers:
667
- - Authorization: Bearer <token>
668
-
669
- Response (200):
670
- {
671
- "data": {
672
- "user_id": "string",
673
- "total_ratings_received": 5,
674
- "average_rating": 4.6,
675
- "trust_score": 4.8,
676
- "ratings_by_type": {
677
- "BORROWER_TO_OWNER": {
678
- "count": 3,
679
- "average": 4.7
680
- },
681
- "OWNER_TO_BORROWER": {
682
- "count": 2,
683
- "average": 4.5
684
- }
685
- },
686
- "rating_distribution": {
687
- "5": 3,
688
- "4": 1,
689
- "3": 1
690
- },
691
- "recent_ratings": [...]
692
- }
693
- }
694
-
695
-
696
- GET /api/ratings/{ratingId}
697
- ----------------------------
698
- Get single rating details
699
-
700
- Headers:
701
- - Authorization: Bearer <token>
702
-
703
- Response (200): Returns rating object
704
-
705
-
706
- ================================================================================
707
- 4. DATA MODELS & DTOs
708
- ================================================================================
709
-
710
- 4.1 USER MODEL
711
- --------------
712
- {
713
- "id": "string",
714
- "username": "string (unique)",
715
- "name": "string",
716
- "email": "string (unique)",
717
- "phoneNumber": "string",
718
- "profileImageUrl": "string",
719
- "role": "USER|ADMIN",
720
- "isActive": boolean,
721
- "trustScore": number (0-5),
722
- "location": {
723
- "type": "Point",
724
- "coordinates": [longitude, latitude]
725
- },
726
- "address": {
727
- "street": "string",
728
- "city": "string",
729
- "state": "string",
730
- "zipCode": "string",
731
- "country": "string"
732
- },
733
- "createdDate": "timestamp",
734
- "lastLogin": "timestamp",
735
- "totalRatingsReceived": number,
736
- "ratingCountAsBorrower": number,
737
- "ratingCountAsLender": number,
738
- "averageRatingAsBorrower": number,
739
- "averageRatingAsLender": number
740
- }
741
-
742
-
743
- 4.2 ITEM MODEL
744
- --------------
745
- {
746
- "id": "string",
747
- "name": "string",
748
- "description": "string",
749
- "condition": "NEW|EXCELLENT|GOOD|FAIR|POOR",
750
- "status": "AVAILABLE|UNAVAILABLE|BORROWED",
751
- "deposit": number,
752
- "images": ["url1", "url2"],
753
- "averageRating": number,
754
- "ownerId": "string",
755
- "ownerName": "string",
756
- "categoryId": "string",
757
- "categoryName": "string",
758
- "canBeBorrowed": boolean,
759
- "createdAt": "timestamp",
760
- "updatedAt": "timestamp"
761
- }
762
-
763
-
764
- 4.3 BOOKING MODEL
765
- -----------------
766
- {
767
- "id": "string",
768
- "itemId": "string",
769
- "itemName": "string",
770
- "itemImageUrl": "string",
771
- "borrowerId": "string",
772
- "borrowerName": "string",
773
- "ownerId": "string",
774
- "ownerName": "string",
775
- "status": "PENDING|CONFIRMED|ACTIVE|COMPLETED|REJECTED|CANCELLED",
776
- "startDate": "timestamp",
777
- "endDate": "timestamp",
778
- "actualStartDate": "timestamp",
779
- "actualEndDate": "timestamp",
780
- "bookingNotes": "string",
781
- "ownerNotes": "string",
782
- "depositAmount": number,
783
- "depositPaid": boolean,
784
- "createdDate": "timestamp",
785
- "updatedDate": "timestamp",
786
- "confirmedDate": "timestamp",
787
- "cancelledDate": "timestamp",
788
- "cancellationReason": "string",
789
- "isRated": boolean,
790
- "durationDays": number,
791
- "statusDescription": "string",
792
- "timeAgo": "string",
793
- "daysUntilStart": number,
794
- "daysUntilEnd": number,
795
- "canBeCancelled": boolean,
796
- "canBeConfirmed": boolean,
797
- "canBeActivated": boolean,
798
- "canBeCompleted": boolean,
799
- "isOverdue": boolean,
800
- "requiresDeposit": boolean
801
- }
802
-
803
-
804
- 4.4 RATING MODEL
805
- ----------------
806
- {
807
- "id": "string",
808
- "raterId": "string",
809
- "raterName": "string",
810
- "rateeId": "string",
811
- "rateeName": "string",
812
- "ratingType": "USER_TO_USER|USER_TO_ITEM|BORROWER_TO_OWNER|OWNER_TO_BORROWER",
813
- "ratingValue": number (1-5),
814
- "comment": "string",
815
- "bookingId": "string",
816
- "itemId": "string",
817
- "isAnonymous": boolean,
818
- "createdAt": "timestamp"
819
- }
820
-
821
-
822
- 4.5 CATEGORY MODEL
823
- ------------------
824
- {
825
- "id": "string",
826
- "name": "string",
827
- "description": "string",
828
- "parentCategoryId": "string",
829
- "parentCategoryName": "string",
830
- "isActive": boolean,
831
- "itemCount": number,
832
- "hasSubcategories": boolean,
833
- "createdAt": "timestamp",
834
- "updatedAt": "timestamp"
835
- }
836
-
837
-
838
- ================================================================================
839
- 5. FRONTEND PAGES/VIEWS REQUIRED
840
- ================================================================================
841
-
842
- 5.1 AUTHENTICATION PAGES
843
- -------------------------
844
- - /register - User Registration Page
845
- • Form: username, name, email, password, phone number
846
- • Validation: Real-time field validation
847
- • Auto-login after successful registration
848
-
849
- - /login - User Login Page
850
- • Form: username/email, password
851
- • Store JWT token in localStorage/sessionStorage
852
- • Redirect to dashboard after login
853
-
854
-
855
- 5.2 PUBLIC PAGES
856
- ----------------
857
- - / (Home) - Landing Page
858
- • Hero section with platform overview
859
- • Featured/recent items
860
- • Category browse
861
- • Search bar
862
-
863
- - /items - Browse All Items
864
- • Grid/list view of all items
865
- • Filters: category, condition, availability
866
- • Pagination
867
- • Search functionality
868
-
869
- - /items/{itemId} - Item Detail Page
870
- • Full item details with image gallery
871
- • Owner information
872
- • Average rating and reviews
873
- • "Request to Borrow" button (if authenticated)
874
- • Calendar showing availability
875
-
876
- - /categories - Browse Categories
877
- • Category grid with item counts
878
- • Click to filter items by category
879
-
880
- - /users/{userId} - Public User Profile
881
- • User name, trust score, member since
882
- • Items shared by user
883
- • Public ratings/reviews
884
-
885
-
886
- 5.3 AUTHENTICATED USER PAGES
887
- -----------------------------
888
- - /dashboard - User Dashboard
889
- • Overview of user's items
890
- • Active bookings (as borrower and owner)
891
- • Pending approvals
892
- • Recent activity
893
- • Trust score display
894
-
895
- - /my-items - Manage My Items
896
- • List of all user's items
897
- • Add new item button
898
- • Edit/delete item actions
899
- • Item status toggle (available/unavailable)
900
-
901
- - /my-items/new - Add New Item
902
- • Form: name, description, category, condition, deposit, images
903
- • Image upload functionality
904
- • Category selector
905
-
906
- - /my-items/{itemId}/edit - Edit Item
907
- • Pre-filled form with existing item data
908
- • Update item details
909
-
910
- - /my-bookings - My Bookings (as Borrower)
911
- • List of all bookings where user is borrower
912
- • Filter by status (pending, active, completed)
913
- • Actions: cancel, start, complete
914
- • Rate after completion
915
-
916
- - /bookings-received - Bookings Received (as Owner)
917
- • List of bookings for user's items
918
- • Pending approvals highlighted
919
- • Actions: approve, reject
920
-
921
- - /bookings/{bookingId} - Booking Detail Page
922
- • Full booking information
923
- • Timeline/status tracker
924
- • Chat/messaging (future feature)
925
- • Actions based on role and status
926
-
927
- - /profile - User Profile Settings
928
- • Edit profile information
929
- • Change password
930
- • Upload profile picture
931
- • Location/address settings
932
-
933
- - /profile/ratings - My Ratings
934
- • Tabs: Received, Given
935
- • Rating statistics
936
- • Filter by rating type
937
-
938
-
939
- 5.4 BOOKING FLOW PAGES
940
- -----------------------
941
- - /items/{itemId}/book - Request Booking
942
- • Date picker for start/end dates
943
- • Duration calculator
944
- • Deposit amount display
945
- • Booking notes textarea
946
- • Terms and conditions checkbox
947
- • "Submit Booking Request" button
948
-
949
- - /bookings/{bookingId}/rate - Rate User/Item
950
- • Star rating selector (1-5)
951
- • Comment textarea
952
- • Anonymous option checkbox
953
- • Pre-check if booking can be rated (use can-rate endpoint)
954
-
955
-
956
- 5.5 ADDITIONAL UI COMPONENTS
957
- -----------------------------
958
- - Navigation Bar
959
- • Logo/Home link
960
- • Search bar
961
- • Categories dropdown
962
- • User menu (if authenticated)
963
- - Dashboard
964
- - My Items
965
- - My Bookings
966
- - Profile
967
- - Logout
968
- • Login/Register buttons (if not authenticated)
969
-
970
- - Footer
971
- • About, Terms, Privacy links
972
- • Social media links
973
- • Contact information
974
-
975
- - Search Component
976
- • Auto-complete suggestions
977
- • Filter options
978
- • Recent searches
979
-
980
- - Notification System
981
- • Toast/snackbar for success/error messages
982
- • Notification bell for:
983
- - New booking requests
984
- - Booking approvals/rejections
985
- - Upcoming booking dates
986
- - Rating reminders
987
-
988
- - Trust Score Badge
989
- • Visual representation (stars/number)
990
- • Tooltip with breakdown
991
-
992
- - Status Badges
993
- • Color-coded status indicators
994
- • PENDING: Yellow
995
- • CONFIRMED: Blue
996
- • ACTIVE: Green
997
- • COMPLETED: Gray
998
- • REJECTED/CANCELLED: Red
999
-
1000
-
1001
- ================================================================================
1002
- 6. COMMON WORKFLOWS
1003
- ================================================================================
1004
-
1005
- 6.1 USER REGISTRATION & LOGIN
1006
- ------------------------------
1007
- 1. User fills registration form
1008
- 2. POST /api/auth/register with user details
1009
- 3. Store returned user data and auto-login
1010
- 4. Redirect to dashboard
1011
-
1012
- For login:
1013
- 1. User enters credentials
1014
- 2. POST /api/auth/login
1015
- 3. Store JWT token in localStorage
1016
- 4. Store user data in app state
1017
- 5. Redirect to dashboard
1018
-
1019
-
1020
- 6.2 BROWSING & SEARCHING ITEMS
1021
- -------------------------------
1022
- 1. GET /api/items with pagination/filters
1023
- 2. Display items in grid/list view
1024
- 3. For search: GET /api/items/search?query={term}
1025
- 4. Click item → Navigate to /items/{itemId}
1026
- 5. GET /api/items/{itemId} for full details
1027
-
1028
-
1029
- 6.3 CREATING AN ITEM LISTING
1030
- -----------------------------
1031
- 1. Navigate to /my-items/new
1032
- 2. Fill item details form
1033
- 3. Upload images (store URLs)
1034
- 4. POST /api/items with Authorization and X-User-Id headers
1035
- 5. On success, redirect to /my-items
1036
- 6. Show success notification
1037
-
1038
-
1039
- 6.4 BOOKING REQUEST FLOW (Complete Lifecycle)
1040
- ----------------------------------------------
1041
- BORROWER SIDE:
1042
- 1. Browse items, click "Request to Borrow"
1043
- 2. Fill booking form (dates, notes)
1044
- 3. POST /api/bookings → Status: PENDING
1045
- 4. Wait for owner approval
1046
- 5. When approved (CONFIRMED), receive notification
1047
- 6. When ready to pick up, click "Start Booking"
1048
- 7. PATCH /api/bookings/{id}/start → Status: ACTIVE
1049
- 8. Use item during booking period
1050
- 9. When returning, click "Complete Booking"
1051
- 10. PATCH /api/bookings/{id}/complete → Status: COMPLETED
1052
- 11. System prompts to rate owner
1053
- 12. Check if can rate: GET /api/ratings/can-rate/{bookingId}
1054
- 13. If can_rate: true, show rating form
1055
- 14. POST /api/ratings with booking details
1056
- 15. Booking marked as rated (isRated: true)
1057
-
1058
- OWNER SIDE:
1059
- 1. Receive notification of new booking request
1060
- 2. View pending bookings in dashboard
1061
- 3. GET /api/bookings/pending-approvals
1062
- 4. Click booking to view details
1063
- 5. Review borrower profile, trust score
1064
- 6. Decide to approve or reject
1065
- 7. PATCH /api/bookings/{id}/approve → Status: CONFIRMED
1066
- OR
1067
- PATCH /api/bookings/{id}/reject → Status: REJECTED
1068
- 8. Track booking status through lifecycle
1069
- 9. After completion, optionally rate borrower
1070
-
1071
-
1072
- 6.5 RATING AFTER BOOKING
1073
- -------------------------
1074
- 1. Booking completed
1075
- 2. Call GET /api/ratings/can-rate/{bookingId}
1076
- 3. If response.can_rate === true:
1077
- - Show rating button/form
1078
- - User selects rating (1-5 stars)
1079
- - User writes optional comment
1080
- - POST /api/ratings with:
1081
- • rateeId: owner's user ID
1082
- • ratingType: BORROWER_TO_OWNER
1083
- • ratingValue: 1-5
1084
- • comment: optional text
1085
- • bookingId: booking ID
1086
- 4. On success:
1087
- - Show success message
1088
- - Update booking UI (show "Rated" badge)
1089
- - Refresh user's trust score
1090
- 5. If response.can_rate === false:
1091
- - Hide rating button
1092
- - Show "Already Rated" badge
1093
-
1094
-
1095
- 6.6 UPDATING ITEM STATUS
1096
- -------------------------
1097
- 1. Navigate to /my-items
1098
- 2. Toggle item availability switch
1099
- 3. PATCH /api/items/{itemId}/status
1100
- 4. Headers: Authorization + X-User-Id
1101
- 5. Body: { "status": "AVAILABLE" | "UNAVAILABLE" }
1102
- 6. Update UI to reflect new status
1103
-
1104
-
1105
- ================================================================================
1106
- 7. ERROR HANDLING
1107
- ================================================================================
1108
-
1109
- 7.1 HTTP STATUS CODES
1110
- ----------------------
1111
- - 200 OK: Successful GET/PUT/PATCH
1112
- - 201 Created: Successful POST
1113
- - 204 No Content: Successful DELETE
1114
- - 400 Bad Request: Validation errors, invalid data
1115
- - 401 Unauthorized: Missing or invalid JWT token
1116
- - 403 Forbidden: Insufficient permissions
1117
- - 404 Not Found: Resource doesn't exist
1118
- - 409 Conflict: Duplicate resource (username/email exists)
1119
- - 500 Internal Server Error: Server-side error
1120
-
1121
-
1122
- 7.2 ERROR RESPONSE FORMAT
1123
- --------------------------
1124
- {
1125
- "status": 400,
1126
- "message": "Validation failed",
1127
- "timestamp": "2025-11-08T10:00:00",
1128
- "details": "Username already exists",
1129
- "error_code": "VALIDATION_ERROR"
1130
- }
1131
-
1132
-
1133
- 7.3 COMMON ERROR SCENARIOS & HANDLING
1134
- --------------------------------------
1135
- 401 Unauthorized:
1136
- - JWT token expired or invalid
1137
- - Action: Redirect to login, clear stored token
1138
- - Show message: "Session expired, please login again"
1139
-
1140
- 403 Forbidden:
1141
- - User trying to modify someone else's resource
1142
- - Action: Show error message, don't allow action
1143
- - Example: Trying to delete another user's item
1144
-
1145
- 400 Bad Request (Booking):
1146
- - "Cannot transition from CONFIRMED to COMPLETED"
1147
- - Action: Show current status, available actions
1148
- - Guide user through correct status flow
1149
-
1150
- 400 Bad Request (Rating):
1151
- - "Required request header 'X-User-Id' is not present"
1152
- - Action: Ensure X-User-Id header is included in request
1153
-
1154
- 409 Conflict:
1155
- - "Username already exists"
1156
- - Action: Highlight field, suggest alternatives
1157
- - Real-time validation during registration
1158
-
1159
- Issue #26 Specific Errors:
1160
- - "Booking must be COMPLETED to rate"
1161
- → Show booking status, explain rating requirements
1162
- - "Only borrower can rate this booking"
1163
- → Hide rating button if user is not borrower
1164
- - "Booking has already been rated"
1165
- → Show "Already Rated" badge, hide rating form
1166
-
1167
-
1168
- 7.4 FRONTEND ERROR HANDLING BEST PRACTICES
1169
- -------------------------------------------
1170
- 1. Always wrap API calls in try-catch blocks
1171
- 2. Check response status before parsing JSON
1172
- 3. Display user-friendly error messages
1173
- 4. Log errors for debugging (console.error)
1174
- 5. Provide actionable guidance (e.g., "Check your internet connection")
1175
- 6. Use loading states during API calls
1176
- 7. Implement retry logic for transient failures
1177
- 8. Validate forms client-side before API calls
1178
-
1179
-
1180
- ================================================================================
1181
- 8. BEST PRACTICES
1182
- ================================================================================
1183
-
1184
- 8.1 AUTHENTICATION & SECURITY
1185
- ------------------------------
1186
- ✓ Store JWT token securely (httpOnly cookies preferred, or localStorage)
1187
- ✓ Include Authorization header in ALL authenticated requests
1188
- ✓ Clear token on logout
1189
- ✓ Redirect to login on 401 Unauthorized
1190
- ✓ Don't store sensitive data (passwords) in frontend
1191
- ✓ Implement token refresh mechanism if token has expiration
1192
- ✓ Validate user input before sending to backend
1193
-
1194
-
1195
- 8.2 API CALLS
1196
- -------------
1197
- ✓ Use environment variables for API base URL
1198
- ✓ Create reusable API service/utility functions
1199
- ✓ Implement request interceptors for adding auth headers
1200
- ✓ Implement response interceptors for error handling
1201
- ✓ Show loading indicators during API calls
1202
- ✓ Debounce search inputs (300-500ms delay)
1203
- ✓ Cache frequently accessed data (categories, user profile)
1204
- ✓ Use pagination for large data sets
1205
-
1206
-
1207
- 8.3 STATE MANAGEMENT
1208
- --------------------
1209
- ✓ Store authenticated user data in global state
1210
- ✓ Update local state optimistically for better UX
1211
- ✓ Sync state after API responses
1212
- ✓ Invalidate cached data after mutations
1213
- ✓ Use React Query, SWR, or similar for data fetching
1214
-
1215
-
1216
- 8.4 UI/UX CONSIDERATIONS
1217
- ------------------------
1218
- ✓ Show clear status indicators for bookings
1219
- ✓ Use color coding for different statuses
1220
- ✓ Provide real-time feedback (success/error messages)
1221
- ✓ Implement skeleton loaders for better perceived performance
1222
- ✓ Make trust scores prominent to build confidence
1223
- ✓ Show confirmation dialogs for destructive actions
1224
- ✓ Use form validation with clear error messages
1225
- ✓ Implement responsive design (mobile-first)
1226
- ✓ Add empty states ("No items yet" with CTA)
1227
- ✓ Show item availability calendars
1228
-
1229
-
1230
- 8.5 RATING SYSTEM (Issue #26)
1231
- ------------------------------
1232
- ✓ Always call can-rate endpoint before showing rating form
1233
- ✓ Disable rating button if can_rate is false
1234
- ✓ Show clear messages:
1235
- - "Complete the booking to rate"
1236
- - "You've already rated this booking"
1237
- - "Only borrowers can rate"
1238
- ✓ Display "Rated" badge on completed rated bookings
1239
- ✓ Prevent duplicate rating attempts on frontend
1240
- ✓ Show trust score updates immediately after rating
1241
- ✓ Allow users to see their rating history
1242
-
1243
-
1244
- 8.6 IMAGE HANDLING
1245
- ------------------
1246
- ✓ Implement image upload functionality
1247
- ✓ Compress images before upload
1248
- ✓ Support multiple image formats (JPG, PNG, WebP)
1249
- ✓ Show image previews before upload
1250
- ✓ Implement image gallery/carousel for item details
1251
- ✓ Use placeholder images for items without photos
1252
- ✓ Lazy load images for better performance
1253
-
1254
-
1255
- 8.7 SEARCH & FILTERING
1256
- -----------------------
1257
- ✓ Implement auto-complete for search
1258
- ✓ Show recent searches
1259
- ✓ Persist search query in URL for shareability
1260
- ✓ Allow multiple filter combinations
1261
- ✓ Show filter count ("Showing 12 of 45 items")
1262
- ✓ Clear filters button
1263
- ✓ Sort options (newest, popular, nearest)
1264
-
1265
-
1266
- 8.8 NOTIFICATIONS
1267
- -----------------
1268
- ✓ Show success messages after actions
1269
- ✓ Display error messages clearly
1270
- ✓ Implement in-app notification center
1271
- ✓ Badge count for unread notifications
1272
- ✓ Types of notifications:
1273
- - New booking request received
1274
- - Booking approved/rejected
1275
- - Booking starting soon
1276
- - Booking completed - reminder to rate
1277
- - New rating received
1278
- ✓ Push notifications (future enhancement)
1279
-
1280
-
1281
- 8.9 PERFORMANCE OPTIMIZATION
1282
- -----------------------------
1283
- ✓ Lazy load routes/pages
1284
- ✓ Implement infinite scroll for item lists
1285
- ✓ Cache API responses
1286
- ✓ Minimize bundle size
1287
- ✓ Use CDN for static assets
1288
- ✓ Implement service workers for offline support
1289
- ✓ Optimize images (WebP, responsive sizes)
1290
-
1291
-
1292
- 8.10 TESTING
1293
- ------------
1294
- ✓ Test authentication flow end-to-end
1295
- ✓ Test booking lifecycle thoroughly
1296
- ✓ Test rating system with various scenarios
1297
- ✓ Test error handling and edge cases
1298
- ✓ Test on different devices and browsers
1299
- ✓ Test with slow network (throttling)
1300
- ✓ Verify security (XSS, CSRF protection)
1301
-
1302
-
1303
- ================================================================================
1304
- APPENDIX A: EXAMPLE API CALL SNIPPETS
1305
- ================================================================================
1306
-
1307
- A.1 JAVASCRIPT FETCH EXAMPLES
1308
- ------------------------------
1309
-
1310
- // Login
1311
- async function login(usernameOrEmail, password) {
1312
- const response = await fetch('http://localhost:8080/api/auth/login', {
1313
- method: 'POST',
1314
- headers: { 'Content-Type': 'application/json' },
1315
- body: JSON.stringify({ usernameOrEmail, password })
1316
- });
1317
- const data = await response.json();
1318
- localStorage.setItem('token', data.token);
1319
- return data;
1320
- }
1321
-
1322
- // Get items with auth
1323
- async function getItems(page = 0, size = 10) {
1324
- const token = localStorage.getItem('token');
1325
- const response = await fetch(
1326
- `http://localhost:8080/api/items?page=${page}&size=${size}`,
1327
- {
1328
- headers: { 'Authorization': `Bearer ${token}` }
1329
- }
1330
- );
1331
- return response.json();
1332
- }
1333
-
1334
- // Create item
1335
- async function createItem(itemData, userId) {
1336
- const token = localStorage.getItem('token');
1337
- const response = await fetch('http://localhost:8080/api/items', {
1338
- method: 'POST',
1339
- headers: {
1340
- 'Content-Type': 'application/json',
1341
- 'Authorization': `Bearer ${token}`,
1342
- 'X-User-Id': userId
1343
- },
1344
- body: JSON.stringify(itemData)
1345
- });
1346
- return response.json();
1347
- }
1348
-
1349
- // Check if can rate booking (Issue #26)
1350
- async function canRateBooking(bookingId) {
1351
- const token = localStorage.getItem('token');
1352
- const response = await fetch(
1353
- `http://localhost:8080/api/ratings/can-rate/${bookingId}`,
1354
- {
1355
- headers: { 'Authorization': `Bearer ${token}` }
1356
- }
1357
- );
1358
- const data = await response.json();
1359
- return data.can_rate;
1360
- }
1361
-
1362
- // Create rating
1363
- async function createRating(ratingData) {
1364
- const token = localStorage.getItem('token');
1365
- const response = await fetch('http://localhost:8080/api/ratings', {
1366
- method: 'POST',
1367
- headers: {
1368
- 'Content-Type': 'application/json',
1369
- 'Authorization': `Bearer ${token}`
1370
- },
1371
- body: JSON.stringify(ratingData)
1372
- });
1373
- return response.json();
1374
- }
1375
-
1376
-
1377
- A.2 AXIOS EXAMPLES
1378
- ------------------
1379
-
1380
- import axios from 'axios';
1381
-
1382
- // Configure axios instance
1383
- const api = axios.create({
1384
- baseURL: 'http://localhost:8080',
1385
- headers: { 'Content-Type': 'application/json' }
1386
- });
1387
-
1388
- // Request interceptor to add auth token
1389
- api.interceptors.request.use(config => {
1390
- const token = localStorage.getItem('token');
1391
- if (token) {
1392
- config.headers.Authorization = `Bearer ${token}`;
1393
- }
1394
- return config;
1395
- });
1396
-
1397
- // Response interceptor for error handling
1398
- api.interceptors.response.use(
1399
- response => response,
1400
- error => {
1401
- if (error.response?.status === 401) {
1402
- localStorage.removeItem('token');
1403
- window.location.href = '/login';
1404
- }
1405
- return Promise.reject(error);
1406
- }
1407
- );
1408
-
1409
- // Usage examples
1410
- const login = (usernameOrEmail, password) =>
1411
- api.post('/api/auth/login', { usernameOrEmail, password });
1412
-
1413
- const getItems = (params) =>
1414
- api.get('/api/items', { params });
1415
-
1416
- const createItem = (itemData, userId) =>
1417
- api.post('/api/items', itemData, {
1418
- headers: { 'X-User-Id': userId }
1419
- });
1420
-
1421
- const canRateBooking = (bookingId) =>
1422
- api.get(`/api/ratings/can-rate/${bookingId}`);
1423
-
1424
- const createRating = (ratingData) =>
1425
- api.post('/api/ratings', ratingData);
1426
-
1427
- const approveBooking = (bookingId, ownerId) =>
1428
- api.patch(`/api/bookings/${bookingId}/approve`, null, {
1429
- headers: { 'X-User-Id': ownerId }
1430
- });
1431
-
1432
-
1433
- ================================================================================
1434
- APPENDIX B: SAMPLE FORM VALIDATION RULES
1435
- ================================================================================
1436
-
1437
- Registration Form:
1438
- - username: 3-50 chars, alphanumeric, unique
1439
- - name: 2-100 chars, required
1440
- - email: valid email format, unique
1441
- - password: min 8 chars, required
1442
- - phoneNumber: optional, valid phone format
1443
-
1444
- Item Form:
1445
- - name: required, max 200 chars
1446
- - description: required, max 2000 chars
1447
- - categoryId: required, must exist
1448
- - deposit: number, min 0, max 10000
1449
- - condition: one of [NEW, EXCELLENT, GOOD, FAIR, POOR]
1450
- - images: array of valid URLs
1451
-
1452
- Booking Form:
1453
- - itemId: required, must exist and be available
1454
- - startDate: required, must be future date
1455
- - endDate: required, must be after startDate
1456
- - bookingNotes: max 500 chars
1457
- - depositAmount: number, min 0
1458
- - acceptTerms: must be true
1459
-
1460
- Rating Form:
1461
- - rateeId: required, valid user ID
1462
- - ratingType: required, valid enum
1463
- - ratingValue: required, integer 1-5
1464
- - comment: max 1000 chars
1465
- - bookingId: required for user ratings
1466
- - itemId: required for item ratings
1467
-
1468
-
1469
- ================================================================================
1470
- APPENDIX C: WEBSOCKET/REAL-TIME FEATURES (Future Enhancement)
1471
- ================================================================================
1472
-
1473
- For future implementation, consider adding WebSocket support for:
1474
- - Real-time notifications
1475
- - Chat between borrowers and owners
1476
- - Live booking status updates
1477
- - Real-time availability updates
1478
-
1479
- Endpoint: ws://localhost:8080/ws
1480
- Authentication: Send JWT token in connection handshake
1481
-
1482
-
1483
- ================================================================================
1484
- APPENDIX D: MOBILE APP CONSIDERATIONS
1485
- ================================================================================
1486
-
1487
- This backend is fully compatible with mobile app development (React Native,
1488
- Flutter, etc.). Same API endpoints, authentication flow, and data models apply.
1489
-
1490
- Additional considerations for mobile:
1491
- - Implement token refresh for longer sessions
1492
- - Use secure storage for JWT tokens (KeyChain, KeyStore)
1493
- - Handle offline mode gracefully
1494
- - Optimize image uploads for mobile networks
1495
- - Implement push notifications
1496
- - Add location-based features (nearby items)
1497
- - Use biometric authentication
1498
-
1499
-
1500
- ================================================================================
1501
- END OF GUIDE
1502
- ================================================================================
1503
-
1504
- For questions or issues, refer to:
1505
- - Backend source code: /src/main/java/com/locallend/locallend/
1506
- - API documentation: Swagger UI at http://localhost:8080/swagger-ui.html (if enabled)
1507
- - Backend logs: docker logs locallend-backend
1508
-
1509
- Last Updated: November 8, 2025
1510
- Version: 1.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/FRONTEND_INTEGRATION_GUIDE_MARK2.md DELETED
@@ -1,945 +0,0 @@
1
- # 🚀 LocalLend Frontend Integration Guide - Mark 2
2
- ## Comprehensive Backend API Documentation & Integration Manual
3
-
4
- **Version**: 2.0
5
- **Date**: November 12, 2025
6
- **Backend Base URL**: `http://localhost:8080`
7
- **Frontend Base URL**: `http://localhost:5173`
8
-
9
- ---
10
-
11
- ## 📋 Table of Contents
12
-
13
- 1. [🔐 Authentication System](#authentication-system)
14
- 2. [🏗️ API Architecture](#api-architecture)
15
- 3. [📊 Data Models & Enums](#data-models--enums)
16
- 4. [🛠️ API Endpoints](#api-endpoints)
17
- 5. [🔄 Authentication Workflows](#authentication-workflows)
18
- 6. [⚠️ Error Handling](#error-handling)
19
- 7. [🧪 Testing Guide](#testing-guide)
20
- 8. [🐛 Common Issues & Solutions](#common-issues--solutions)
21
-
22
- ---
23
-
24
- ## 🔐 Authentication System
25
-
26
- ### **JWT Token Configuration**
27
- ```properties
28
- # Backend Configuration (application.properties)
29
- app.jwt.secret=change-this-secret-in-production-with-a-very-long-secure-random-string-at-least-256-bits
30
- app.jwt.expiration-ms=86400000 # 24 hours
31
- ```
32
-
33
- ### **Authentication Headers Required**
34
- ```http
35
- # For authenticated endpoints
36
- Authorization: Bearer {jwt_token}
37
- X-User-Id: {user_id}
38
- Content-Type: application/json
39
- ```
40
-
41
- ### **Public Endpoints (No Authentication Required)**
42
- - `POST /api/auth/register`
43
- - `POST /api/auth/login`
44
- - `GET /api/categories/**`
45
- - `GET /api/items/**` (browsing only)
46
- - `GET /api/users/*/public`
47
- - `GET /api/users/search`
48
-
49
- ### **Protected Endpoints (Authentication Required)**
50
- - All other endpoints require valid JWT token
51
- - X-User-Id header must match the token's user
52
-
53
- ---
54
-
55
- ## 🏗️ API Architecture
56
-
57
- ### **Base URL Structure**
58
- ```
59
- Backend API: http://localhost:8080/api
60
- ├── /auth # Authentication
61
- ├── /users # User management
62
- ├── /items # Item management
63
- ├── /categories # Category management
64
- ├── /bookings # Booking system
65
- └── /ratings # Rating system
66
- ```
67
-
68
- ### **Security Configuration**
69
- - **CORS**: Enabled for `http://localhost:*`
70
- - **Methods**: GET, POST, PUT, PATCH, DELETE
71
- - **Headers**: All headers allowed including Authorization and X-User-Id
72
- - **Session**: Stateless (JWT-based)
73
- - **CSRF**: Disabled for API
74
-
75
- ---
76
-
77
- ## 📊 Data Models & Enums
78
-
79
- ### **ItemStatus Enum**
80
- ```java
81
- public enum ItemStatus {
82
- AVAILABLE, // Item is available for booking
83
- BOOKED, // Item has been booked
84
- BORROWED, // Item is currently borrowed
85
- MAINTENANCE, // Item is under maintenance
86
- UNAVAILABLE // Owner has marked it unavailable
87
- }
88
- ```
89
-
90
- ### **ItemCondition Enum**
91
- ```java
92
- public enum ItemCondition {
93
- NEW,
94
- EXCELLENT,
95
- GOOD,
96
- FAIR,
97
- POOR
98
- }
99
- ```
100
-
101
- ### **BookingStatus Enum**
102
- ```java
103
- public enum BookingStatus {
104
- PENDING, // Waiting for owner approval
105
- CONFIRMED, // Owner has approved
106
- ACTIVE, // Booking is currently active
107
- COMPLETED, // Booking completed successfully
108
- CANCELLED, // Cancelled by borrower
109
- REJECTED // Rejected by owner
110
- }
111
- ```
112
-
113
- ### **RatingType Enum**
114
- ```java
115
- public enum RatingType {
116
- USER_TO_ITEM, // Rating an item
117
- USER_TO_USER, // General user rating
118
- OWNER_TO_BORROWER, // Owner rating borrower
119
- BORROWER_TO_OWNER // Borrower rating owner
120
- }
121
- ```
122
-
123
- ---
124
-
125
- ## 🛠️ API Endpoints
126
-
127
- ### **🔐 Authentication Endpoints**
128
-
129
- #### **Register User**
130
- ```http
131
- POST /api/auth/register
132
- Content-Type: application/json
133
-
134
- {
135
- "username": "johndoe", # Required, 3-50 chars
136
- "name": "John Doe", # Required, 2-100 chars
137
- "email": "john@example.com", # Required, valid email
138
- "password": "password123", # Required, min 8 chars
139
- "phoneNumber": "+1234567890" # Optional
140
- }
141
- ```
142
-
143
- **Response (201 Created):**
144
- ```json
145
- {
146
- "id": "user_id",
147
- "username": "johndoe",
148
- "name": "John Doe",
149
- "email": "john@example.com",
150
- "phoneNumber": "+1234567890",
151
- "profileImageUrl": null,
152
- "isActive": true,
153
- "createdAt": "2025-11-12T10:00:00Z"
154
- }
155
- ```
156
-
157
- #### **Login User**
158
- ```http
159
- POST /api/auth/login
160
- Content-Type: application/json
161
-
162
- {
163
- "usernameOrEmail": "johndoe", # Username or email
164
- "password": "password123"
165
- }
166
- ```
167
-
168
- **Response (200 OK):**
169
- ```json
170
- {
171
- "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
172
- "tokenType": "Bearer",
173
- "user": {
174
- "id": "user_id",
175
- "username": "johndoe",
176
- "name": "John Doe",
177
- "email": "john@example.com",
178
- "phoneNumber": "+1234567890",
179
- "profileImageUrl": null,
180
- "isActive": true,
181
- "createdAt": "2025-11-12T10:00:00Z"
182
- }
183
- }
184
- ```
185
-
186
- ---
187
-
188
- ### **👤 User Management Endpoints**
189
-
190
- #### **Get Current User Profile**
191
- ```http
192
- GET /api/users/profile
193
- Authorization: Bearer {jwt_token}
194
- ```
195
-
196
- #### **Update Current User Profile**
197
- ```http
198
- PUT /api/users/profile
199
- Authorization: Bearer {jwt_token}
200
- Content-Type: application/json
201
-
202
- {
203
- "name": "John Smith", # Optional
204
- "phoneNumber": "+1987654321", # Optional
205
- "profileImageUrl": "https://..." # Optional
206
- }
207
- ```
208
-
209
- #### **Get User Public Info**
210
- ```http
211
- GET /api/users/{userId}/public
212
- # No authentication required
213
- ```
214
-
215
- #### **Search Users**
216
- ```http
217
- GET /api/users/search?term=john&page=0&size=10
218
- # No authentication required
219
- ```
220
-
221
- ---
222
-
223
- ### **📦 Item Management Endpoints**
224
-
225
- #### **Create Item**
226
- ```http
227
- POST /api/items
228
- Authorization: Bearer {jwt_token}
229
- X-User-Id: {user_id}
230
- Content-Type: application/json
231
-
232
- {
233
- "name": "Laptop", # Required, 3-100 chars
234
- "description": "Gaming laptop", # Optional, max 500 chars
235
- "categoryId": "category_id", # Required
236
- "deposit": 100.0, # Optional, default 0.0
237
- "images": ["url1", "url2"], # Optional
238
- "condition": "EXCELLENT" # Optional, default GOOD
239
- }
240
- ```
241
-
242
- #### **Get All Available Items**
243
- ```http
244
- GET /api/items?page=0&size=10&sortBy=name&sortDir=asc
245
- # No authentication required
246
- ```
247
-
248
- #### **Get Item by ID**
249
- ```http
250
- GET /api/items/{itemId}
251
- # No authentication required
252
- ```
253
-
254
- #### **Search Items**
255
- ```http
256
- GET /api/items/search?q=laptop&page=0&size=10
257
- # No authentication required
258
- ```
259
-
260
- #### **Get Items by Category**
261
- ```http
262
- GET /api/items/category/{categoryId}?page=0&size=10
263
- # No authentication required
264
- ```
265
-
266
- #### **Get Items by Owner**
267
- ```http
268
- GET /api/items/owner/{ownerId}?page=0&size=10
269
- # No authentication required
270
- ```
271
-
272
- #### **Get Current User's Items**
273
- ```http
274
- GET /api/items/my-items?page=0&size=10
275
- Authorization: Bearer {jwt_token}
276
- X-User-Id: {user_id}
277
- ```
278
-
279
- #### **Update Item**
280
- ```http
281
- PUT /api/items/{itemId}
282
- Authorization: Bearer {jwt_token}
283
- X-User-Id: {user_id}
284
- Content-Type: application/json
285
-
286
- {
287
- "name": "Updated Laptop", # Optional
288
- "description": "New description", # Optional
289
- "condition": "GOOD", # Optional
290
- "deposit": 150.0, # Optional
291
- "images": ["new_url"], # Optional
292
- "isAvailable": true # Optional
293
- }
294
- ```
295
-
296
- #### **⚠️ MISSING ENDPOINT - Item Status Update**
297
- **❌ Frontend expects but backend doesn't have:**
298
- ```http
299
- PATCH /api/items/{itemId}/status
300
- Authorization: Bearer {jwt_token}
301
- X-User-Id: {user_id}
302
- Content-Type: application/json
303
-
304
- {
305
- "status": "UNAVAILABLE" # AVAILABLE, UNAVAILABLE, MAINTENANCE
306
- }
307
- ```
308
-
309
- **✅ Available alternatives:**
310
- ```http
311
- # Toggle availability (switches between AVAILABLE/UNAVAILABLE)
312
- PATCH /api/items/{itemId}/toggle-availability
313
- Authorization: Bearer {jwt_token}
314
- X-User-Id: {user_id}
315
-
316
- # Set specific availability
317
- PATCH /api/items/{itemId}/availability
318
- Authorization: Bearer {jwt_token}
319
- X-User-Id: {user_id}
320
- Content-Type: application/json
321
-
322
- {
323
- "isAvailable": false # true = AVAILABLE, false = UNAVAILABLE
324
- }
325
- ```
326
-
327
- #### **Delete Item**
328
- ```http
329
- DELETE /api/items/{itemId}
330
- Authorization: Bearer {jwt_token}
331
- X-User-Id: {user_id}
332
- ```
333
-
334
- ---
335
-
336
- ### **📂 Category Management Endpoints**
337
-
338
- #### **Create Category**
339
- ```http
340
- POST /api/categories
341
- Authorization: Bearer {jwt_token}
342
- Content-Type: application/json
343
-
344
- {
345
- "name": "Electronics", # Required
346
- "description": "Electronic items", # Optional
347
- "parentId": "parent_category_id" # Optional (for subcategories)
348
- }
349
- ```
350
-
351
- #### **Get All Categories**
352
- ```http
353
- GET /api/categories?sort=name
354
- # No authentication required
355
- ```
356
-
357
- #### **Get Category by ID**
358
- ```http
359
- GET /api/categories/{categoryId}
360
- # No authentication required
361
- ```
362
-
363
- #### **Get Root Categories**
364
- ```http
365
- GET /api/categories/root?sort=name
366
- # No authentication required
367
- ```
368
-
369
- ---
370
-
371
- ### **📅 Booking Management Endpoints**
372
-
373
- #### **Create Booking**
374
- ```http
375
- POST /api/bookings
376
- Authorization: Bearer {jwt_token}
377
- X-User-Id: {borrower_id}
378
- Content-Type: application/json
379
-
380
- {
381
- "itemId": "item_id",
382
- "startDate": "2025-11-15",
383
- "endDate": "2025-11-20",
384
- "notes": "I need this for a project" # Optional
385
- }
386
- ```
387
-
388
- #### **Get Booking by ID**
389
- ```http
390
- GET /api/bookings/{bookingId}
391
- Authorization: Bearer {jwt_token}
392
- ```
393
-
394
- #### **Get User's Bookings (as Borrower)**
395
- ```http
396
- GET /api/bookings/my-bookings
397
- Authorization: Bearer {jwt_token}
398
- X-User-Id: {user_id}
399
-
400
- # Alternative endpoint:
401
- GET /api/bookings/my
402
- Authorization: Bearer {jwt_token}
403
- X-User-Id: {user_id}
404
- ```
405
-
406
- #### **Get Pending Approvals (as Owner)**
407
- ```http
408
- GET /api/bookings/pending-approvals
409
- Authorization: Bearer {jwt_token}
410
- X-User-Id: {owner_id}
411
- ```
412
-
413
- #### **Get Owner's Bookings**
414
- ```http
415
- GET /api/bookings/my-owned
416
- Authorization: Bearer {jwt_token}
417
- X-User-Id: {owner_id}
418
- ```
419
-
420
- #### **Approve/Confirm Booking**
421
- ```http
422
- # New naming (Issue #15)
423
- PATCH /api/bookings/{bookingId}/approve
424
- Authorization: Bearer {jwt_token}
425
- X-User-Id: {owner_id}
426
- Content-Type: application/json
427
-
428
- {
429
- "ownerNotes": "Please take care of it" # Optional
430
- }
431
-
432
- # Original endpoint (still works)
433
- PATCH /api/bookings/{bookingId}/confirm
434
- Authorization: Bearer {jwt_token}
435
- X-User-Id: {owner_id}
436
- Content-Type: application/json
437
-
438
- {
439
- "ownerNotes": "Please take care of it" # Optional
440
- }
441
- ```
442
-
443
- #### **Start/Activate Booking**
444
- ```http
445
- # New naming (Issue #15)
446
- PATCH /api/bookings/{bookingId}/start
447
- Authorization: Bearer {jwt_token}
448
- X-User-Id: {borrower_id}
449
-
450
- # Original endpoint (still works)
451
- PATCH /api/bookings/{bookingId}/activate
452
- Authorization: Bearer {jwt_token}
453
- X-User-Id: {borrower_id}
454
- ```
455
-
456
- #### **Complete Booking**
457
- ```http
458
- PATCH /api/bookings/{bookingId}/complete
459
- Authorization: Bearer {jwt_token}
460
- X-User-Id: {borrower_id}
461
- ```
462
-
463
- #### **Cancel Booking**
464
- ```http
465
- PATCH /api/bookings/{bookingId}/cancel
466
- Authorization: Bearer {jwt_token}
467
- X-User-Id: {borrower_id}
468
- Content-Type: application/json
469
-
470
- {
471
- "reason": "Plans changed" # Optional
472
- }
473
- ```
474
-
475
- #### **Reject Booking**
476
- ```http
477
- PATCH /api/bookings/{bookingId}/reject
478
- Authorization: Bearer {jwt_token}
479
- X-User-Id: {owner_id}
480
- Content-Type: application/json
481
-
482
- {
483
- "reason": "Item not available" # Optional
484
- }
485
- ```
486
-
487
- ---
488
-
489
- ### **⭐ Rating System Endpoints**
490
-
491
- #### **Create Rating**
492
- ```http
493
- POST /api/ratings
494
- Authorization: Bearer {jwt_token}
495
- Content-Type: application/json
496
-
497
- # Rating an item
498
- {
499
- "ratingType": "USER_TO_ITEM",
500
- "itemId": "item_id",
501
- "ratingValue": 5, # 1-5
502
- "comment": "Great item!", # Optional
503
- "isAnonymous": false # Optional, default false
504
- }
505
-
506
- # Rating a user
507
- {
508
- "ratingType": "BORROWER_TO_OWNER", # or OWNER_TO_BORROWER, USER_TO_USER
509
- "rateeId": "user_id",
510
- "bookingId": "booking_id", # Optional
511
- "ratingValue": 4, # 1-5
512
- "comment": "Great communication!" # Optional
513
- }
514
- ```
515
-
516
- #### **Get User Ratings**
517
- ```http
518
- GET /api/ratings/user/{userId}
519
- # No authentication required
520
- ```
521
-
522
- #### **Get Item Ratings**
523
- ```http
524
- GET /api/ratings/item/{itemId}
525
- # No authentication required
526
- ```
527
-
528
- #### **Get Rating Statistics**
529
- ```http
530
- GET /api/ratings/user/{userId}/stats
531
- # No authentication required
532
-
533
- GET /api/ratings/item/{itemId}/stats
534
- # No authentication required
535
- ```
536
-
537
- ---
538
-
539
- ## 🔄 Authentication Workflows
540
-
541
- ### **Registration Flow**
542
- ```mermaid
543
- sequenceDiagram
544
- participant F as Frontend
545
- participant B as Backend
546
- participant DB as Database
547
-
548
- F->>B: POST /api/auth/register
549
- B->>B: Validate input
550
- B->>B: Hash password
551
- B->>DB: Save user
552
- B->>F: Return user data (201)
553
- Note over F: Redirect to login
554
- ```
555
-
556
- ### **Login Flow**
557
- ```mermaid
558
- sequenceDiagram
559
- participant F as Frontend
560
- participant B as Backend
561
- participant DB as Database
562
-
563
- F->>B: POST /api/auth/login
564
- B->>DB: Verify credentials
565
- B->>B: Generate JWT token
566
- B->>F: Return token + user data
567
- F->>F: Store token in localStorage
568
- F->>F: Set Authorization header
569
- ```
570
-
571
- ### **Authenticated Request Flow**
572
- ```mermaid
573
- sequenceDiagram
574
- participant F as Frontend
575
- participant B as Backend
576
- participant DB as Database
577
-
578
- F->>B: Request with Authorization header
579
- B->>B: Validate JWT token
580
- B->>B: Extract user from token
581
- B->>B: Check X-User-Id matches token
582
- B->>DB: Process request
583
- B->>F: Return response
584
- ```
585
-
586
- ### **Token Storage & Management**
587
- ```javascript
588
- // Frontend token management
589
- class AuthService {
590
- login(credentials) {
591
- return api.post('/api/auth/login', credentials)
592
- .then(response => {
593
- const { token, user } = response.data;
594
- localStorage.setItem('token', token);
595
- localStorage.setItem('user', JSON.stringify(user));
596
- this.setAuthHeader(token);
597
- return { token, user };
598
- });
599
- }
600
-
601
- setAuthHeader(token) {
602
- api.defaults.headers.common['Authorization'] = `Bearer ${token}`;
603
- }
604
-
605
- logout() {
606
- localStorage.removeItem('token');
607
- localStorage.removeItem('user');
608
- delete api.defaults.headers.common['Authorization'];
609
- }
610
-
611
- getCurrentUser() {
612
- return JSON.parse(localStorage.getItem('user'));
613
- }
614
-
615
- getToken() {
616
- return localStorage.getItem('token');
617
- }
618
-
619
- isAuthenticated() {
620
- const token = this.getToken();
621
- if (!token) return false;
622
-
623
- // Check if token is expired
624
- try {
625
- const payload = JSON.parse(atob(token.split('.')[1]));
626
- return payload.exp * 1000 > Date.now();
627
- } catch {
628
- return false;
629
- }
630
- }
631
- }
632
- ```
633
-
634
- ---
635
-
636
- ## ⚠️ Error Handling
637
-
638
- ### **Standard Error Response Format**
639
- ```json
640
- {
641
- "success": false,
642
- "message": "Error description",
643
- "error_code": "ERROR_TYPE",
644
- "timestamp": "2025-11-12T10:00:00Z"
645
- }
646
- ```
647
-
648
- ### **HTTP Status Codes**
649
- - **200** - Success
650
- - **201** - Created
651
- - **400** - Bad Request (validation errors)
652
- - **401** - Unauthorized (invalid/missing token)
653
- - **403** - Forbidden (no permission)
654
- - **404** - Not Found
655
- - **409** - Conflict (duplicate data)
656
- - **500** - Internal Server Error
657
-
658
- ### **Common Error Scenarios**
659
-
660
- #### **Authentication Errors**
661
- ```json
662
- // 401 - Invalid token
663
- {
664
- "success": false,
665
- "message": "Invalid or expired token",
666
- "error_code": "INVALID_TOKEN"
667
- }
668
-
669
- // 403 - Insufficient permissions
670
- {
671
- "success": false,
672
- "message": "You don't own this item",
673
- "error_code": "ACCESS_DENIED"
674
- }
675
- ```
676
-
677
- #### **Validation Errors**
678
- ```json
679
- // 400 - Missing required fields
680
- {
681
- "success": false,
682
- "message": "Name is required",
683
- "error_code": "VALIDATION_ERROR"
684
- }
685
- ```
686
-
687
- #### **Business Logic Errors**
688
- ```json
689
- // 409 - Booking conflict
690
- {
691
- "success": false,
692
- "message": "Item is already booked for this period",
693
- "error_code": "BOOKING_CONFLICT"
694
- }
695
- ```
696
-
697
- ---
698
-
699
- ## 🧪 Testing Guide
700
-
701
- ### **Basic API Health Check**
702
- ```bash
703
- # Test if backend is running
704
- curl -I http://localhost:8080/api/categories
705
- # Expected: HTTP/1.1 200 OK
706
-
707
- # Test authentication endpoint
708
- curl -X POST http://localhost:8080/api/auth/login \
709
- -H "Content-Type: application/json" \
710
- -d '{"usernameOrEmail":"test","password":"test"}'
711
- # Expected: 401 or valid response (not 404/500)
712
- ```
713
-
714
- ### **Authentication Testing**
715
- ```bash
716
- # 1. Register a test user
717
- curl -X POST http://localhost:8080/api/auth/register \
718
- -H "Content-Type: application/json" \
719
- -d '{
720
- "username": "testuser",
721
- "name": "Test User",
722
- "email": "test@example.com",
723
- "password": "password123"
724
- }'
725
-
726
- # 2. Login to get token
727
- curl -X POST http://localhost:8080/api/auth/login \
728
- -H "Content-Type: application/json" \
729
- -d '{
730
- "usernameOrEmail": "testuser",
731
- "password": "password123"
732
- }'
733
-
734
- # 3. Test authenticated endpoint
735
- curl -X GET http://localhost:8080/api/users/profile \
736
- -H "Authorization: Bearer YOUR_TOKEN_HERE"
737
- ```
738
-
739
- ### **Item Management Testing**
740
- ```bash
741
- # Test item availability endpoints (the missing one)
742
- curl -X PATCH http://localhost:8080/api/items/ITEM_ID/status \
743
- -H "Content-Type: application/json" \
744
- -H "Authorization: Bearer TOKEN" \
745
- -H "X-User-Id: USER_ID" \
746
- -d '{"status":"UNAVAILABLE"}'
747
- # Expected: 404 (endpoint doesn't exist)
748
-
749
- # Test working availability endpoint
750
- curl -X PATCH http://localhost:8080/api/items/ITEM_ID/availability \
751
- -H "Content-Type: application/json" \
752
- -H "Authorization: Bearer TOKEN" \
753
- -H "X-User-Id: USER_ID" \
754
- -d '{"isAvailable":false}'
755
- # Expected: 200 with updated item
756
- ```
757
-
758
- ---
759
-
760
- ## 🐛 Common Issues & Solutions
761
-
762
- ### **Issue 1: "Make Unavailable" Button Not Working**
763
-
764
- **Problem**: Frontend calls `PATCH /api/items/{id}/status` but backend doesn't have this endpoint.
765
-
766
- **Root Cause**: Mismatch between frontend expectations and backend implementation.
767
-
768
- **Solutions**:
769
-
770
- **Option A: Update Frontend (Recommended)**
771
- ```javascript
772
- // Change in itemService.ts
773
- updateItemStatus: async (itemId: string, status: ItemStatus, userId: string): Promise<Item> => {
774
- // Instead of:
775
- // const response = await api.patch(`/api/items/${itemId}/status`, { status }, {
776
-
777
- // Use:
778
- const isAvailable = status === 'AVAILABLE';
779
- const response = await api.patch(`/api/items/${itemId}/availability`, { isAvailable }, {
780
- headers: addUserIdHeader(userId)
781
- });
782
- return response;
783
- }
784
- ```
785
-
786
- **Option B: Add Missing Backend Endpoint**
787
- ```java
788
- // Add to ItemController.java
789
- @PatchMapping("/{id}/status")
790
- public ResponseEntity<?> updateItemStatus(
791
- @PathVariable String id,
792
- @RequestBody Map<String, String> request,
793
- @RequestHeader(value = "X-User-Id", required = true) String userId) {
794
-
795
- try {
796
- String status = request.get("status");
797
- if (status == null) {
798
- return ResponseEntity.badRequest()
799
- .body(Map.of("error", "Status is required"));
800
- }
801
-
802
- // Convert status to availability
803
- boolean isAvailable = "AVAILABLE".equals(status.toUpperCase());
804
- ItemDTO updatedItem = itemService.setAvailability(id, isAvailable, userId);
805
- return ResponseEntity.ok(updatedItem);
806
- } catch (Exception e) {
807
- return ResponseEntity.status(HttpStatus.BAD_REQUEST)
808
- .body(Map.of("error", e.getMessage()));
809
- }
810
- }
811
- ```
812
-
813
- ### **Issue 2: CORS Errors**
814
-
815
- **Problem**: Frontend can't reach backend due to CORS policy.
816
-
817
- **Solution**: Backend already has CORS configured correctly:
818
- ```java
819
- @CrossOrigin(origins = "*", maxAge = 3600) // On controllers
820
- ```
821
-
822
- ### **Issue 3: Authentication Failures**
823
-
824
- **Common Problems**:
825
- 1. **Missing X-User-Id header**
826
- 2. **Token format issues** (missing "Bearer " prefix)
827
- 3. **Expired tokens** (24-hour expiration)
828
-
829
- **Frontend Fix**:
830
- ```javascript
831
- // Ensure proper headers
832
- const addUserIdHeader = (userId) => ({
833
- 'X-User-Id': userId,
834
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
835
- });
836
- ```
837
-
838
- ### **Issue 4: Static Resource Errors**
839
-
840
- **Problem**: Getting "No static resource" instead of API responses.
841
-
842
- **Causes**:
843
- 1. Backend not running on port 8080
844
- 2. Wrong URL (missing `/api` prefix)
845
- 3. Spring Boot serving static files instead of API
846
-
847
- **Solutions**:
848
- 1. Verify backend is running: `curl http://localhost:8080/api/categories`
849
- 2. Check application.properties for conflicting static mappings
850
- 3. Ensure controllers are properly registered
851
-
852
- ---
853
-
854
- ## 📝 Frontend Implementation Checklist
855
-
856
- ### **Authentication Integration**
857
- - [ ] Implement login/register forms
858
- - [ ] Store JWT token in localStorage
859
- - [ ] Set Authorization header on all authenticated requests
860
- - [ ] Include X-User-Id header where required
861
- - [ ] Handle token expiration (401 responses)
862
- - [ ] Implement logout functionality
863
-
864
- ### **Item Management Integration**
865
- - [ ] ⚠️ **Fix item status update** (use `/availability` endpoint)
866
- - [ ] Implement item creation form
867
- - [ ] Add item browsing/search functionality
868
- - [ ] Handle image uploads properly
869
- - [ ] Implement owner-only actions (edit/delete)
870
-
871
- ### **Booking System Integration**
872
- - [ ] Create booking request form
873
- - [ ] Implement booking approval workflow
874
- - [ ] Add booking status tracking
875
- - [ ] Handle booking state transitions
876
- - [ ] Display pending approvals for owners
877
-
878
- ### **Rating System Integration**
879
- - [ ] Add rating creation forms
880
- - [ ] Display ratings on items/users
881
- - [ ] Implement rating statistics
882
- - [ ] Handle different rating types
883
-
884
- ### **Error Handling**
885
- - [ ] Implement global error interceptor
886
- - [ ] Handle authentication errors
887
- - [ ] Display user-friendly error messages
888
- - [ ] Add loading states
889
-
890
- ---
891
-
892
- ## 🎯 Quick Start Checklist
893
-
894
- 1. **Start Backend**: `mvn spring-boot:run` (port 8080)
895
- 2. **Start Frontend**: `npm run dev` (port 5173)
896
- 3. **Test Registration**: Create a test user
897
- 4. **Test Login**: Get JWT token
898
- 5. **Test Authenticated Endpoint**: Use token to access protected routes
899
- 6. **Fix Item Status Issue**: Update frontend to use `/availability` endpoint
900
-
901
- ---
902
-
903
- ## 📞 Support & Troubleshooting
904
-
905
- ### **Backend Logs**
906
- ```bash
907
- # Check Spring Boot logs for errors
908
- mvn spring-boot:run
909
-
910
- # Look for:
911
- ✅ "Started Application in X seconds"
912
- ✅ "Mapping servlet: 'dispatcherServlet' to [/]"
913
- ❌ Authentication/authorization errors
914
- ❌ Database connection issues
915
- ```
916
-
917
- ### **Frontend Debugging**
918
- ```javascript
919
- // Add to browser console for debugging
920
- console.log('Token:', localStorage.getItem('token'));
921
- console.log('User:', localStorage.getItem('user'));
922
-
923
- // Check API calls in Network tab
924
- // Verify Authorization headers are set
925
- // Check response status codes
926
- ```
927
-
928
- ### **Common Commands**
929
- ```bash
930
- # Backend health check
931
- curl http://localhost:8080/api/categories
932
-
933
- # Test authentication
934
- curl -X POST http://localhost:8080/api/auth/login \
935
- -H "Content-Type: application/json" \
936
- -d '{"usernameOrEmail":"test","password":"test"}'
937
-
938
- # Test protected endpoint
939
- curl -X GET http://localhost:8080/api/users/profile \
940
- -H "Authorization: Bearer YOUR_TOKEN"
941
- ```
942
-
943
- ---
944
-
945
- **This guide should provide everything needed for successful frontend-backend integration. The key issue identified is the missing `/status` endpoint - fix this first for the "Make Unavailable" button to work!**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/JSON_FIX_SUMMARY.md DELETED
@@ -1,79 +0,0 @@
1
- # FIXED: JSON Parse Error - Boolean vs String Issue
2
-
3
- ## 🎯 **Root Cause**
4
-
5
- **Backend Error:** `Cannot deserialize value of type java.lang.Boolean from String "UNAVAILABLE"`
6
-
7
- **Issue:** The `/api/items/{id}/availability` endpoint expects a **boolean** field, not a string status.
8
-
9
- ---
10
-
11
- ## 🔧 **Fix Applied**
12
-
13
- ### **BEFORE (Wrong JSON):**
14
- ```json
15
- {
16
- "status": "UNAVAILABLE" // ❌ String value
17
- }
18
- ```
19
-
20
- ### **AFTER (Correct JSON):**
21
- ```json
22
- {
23
- "available": false // ✅ Boolean value
24
- }
25
- ```
26
-
27
- ### **Mapping Logic:**
28
- - **"AVAILABLE" status** → `{"available": true}`
29
- - **"UNAVAILABLE" status** → `{"available": false}`
30
-
31
- ---
32
-
33
- ## 📁 **Files Updated:**
34
-
35
- ### **1. itemService.ts**
36
- ```typescript
37
- // OLD (wrong format)
38
- { status } // Sends: {"status": "UNAVAILABLE"}
39
-
40
- // NEW (correct format)
41
- const isAvailable = status === 'AVAILABLE';
42
- { available: isAvailable } // Sends: {"available": false}
43
- ```
44
-
45
- ### **2. DashboardPage.tsx**
46
- ```typescript
47
- // OLD (wrong format)
48
- body: JSON.stringify({ status: newStatus })
49
-
50
- // NEW (correct format)
51
- const isAvailable = newStatus === 'AVAILABLE';
52
- body: JSON.stringify({ available: isAvailable })
53
- ```
54
-
55
- ---
56
-
57
- ## 🎯 **What Happens Now:**
58
-
59
- ### **When clicking "Make Unavailable":**
60
- 1. Frontend calculates: `isAvailable = false` (since newStatus !== 'AVAILABLE')
61
- 2. Sends: `{"available": false}` to `/api/items/{id}/availability`
62
- 3. Backend receives boolean and processes correctly
63
- 4. Item becomes unavailable ✅
64
-
65
- ### **When clicking "Make Available":**
66
- 1. Frontend calculates: `isAvailable = true` (since newStatus === 'AVAILABLE')
67
- 2. Sends: `{"available": true}` to `/api/items/{id}/availability`
68
- 3. Backend receives boolean and processes correctly
69
- 4. Item becomes available ✅
70
-
71
- ---
72
-
73
- ## ✅ **Expected Result:**
74
-
75
- - **No more JSON parse errors** - Backend gets boolean values it expects
76
- - **Status toggle works correctly** - Available/Unavailable as intended
77
- - **Proper button behavior** - "Make Available" vs "Make Unavailable"
78
-
79
- The malformed JSON error should be gone and the status toggle should work properly!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/README-LOCALLEND.md DELETED
@@ -1,323 +0,0 @@
1
- # LocalLend Frontend
2
-
3
- A React + TypeScript + Vite application for the LocalLend peer-to-peer item sharing platform.
4
-
5
- ## 📋 Project Overview
6
-
7
- LocalLend is a community platform where users can:
8
- - List items they own for others to borrow
9
- - Browse and search items available in their community
10
- - Request to borrow items from other users
11
- - Manage bookings (approve, track, complete)
12
- - Rate users and items after completed transactions
13
- - Build trust scores through positive interactions
14
-
15
- ## 🛠 Technology Stack
16
-
17
- - **Frontend Framework:** React 19+ with TypeScript
18
- - **Build Tool:** Vite 7+
19
- - **UI Framework:** Material-UI (MUI) v6
20
- - **Routing:** React Router DOM v6
21
- - **HTTP Client:** Axios (currently using fetch as fallback)
22
- - **Form Handling:** React Hook Form with Yup validation
23
- - **Date Handling:** Day.js
24
- - **State Management:** React Context API
25
- - **Authentication:** JWT tokens with localStorage
26
-
27
- ## 📁 Project Structure
28
-
29
- ```
30
- src/
31
- ├── components/ # Reusable UI components
32
- ├── pages/ # Page-level components
33
- ├── services/ # API service layer
34
- ├── context/ # React context providers
35
- ├── hooks/ # Custom React hooks
36
- ├── utils/ # Helper functions and constants
37
- ├── types/ # TypeScript type definitions
38
- └── assets/ # Static assets
39
- ```
40
-
41
- ## 🚀 Getting Started
42
-
43
- ### Prerequisites
44
-
45
- - Node.js 18+ and npm/yarn
46
- - Backend API running on http://localhost:8080
47
-
48
- ### Installation
49
-
50
- 1. **Install Dependencies** (if npm install fails due to esbuild issues):
51
- ```bash
52
- # Remove problematic node_modules if they exist
53
- rm -rf node_modules package-lock.json
54
-
55
- # Install dependencies
56
- npm install
57
-
58
- # If still failing, try with force flag
59
- npm install --force
60
- ```
61
-
62
- 2. **Environment Setup**
63
- ```bash
64
- # Create .env file in root directory
65
- echo "VITE_API_BASE_URL=http://localhost:8080" > .env
66
- ```
67
-
68
- 3. **Start Development Server**
69
- ```bash
70
- npm run dev
71
- ```
72
-
73
- ## 📦 Dependencies Status
74
-
75
- The project dependencies are configured in package.json but may need to be installed:
76
-
77
- ### Core Dependencies (Added to package.json)
78
- - `react` & `react-dom` - React framework
79
- - `react-router-dom` - Client-side routing
80
- - `axios` - HTTP client for API calls
81
- - `@mui/material` - Material-UI component library
82
- - `@emotion/react` & `@emotion/styled` - CSS-in-JS for MUI
83
- - `@mui/icons-material` - Material Design icons
84
-
85
- ### Form & Validation
86
- - `react-hook-form` - Form state management
87
- - `@hookform/resolvers` - Form validation resolvers
88
- - `yup` - Schema validation
89
-
90
- ### Date Handling
91
- - `dayjs` - Date manipulation library
92
- - `@mui/x-date-pickers` - MUI date picker components
93
-
94
- ### Notifications
95
- - `react-hot-toast` - Toast notifications
96
-
97
- ## 🏗 API Integration
98
-
99
- The frontend integrates with the LocalLend backend API following these patterns:
100
-
101
- ### Authentication
102
- - JWT tokens stored in localStorage
103
- - Automatic token refresh on API calls
104
- - Redirect to login on 401 responses
105
-
106
- ### API Services (Implemented)
107
- - `authService` - User authentication
108
- - `itemService` - Item CRUD operations
109
- - `bookingService` - Booking lifecycle management
110
- - `categoryService` - Category management
111
- - `ratingService` - User and item ratings
112
- - `userService` - User profile operations
113
-
114
- ### Required Headers
115
- ```javascript
116
- // For authenticated requests
117
- Authorization: Bearer <jwt_token>
118
-
119
- // For owner/borrower specific operations
120
- X-User-Id: <user_id>
121
- ```
122
-
123
- ## 📱 Key Features (Designed)
124
-
125
- ### Authentication Flow
126
- 1. User registration/login
127
- 2. JWT token storage
128
- 3. Protected route navigation
129
- 4. Automatic session management
130
-
131
- ### Item Management
132
- 1. Browse items with pagination/filters
133
- 2. Create/edit item listings
134
- 3. Upload item images
135
- 4. Category-based organization
136
-
137
- ### Booking System
138
- 1. Request to borrow items
139
- 2. Owner approval workflow
140
- 3. Booking status tracking
141
- 4. Start/complete booking actions
142
-
143
- ### Rating System
144
- 1. Rate users after completed bookings
145
- 2. Rate items based on experience
146
- 3. Trust score calculation
147
- 4. Community feedback display
148
-
149
- ## 🎨 UI Components Structure (Planned)
150
-
151
- ### Pages (Major Views)
152
- - `HomePage` - Item browsing and search
153
- - `LoginPage` - User authentication ✅ (Structure created)
154
- - `RegisterPage` - Account creation ✅ (Structure created)
155
- - `DashboardPage` - User dashboard
156
- - `ItemDetailPage` - Individual item view
157
- - `BookingPage` - Booking management
158
- - `ProfilePage` - User profile settings
159
-
160
- ### Components (Reusable)
161
- - `Navbar` - Navigation with search
162
- - `ItemCard` - Item display component
163
- - `BookingCard` - Booking status display
164
- - `RatingForm` - Rating submission
165
- - `LoadingSpinner` - Loading states
166
- - `ErrorBoundary` - Error handling
167
-
168
- ## 🔧 Development Commands
169
-
170
- ```bash
171
- # Development server
172
- npm run dev
173
-
174
- # Production build
175
- npm run build
176
-
177
- # Preview production build
178
- npm run preview
179
-
180
- # Lint code
181
- npm run lint
182
- ```
183
-
184
- ## 🌐 API Endpoints Integration
185
-
186
- The frontend connects to these key backend endpoints:
187
-
188
- ### Authentication
189
- - `POST /api/auth/register` - User registration
190
- - `POST /api/auth/login` - User login
191
-
192
- ### Items
193
- - `GET /api/items` - Browse items (public)
194
- - `POST /api/items` - Create item (authenticated)
195
- - `GET /api/items/{id}` - Item details
196
- - `PUT /api/items/{id}` - Update item
197
- - `PATCH /api/items/{id}/status` - Update status
198
-
199
- ### Bookings
200
- - `POST /api/bookings` - Create booking request
201
- - `GET /api/bookings` - My bookings
202
- - `PATCH /api/bookings/{id}/approve` - Approve booking
203
- - `PATCH /api/bookings/{id}/start` - Start booking
204
- - `PATCH /api/bookings/{id}/complete` - Complete booking
205
-
206
- ### Ratings
207
- - `GET /api/ratings/can-rate/{bookingId}` - Check if can rate
208
- - `POST /api/ratings` - Submit rating
209
-
210
- ## 🚨 Current Status
211
-
212
- ✅ **Completed:**
213
- - Project scaffolding and structure
214
- - TypeScript type definitions (complete API coverage)
215
- - API service layer with fetch-based client
216
- - Authentication context setup
217
- - Basic component structure (placeholder)
218
- - Form validation utilities
219
- - Constants and helper functions
220
- - Project documentation
221
-
222
- ⏳ **Pending (requires dependency installation):**
223
- - Material-UI component integration
224
- - React Router implementation
225
- - Form handling with React Hook Form
226
- - Complete page implementations
227
- - Image upload functionality
228
- - Real-time notifications
229
- - Error boundary implementation
230
-
231
- ## 🛠 Implementation Roadmap
232
-
233
- ### Phase 1: Dependencies & Core Setup
234
- 1. Resolve npm installation issues
235
- 2. Install all required dependencies
236
- 3. Set up Material-UI theme provider
237
- 4. Implement React Router with protected routes
238
-
239
- ### Phase 2: Authentication & Navigation
240
- 1. Complete Login/Register pages with MUI components
241
- 2. Implement navigation bar with search
242
- 3. Set up authentication guards
243
- 4. Add toast notification system
244
-
245
- ### Phase 3: Core Features
246
- 1. Home page with item listings
247
- 2. Item detail and creation pages
248
- 3. User dashboard and profile pages
249
- 4. Booking request and management system
250
-
251
- ### Phase 4: Advanced Features
252
- 1. Rating and review system
253
- 2. Real-time notifications
254
- 3. Image upload handling
255
- 4. Search and filter functionality
256
-
257
- ### Phase 5: Polish & Optimization
258
- 1. Loading states and error handling
259
- 2. Responsive design improvements
260
- 3. Performance optimizations
261
- 4. Testing implementation
262
-
263
- ## 🔗 Backend Integration
264
-
265
- This frontend is designed to work with the LocalLend backend API. Ensure the backend is running on `http://localhost:8080` before starting the frontend development server.
266
-
267
- ### Backend Requirements
268
- - Spring Boot 3.5.6 application
269
- - MongoDB database
270
- - JWT authentication enabled
271
- - CORS configured for `http://localhost:5173` (Vite dev server)
272
-
273
- ## 📚 Documentation Reference
274
-
275
- - [Frontend Integration Guide](../FRONTEND_INTEGRATION_GUIDE.txt) - Complete API reference
276
- - [React Documentation](https://react.dev/)
277
- - [Material-UI Documentation](https://mui.com/)
278
- - [Vite Documentation](https://vitejs.dev/)
279
-
280
- ## 🤝 Contributing
281
-
282
- 1. Follow the existing code structure and patterns
283
- 2. Implement proper TypeScript typing
284
- 3. Add proper error handling for all API calls
285
- 4. Follow the authentication patterns established
286
- 5. Test with the backend API endpoints
287
-
288
- ## ⚠️ Known Issues
289
-
290
- 1. **NPM Installation**: There are currently issues with installing dependencies due to esbuild conflicts
291
- 2. **React Types**: JSX types are not available until React dependencies are properly installed
292
- 3. **Import Meta**: Environment variables access needs proper Vite typing
293
-
294
- ## 🔧 Troubleshooting
295
-
296
- ### NPM Installation Issues
297
- If you encounter esbuild-related errors during `npm install`:
298
-
299
- ```bash
300
- # Method 1: Clean install
301
- rm -rf node_modules package-lock.json
302
- npm cache clean --force
303
- npm install
304
-
305
- # Method 2: Force install
306
- npm install --force
307
-
308
- # Method 3: Use different package manager
309
- yarn install
310
- # or
311
- pnpm install
312
- ```
313
-
314
- ### Development Server Issues
315
- If the dev server fails to start:
316
-
317
- 1. Ensure you're in the correct directory (`locallend-frontend/`)
318
- 2. Verify package.json exists in the current directory
319
- 3. Try running with verbose output: `npm run dev --verbose`
320
-
321
- ---
322
-
323
- **Note:** This project is currently in development phase with complete architectural planning done. All core functionality is designed and ready for implementation once dependency installation is resolved.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/README.md DELETED
@@ -1,73 +0,0 @@
1
- # React + TypeScript + Vite
2
-
3
- This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
-
5
- Currently, two official plugins are available:
6
-
7
- - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8
- - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9
-
10
- ## React Compiler
11
-
12
- The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
-
14
- ## Expanding the ESLint configuration
15
-
16
- If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
-
18
- ```js
19
- export default defineConfig([
20
- globalIgnores(['dist']),
21
- {
22
- files: ['**/*.{ts,tsx}'],
23
- extends: [
24
- // Other configs...
25
-
26
- // Remove tseslint.configs.recommended and replace with this
27
- tseslint.configs.recommendedTypeChecked,
28
- // Alternatively, use this for stricter rules
29
- tseslint.configs.strictTypeChecked,
30
- // Optionally, add this for stylistic rules
31
- tseslint.configs.stylisticTypeChecked,
32
-
33
- // Other configs...
34
- ],
35
- languageOptions: {
36
- parserOptions: {
37
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
- tsconfigRootDir: import.meta.dirname,
39
- },
40
- // other options...
41
- },
42
- },
43
- ])
44
- ```
45
-
46
- You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
47
-
48
- ```js
49
- // eslint.config.js
50
- import reactX from 'eslint-plugin-react-x'
51
- import reactDom from 'eslint-plugin-react-dom'
52
-
53
- export default defineConfig([
54
- globalIgnores(['dist']),
55
- {
56
- files: ['**/*.{ts,tsx}'],
57
- extends: [
58
- // Other configs...
59
- // Enable lint rules for React
60
- reactX.configs['recommended-typescript'],
61
- // Enable lint rules for React DOM
62
- reactDom.configs.recommended,
63
- ],
64
- languageOptions: {
65
- parserOptions: {
66
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
67
- tsconfigRootDir: import.meta.dirname,
68
- },
69
- // other options...
70
- },
71
- },
72
- ])
73
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/SETUP.md DELETED
@@ -1,166 +0,0 @@
1
- # LocalLend Frontend - Installation & Setup Guide
2
-
3
- ## 🚀 Quick Start
4
-
5
- This guide will help you set up the LocalLend frontend application.
6
-
7
- ### Prerequisites
8
-
9
- 1. **Node.js 18+** - Download from [nodejs.org](https://nodejs.org/)
10
- 2. **Backend API** - Ensure the LocalLend backend is running on `http://localhost:8080`
11
- 3. **Git** (optional) - For version control
12
-
13
- ### Step 1: Clone or Navigate to Project
14
-
15
- ```bash
16
- # If you're working with the existing project
17
- cd "d:\Bits\OOAD\Project OOAD\locallend_front\locallend-frontend"
18
- ```
19
-
20
- ### Step 2: Install Dependencies
21
-
22
- The project may have npm installation issues due to esbuild conflicts. Try these approaches:
23
-
24
- #### Method 1: Clean Install
25
- ```bash
26
- # Remove existing node_modules if present
27
- rm -rf node_modules package-lock.json
28
-
29
- # Clear npm cache
30
- npm cache clean --force
31
-
32
- # Install dependencies
33
- npm install
34
- ```
35
-
36
- #### Method 2: Force Install (if Method 1 fails)
37
- ```bash
38
- npm install --force
39
- ```
40
-
41
- #### Method 3: Alternative Package Manager
42
- ```bash
43
- # Using Yarn (install yarn first: npm install -g yarn)
44
- yarn install
45
-
46
- # Or using pnpm (install pnpm first: npm install -g pnpm)
47
- pnpm install
48
- ```
49
-
50
- ### Step 3: Environment Configuration
51
-
52
- ```bash
53
- # Copy environment template
54
- cp .env.example .env
55
-
56
- # Edit .env file with your settings (optional, defaults should work)
57
- # VITE_API_BASE_URL=http://localhost:8080
58
- ```
59
-
60
- ### Step 4: Start Development Server
61
-
62
- ```bash
63
- npm run dev
64
- ```
65
-
66
- The application should open at `http://localhost:5173`
67
-
68
- ## 🛠 Troubleshooting
69
-
70
- ### Common Issues
71
-
72
- #### 1. NPM Install Fails with esbuild Error
73
- ```
74
- Error: EBUSY: resource busy or locked, rmdir 'node_modules\esbuild'
75
- ```
76
-
77
- **Solution:**
78
- - Close VS Code and any other processes that might be using the files
79
- - Delete `node_modules` folder manually
80
- - Try `npm install --force`
81
-
82
- #### 2. Development Server Won't Start
83
- ```
84
- npm error code ENOENT
85
- npm error syscall open
86
- npm error path package.json
87
- ```
88
-
89
- **Solution:**
90
- - Ensure you're in the correct directory (`locallend-frontend/`)
91
- - Verify `package.json` exists
92
- - Use `pwd` (PowerShell) to check current directory
93
-
94
- #### 3. TypeScript/JSX Errors
95
- ```
96
- JSX element implicitly has type 'any'
97
- Cannot find module 'react'
98
- ```
99
-
100
- **Solution:**
101
- - These errors are expected until React dependencies are properly installed
102
- - Complete the installation process first, then restart the dev server
103
-
104
- #### 4. API Connection Issues
105
- ```
106
- Network Error / CORS Error
107
- ```
108
-
109
- **Solution:**
110
- - Ensure backend API is running on `http://localhost:8080`
111
- - Check backend CORS configuration allows `http://localhost:5173`
112
- - Verify API endpoints are accessible via browser
113
-
114
- ### Development Notes
115
-
116
- 1. **Current Status:** The project structure is complete with all TypeScript types, services, and component structures ready
117
- 2. **Dependencies:** Some dependencies may need to be installed/resolved
118
- 3. **Components:** Most components are created as placeholders and will render properly once React is loaded
119
- 4. **API Integration:** All API services are implemented and ready to connect to the backend
120
-
121
- ## 📦 Project Structure Overview
122
-
123
- ```
124
- locallend-frontend/
125
- ├── src/
126
- │ ├── components/ # UI components (Navbar, etc.)
127
- │ ├── pages/ # Page components (Home, Login, etc.)
128
- │ ├── services/ # API service layer
129
- │ ├── context/ # React contexts (Auth)
130
- │ ├── utils/ # Helper functions
131
- │ ├── types/ # TypeScript definitions
132
- │ └── assets/ # Static files
133
- ├── public/ # Public assets
134
- ├── package.json # Dependencies and scripts
135
- ├── vite.config.ts # Vite configuration
136
- ├── tsconfig.json # TypeScript configuration
137
- └── README-LOCALLEND.md # Detailed documentation
138
- ```
139
-
140
- ## 🔗 Next Steps After Installation
141
-
142
- 1. **Verify Backend Connection:** Open browser dev tools and check for successful API calls
143
- 2. **Test Authentication:** Try to register/login a user
144
- 3. **Browse Items:** Navigate through the item listing functionality
145
- 4. **Create Test Data:** Use the backend to create some test categories and items
146
- 5. **UI Development:** Continue implementing the Material-UI components
147
-
148
- ## 📚 Documentation
149
-
150
- - **Complete API Guide:** `../FRONTEND_INTEGRATION_GUIDE.txt`
151
- - **Project Documentation:** `README-LOCALLEND.md`
152
- - **Backend Setup:** Refer to backend documentation for API setup
153
-
154
- ## 🆘 Support
155
-
156
- If you encounter issues:
157
-
158
- 1. Check this troubleshooting guide first
159
- 2. Review the error messages carefully
160
- 3. Ensure backend API is running and accessible
161
- 4. Check network/CORS configuration
162
- 5. Try alternative installation methods above
163
-
164
- ---
165
-
166
- **Note:** This project follows the exact specifications from the Frontend Integration Guide to ensure seamless backend compatibility.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/STATUS_TOGGLE_DEBUG.md DELETED
@@ -1,238 +0,0 @@
1
- # Item Status Toggle - Debugging Guide
2
-
3
- ## 🔍 Issue Analysis
4
-
5
- The "Make Unavailable" button is not working. Based on the Integration Guide, here are potential causes and debugging steps:
6
-
7
- ## 📋 Integration Guide Requirements
8
-
9
- ### **PATCH /api/items/{itemId}/status**
10
- ```
11
- Headers:
12
- - Authorization: Bearer <jwt_token>
13
- - X-User-Id: <user_id>
14
- - Content-Type: application/json
15
-
16
- Request Body:
17
- {
18
- "status": "AVAILABLE" // AVAILABLE|UNAVAILABLE|BORROWED
19
- }
20
-
21
- Response (200): Returns updated item object
22
- ```
23
-
24
- ## 🐛 Potential Issues & Debugging
25
-
26
- ### 1. **Backend Connectivity**
27
- **Added**: Health check to verify backend is reachable
28
- ```javascript
29
- const healthCheck = await fetch('http://localhost:8080/api/items');
30
- ```
31
-
32
- ### 2. **Authentication Issues**
33
- **Added**: Token validation and preview
34
- ```javascript
35
- console.log('🔐 Token exists:', !!token);
36
- console.log('🔐 Token preview:', token.substring(0, 20) + '...');
37
- ```
38
-
39
- ### 3. **Request Format Issues**
40
- **Added**: Detailed request logging
41
- ```javascript
42
- console.log('📝 Request body:', JSON.stringify({ status: newStatus }));
43
- console.log('📝 Request headers:', headers);
44
- ```
45
-
46
- ### 4. **Status Validation Issues**
47
- **Added**: Client-side validation
48
- ```javascript
49
- // Validate current status
50
- if (!['AVAILABLE', 'UNAVAILABLE', 'BORROWED'].includes(currentStatus)) {
51
- alert('Invalid current item status');
52
- return;
53
- }
54
-
55
- // Prevent changing borrowed items
56
- if (currentStatus === 'BORROWED') {
57
- alert('Cannot change status of borrowed items');
58
- return;
59
- }
60
- ```
61
-
62
- ### 5. **Backend Response Issues**
63
- **Added**: Detailed error parsing
64
- ```javascript
65
- // Parse different error types
66
- if (directResponse.status === 400) {
67
- errorMessage = 'Bad Request: Invalid status value or request format';
68
- } else if (directResponse.status === 403) {
69
- errorMessage = 'Forbidden: You are not the owner of this item';
70
- } else if (directResponse.status === 404) {
71
- errorMessage = 'Not Found: Item not found or endpoint not available';
72
- }
73
- ```
74
-
75
- ## 🧪 Enhanced Debugging Steps
76
-
77
- ### **Step 1: Check Browser Console**
78
- 1. Open Developer Tools → Console
79
- 2. Click "Make Unavailable" button
80
- 3. Look for these logs:
81
- - `🏥 Backend health check for items endpoint: 200`
82
- - `🔐 Token exists: true`
83
- - `📝 Status change: AVAILABLE → UNAVAILABLE`
84
- - `📡 Direct response status: 200`
85
-
86
- ### **Step 2: Check Network Tab**
87
- 1. Open Developer Tools → Network tab
88
- 2. Filter by "Fetch/XHR"
89
- 3. Click the status toggle button
90
- 4. Look for PATCH request to `/api/items/{itemId}/status`
91
- 5. Check:
92
- - Request headers (Authorization, X-User-Id)
93
- - Request body: `{"status":"UNAVAILABLE"}`
94
- - Response status and body
95
-
96
- ### **Step 3: Test Backend Directly**
97
- Test the backend endpoint directly:
98
- ```bash
99
- # Replace with actual values
100
- curl -X PATCH http://localhost:8080/api/items/{ITEM_ID}/status \
101
- -H "Content-Type: application/json" \
102
- -H "Authorization: Bearer {JWT_TOKEN}" \
103
- -H "X-User-Id: {USER_ID}" \
104
- -d '{"status":"UNAVAILABLE"}'
105
- ```
106
-
107
- ## 🔧 Common Error Scenarios
108
-
109
- ### **400 Bad Request**
110
- **Possible Causes:**
111
- - Invalid status value (not AVAILABLE/UNAVAILABLE/BORROWED)
112
- - Missing Content-Type header
113
- - Malformed JSON body
114
- - Invalid item ID format
115
-
116
- **Debug**: Check request body format and headers
117
-
118
- ### **401 Unauthorized**
119
- **Possible Causes:**
120
- - Missing Authorization header
121
- - Invalid/expired JWT token
122
- - Token format incorrect
123
-
124
- **Debug**: Check token existence and format
125
-
126
- ### **403 Forbidden**
127
- **Possible Causes:**
128
- - User is not the owner of the item
129
- - Missing X-User-Id header
130
- - X-User-Id doesn't match token user
131
-
132
- **Debug**: Verify user ownership and header presence
133
-
134
- ### **404 Not Found**
135
- **Possible Causes:**
136
- - Item doesn't exist
137
- - Invalid item ID
138
- - Backend endpoint not available
139
- - Wrong API URL
140
-
141
- **Debug**: Verify item exists and API URL is correct
142
-
143
- ## 🎯 Alternative Implementation
144
-
145
- If the direct approach fails, try using the service layer properly:
146
-
147
- ```typescript
148
- // Alternative: Use service layer with better error handling
149
- const handleToggleStatusAlternative = async (itemId: string, currentStatus: string) => {
150
- try {
151
- const newStatus = currentStatus === 'AVAILABLE' ? 'UNAVAILABLE' : 'AVAILABLE';
152
-
153
- // Use the item service
154
- const updatedItem = await itemService.updateItemStatus(itemId, newStatus, user.id);
155
-
156
- // Update local state
157
- setMyItems(prevItems =>
158
- prevItems.map(item =>
159
- item.id === itemId ? { ...item, status: updatedItem.status } : item
160
- )
161
- );
162
-
163
- alert(`✅ Item status changed to ${updatedItem.status}`);
164
- } catch (error: any) {
165
- console.error('Service layer error:', error);
166
-
167
- // Extract detailed error information
168
- let errorMessage = 'Failed to update item status';
169
- if (error?.response?.status === 400) {
170
- errorMessage = 'Invalid request: Check item status and permissions';
171
- } else if (error?.response?.status === 403) {
172
- errorMessage = 'You are not authorized to modify this item';
173
- } else if (error?.response?.data?.message) {
174
- errorMessage = error.response.data.message;
175
- }
176
-
177
- alert(`❌ ${errorMessage}`);
178
- }
179
- };
180
- ```
181
-
182
- ## 🔍 Backend Validation Checklist
183
-
184
- ### **Item Requirements:**
185
- - ✅ Item must exist
186
- - ✅ Item must be owned by current user
187
- - ✅ Item must not be currently borrowed (status !== 'BORROWED')
188
-
189
- ### **Request Requirements:**
190
- - ✅ Valid JWT token in Authorization header
191
- - ✅ Correct X-User-Id header
192
- - ✅ Valid status value (AVAILABLE/UNAVAILABLE)
193
- - ✅ Proper JSON format
194
-
195
- ### **Permission Requirements:**
196
- - ✅ User must be authenticated
197
- - ✅ User must own the item
198
- - ✅ No active bookings preventing status change
199
-
200
- ## 📱 Testing Instructions
201
-
202
- ### **Test Valid Status Change:**
203
- 1. Find an item with status "AVAILABLE"
204
- 2. Click "⏸️ Make Unavailable"
205
- 3. Should see success message
206
- 4. Item status should change to "UNAVAILABLE"
207
- 5. Button should change to "▶️ Make Available"
208
-
209
- ### **Test Error Scenarios:**
210
- 1. Try changing status of borrowed item
211
- 2. Try with network disconnected
212
- 3. Try with invalid authentication
213
-
214
- ### **Verify UI Updates:**
215
- 1. Status badge color should change
216
- 2. Button text should update
217
- 3. Local state should reflect change immediately
218
-
219
- ## 🎯 Expected Behavior
220
-
221
- ### **Success Flow:**
222
- 1. User clicks status toggle button
223
- 2. Confirmation (optional)
224
- 3. API call with proper headers
225
- 4. Backend validates ownership and status
226
- 5. Backend updates item status
227
- 6. Frontend receives updated item
228
- 7. UI updates immediately
229
- 8. Success message shown
230
-
231
- ### **Error Flow:**
232
- 1. User clicks status toggle button
233
- 2. API call fails with specific error
234
- 3. Detailed error message shown
235
- 4. UI remains in original state
236
- 5. User can retry or fix issue
237
-
238
- The enhanced debugging should now pinpoint exactly where the status toggle is failing!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/TOGGLE_FIX_SUMMARY.md DELETED
@@ -1,96 +0,0 @@
1
- # TOGGLE FIX: Prioritize Specific Status Setting
2
-
3
- ## 🎯 **Issue Fixed**
4
-
5
- **Problem:** Clicking "Make Unavailable" was using the toggle endpoint first, which just flips the status regardless of intent.
6
-
7
- **Result:** Item that was already UNAVAILABLE would become AVAILABLE when clicking "Make Unavailable" (opposite of expected behavior).
8
-
9
- ---
10
-
11
- ## 🔧 **Solution Applied**
12
-
13
- ### **Changed Endpoint Priority Order:**
14
-
15
- **BEFORE (Wrong):**
16
- 1. Try `/api/items/{id}/toggle-availability` (just flips status)
17
- 2. Fallback to `/api/items/{id}/availability` (sets specific status)
18
-
19
- **AFTER (Fixed):**
20
- 1. Try `/api/items/{id}/availability` (sets specific status) ✅ **PREFERRED**
21
- 2. Fallback to `/api/items/{id}/toggle-availability` (just flips status)
22
-
23
- ### **Why This Fixes It:**
24
-
25
- - **`/availability` endpoint:** Lets us set EXACTLY the status we want (AVAILABLE or UNAVAILABLE)
26
- - **`/toggle-availability` endpoint:** Just flips whatever the current status is
27
-
28
- By using `/availability` first, we ensure:
29
- - ✅ "Make Unavailable" → Always sets to UNAVAILABLE
30
- - ✅ "Make Available" → Always sets to AVAILABLE
31
- - ✅ No unwanted toggling behavior
32
-
33
- ---
34
-
35
- ## 📁 **Files Updated:**
36
-
37
- ### **1. DashboardPage.tsx**
38
- ```typescript
39
- // Now tries /availability first (with specific status)
40
- endpointUsed = `/api/items/${itemId}/availability`;
41
- directResponse = await fetch(url, {
42
- method: 'PATCH',
43
- body: JSON.stringify({ status: newStatus }) // Specific status!
44
- });
45
-
46
- // Only uses toggle as fallback if availability doesn't exist
47
- if (directResponse.status === 404) {
48
- endpointUsed = `/api/items/${itemId}/toggle-availability`;
49
- // ...
50
- }
51
- ```
52
-
53
- ### **2. itemService.ts**
54
- ```typescript
55
- updateItemStatus: async (itemId: string, status: ItemStatus, userId: string) => {
56
- // Try availability endpoint first (specific status setting)
57
- try {
58
- return await api.patch(`/api/items/${itemId}/availability`, { status });
59
- } catch (error) {
60
- // Fallback to toggle only if availability doesn't exist
61
- if (error?.response?.status === 404) {
62
- return await api.patch(`/api/items/${itemId}/toggle-availability`);
63
- }
64
- throw error;
65
- }
66
- }
67
- ```
68
-
69
- ---
70
-
71
- ## 🎯 **Expected Behavior Now:**
72
-
73
- ### **When clicking "Make Unavailable":**
74
- 1. Frontend sends: `PATCH /api/items/{id}/availability` with `{"status":"UNAVAILABLE"}`
75
- 2. Backend sets item status to UNAVAILABLE (regardless of current status)
76
- 3. Button changes to "Make Available"
77
- 4. Status badge shows red/unavailable
78
-
79
- ### **When clicking "Make Available":**
80
- 1. Frontend sends: `PATCH /api/items/{id}/availability` with `{"status":"AVAILABLE"}`
81
- 2. Backend sets item status to AVAILABLE (regardless of current status)
82
- 3. Button changes to "Make Unavailable"
83
- 4. Status badge shows green/available
84
-
85
- ---
86
-
87
- ## 🧪 **Test It:**
88
-
89
- 1. **Find an item that shows "AVAILABLE"**
90
- 2. **Click "Make Unavailable"**
91
- - Should change to UNAVAILABLE and button becomes "Make Available"
92
- 3. **Click "Make Available"**
93
- - Should change to AVAILABLE and button becomes "Make Unavailable"
94
- 4. **Repeat several times** - should work predictably every time
95
-
96
- **No more unwanted toggling!** The button will now do exactly what it says.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/TOGGLE_SIMPLIFIED.md DELETED
@@ -1,95 +0,0 @@
1
- # SIMPLIFIED: Using Toggle Endpoint Only
2
-
3
- ## 🎯 **Simplification Applied**
4
-
5
- **Removed:** Complex endpoint fallback logic with `/availability` endpoint
6
- **Using:** Only the `/api/items/{id}/toggle-availability` endpoint
7
-
8
- ---
9
-
10
- ## 🔧 **What Changed**
11
-
12
- ### **BEFORE (Complex with fallbacks):**
13
- ```typescript
14
- // Try /availability endpoint first with { isAvailable: boolean }
15
- // Fallback to /toggle-availability if 404
16
- // Handle different request body formats
17
- // Complex error handling
18
- ```
19
-
20
- ### **AFTER (Simple toggle only):**
21
- ```typescript
22
- // Just call /toggle-availability
23
- // No request body needed
24
- // Let backend handle the toggle logic
25
- // Much simpler!
26
- ```
27
-
28
- ---
29
-
30
- ## 📁 **Files Updated:**
31
-
32
- ### **1. itemService.ts**
33
- ```typescript
34
- // BEFORE: Complex dual-endpoint logic
35
- updateItemStatus: async (itemId: string, status: ItemStatus, userId: string) => {
36
- // Try availability endpoint...
37
- // Fallback to toggle...
38
- }
39
-
40
- // AFTER: Simple toggle only
41
- updateItemStatus: async (itemId: string, _status: ItemStatus, userId: string) => {
42
- return await api.patch(`/api/items/${itemId}/toggle-availability`, null, {
43
- headers: addUserIdHeader(userId)
44
- });
45
- }
46
- ```
47
-
48
- ### **2. DashboardPage.tsx**
49
- ```typescript
50
- // BEFORE: Try availability, then toggle
51
- endpointUsed = `/api/items/${itemId}/availability`;
52
- // ... complex fallback logic
53
-
54
- // AFTER: Just toggle
55
- endpointUsed = `/api/items/${itemId}/toggle-availability`;
56
- // No body needed, just PATCH request
57
- ```
58
-
59
- ---
60
-
61
- ## 🎯 **How It Works Now:**
62
-
63
- ### **When clicking "Make Unavailable":**
64
- 1. Frontend calls: `PATCH /api/items/{id}/toggle-availability`
65
- 2. Backend looks at current status and flips it
66
- 3. If currently AVAILABLE → becomes UNAVAILABLE
67
- 4. Returns updated item with new status
68
- 5. Frontend updates UI
69
-
70
- ### **When clicking "Make Available":**
71
- 1. Frontend calls: `PATCH /api/items/{id}/toggle-availability`
72
- 2. Backend looks at current status and flips it
73
- 3. If currently UNAVAILABLE → becomes AVAILABLE
74
- 4. Returns updated item with new status
75
- 5. Frontend updates UI
76
-
77
- ---
78
-
79
- ## ✅ **Benefits:**
80
-
81
- - **✅ Simpler code** - No complex endpoint fallback logic
82
- - **✅ Less error prone** - One endpoint, one request format
83
- - **✅ Backend handles logic** - Let the backend decide what "toggle" means
84
- - **✅ No field name issues** - No request body means no JSON parsing errors
85
-
86
- ---
87
-
88
- ## 🧪 **Test It:**
89
-
90
- 1. Go to Dashboard → My Items
91
- 2. Click any status toggle button
92
- 3. Should work smoothly with just the toggle endpoint
93
- 4. Button text and status should update correctly
94
-
95
- **Much simpler and should work reliably now!**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/VALIDATION_FIXES.md DELETED
@@ -1,112 +0,0 @@
1
- # Registration Validation Fixes
2
-
3
- ## Issues Found and Fixed
4
-
5
- ### 1. Frontend Validation Mismatches
6
- **FIXED** - The following validation rules were corrected to match backend requirements:
7
-
8
- #### Username Field ✅
9
- - **Requirement**: 3-50 characters, alphanumeric
10
- - **Status**: Already correct in frontend
11
-
12
- #### Full Name Field ❌➡️✅
13
- - **Backend Requirement**: 2-100 characters, required
14
- - **Frontend Before**: No length validation
15
- - **Frontend After**: Added `minLength={2}` and `maxLength={100}`
16
-
17
- #### Email Field ✅
18
- - **Requirement**: Valid email format, required
19
- - **Status**: Already correct with `type="email"`
20
-
21
- #### Password Field ❌➡️✅
22
- - **Backend Requirement**: Minimum 8 characters
23
- - **Frontend Before**: `minLength={6}`
24
- - **Frontend After**: Changed to `minLength={8}` + updated label
25
-
26
- #### Phone Number Field ❌➡️✅
27
- - **Backend Requirement**: Optional field
28
- - **Frontend Before**: `required` attribute set
29
- - **Frontend After**: Removed `required`, updated label to "(Optional)"
30
-
31
- #### Confirm Password Field ❌➡️✅
32
- - **Frontend Before**: `minLength={6}`
33
- - **Frontend After**: Changed to `minLength={8}` to match password field
34
-
35
- ### 2. Client-Side Validation Enhancement
36
- **ADDED** - Comprehensive validation in `handleSubmit` function:
37
-
38
- ```typescript
39
- // Client-side validation to match backend requirements
40
- const validationErrors: string[] = [];
41
-
42
- if (formData.username.length < 3 || formData.username.length > 50) {
43
- validationErrors.push('Username must be between 3 and 50 characters');
44
- }
45
-
46
- if (formData.name.length < 2 || formData.name.length > 100) {
47
- validationErrors.push('Full name must be between 2 and 100 characters');
48
- }
49
-
50
- if (formData.password.length < 8) {
51
- validationErrors.push('Password must be at least 8 characters long');
52
- }
53
-
54
- if (formData.password !== formData.confirmPassword) {
55
- validationErrors.push('Passwords do not match');
56
- }
57
-
58
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
59
- if (!emailRegex.test(formData.email)) {
60
- validationErrors.push('Please enter a valid email address');
61
- }
62
- ```
63
-
64
- ### 3. Backend Connectivity Issue
65
- **IDENTIFIED** - Backend is not running:
66
- - The `docker-compose.yml` has the backend service commented out
67
- - This means when you try to register, the API call fails with network errors
68
-
69
- ## Testing the Registration
70
-
71
- ### Method 1: Test with Name "user" (Should Work Now)
72
- 1. Username: `testuser` (3+ characters)
73
- 2. Full Name: `user` (2+ characters - now valid!)
74
- 3. Email: `test@example.com`
75
- 4. Password: `password123` (8+ characters)
76
- 5. Phone: Leave empty (now optional)
77
-
78
- ### Method 2: Start Backend First
79
- To actually register and connect to backend:
80
-
81
- 1. **Option A: Start Backend via Docker**
82
- ```bash
83
- # If you have backend docker image
84
- docker run -p 8080:8080 locallend/backend:latest
85
- ```
86
-
87
- 2. **Option B: Enable Backend in docker-compose**
88
- - Uncomment the backend service in `docker-compose.yml`
89
- - Run: `docker-compose up`
90
-
91
- 3. **Option C: Check if Backend is Running**
92
- - Use the "Test Backend Connection" button in the registration page
93
- - Try alternative ports (8081, 8082, etc.) using the port test buttons
94
-
95
- ## Validation Rules Summary (Now Aligned)
96
-
97
- | Field | Frontend | Backend | Status |
98
- |-------|----------|---------|---------|
99
- | Username | 3-50 chars, required | 3-50 chars, alphanumeric, unique | ✅ Aligned |
100
- | Name | 2-100 chars, required | 2-100 chars, required | ✅ Fixed |
101
- | Email | Valid email, required | Valid email, unique | ✅ Aligned |
102
- | Password | 8+ chars, required | Min 8 chars, required | ✅ Fixed |
103
- | Phone | Optional | Optional | ✅ Fixed |
104
-
105
- ## Next Steps
106
-
107
- 1. **Test the registration form** with name "user" - it should now work on the frontend
108
- 2. **Start the backend** to test full registration flow
109
- 3. **Check browser console** for detailed API call logs
110
- 4. **Use the connection test buttons** to verify backend connectivity
111
-
112
- The frontend validation now matches the backend requirements from the integration guide!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/eslint.config.js DELETED
@@ -1,23 +0,0 @@
1
- import js from '@eslint/js'
2
- import globals from 'globals'
3
- import reactHooks from 'eslint-plugin-react-hooks'
4
- import reactRefresh from 'eslint-plugin-react-refresh'
5
- import tseslint from 'typescript-eslint'
6
- import { defineConfig, globalIgnores } from 'eslint/config'
7
-
8
- export default defineConfig([
9
- globalIgnores(['dist']),
10
- {
11
- files: ['**/*.{ts,tsx}'],
12
- extends: [
13
- js.configs.recommended,
14
- tseslint.configs.recommended,
15
- reactHooks.configs['recommended-latest'],
16
- reactRefresh.configs.vite,
17
- ],
18
- languageOptions: {
19
- ecmaVersion: 2020,
20
- globals: globals.browser,
21
- },
22
- },
23
- ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/healthcheck.sh DELETED
@@ -1,11 +0,0 @@
1
- #!/bin/sh
2
- # Health check script for LocalLend Frontend container
3
-
4
- # Check if Nginx is running and serving content
5
- if wget --quiet --tries=1 --spider http://localhost:80/; then
6
- echo "Health check passed: Frontend is responding"
7
- exit 0
8
- else
9
- echo "Health check failed: Frontend is not responding"
10
- exit 1
11
- fi
 
 
 
 
 
 
 
 
 
 
 
 
frontend/index.html DELETED
@@ -1,13 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>locallend-frontend</title>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="/src/main.tsx"></script>
12
- </body>
13
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/nginx.conf DELETED
@@ -1,52 +0,0 @@
1
- server {
2
- listen 0.0.0.0:7860;
3
- server_name _;
4
- root /usr/share/nginx/html;
5
- index index.html;
6
-
7
- # Handle client-side routing (for React Router)
8
- location / {
9
- try_files $uri $uri/ /index.html;
10
- }
11
-
12
- # Proxy API requests to Spring Boot backend running on port 8080
13
- location /api/ {
14
- proxy_pass http://localhost:8080/;
15
- proxy_set_header Host $host;
16
- proxy_set_header X-Real-IP $remote_addr;
17
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18
- proxy_set_header X-Forwarded-Proto $scheme;
19
-
20
- # WebSocket support (if needed)
21
- proxy_http_version 1.1;
22
- proxy_set_header Upgrade $http_upgrade;
23
- proxy_set_header Connection "upgrade";
24
-
25
- # Timeout settings
26
- proxy_connect_timeout 60s;
27
- proxy_send_timeout 60s;
28
- proxy_read_timeout 60s;
29
- }
30
-
31
- # Health check endpoint for HF Spaces
32
- location /health {
33
- access_log off;
34
- return 200 "healthy\n";
35
- add_header Content-Type text/plain;
36
- }
37
-
38
- # Cache static assets
39
- location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
40
- expires 1y;
41
- add_header Cache-Control "public, immutable";
42
- }
43
-
44
- # Security headers
45
- add_header X-Frame-Options "SAMEORIGIN" always;
46
- add_header X-Content-Type-Options "nosniff" always;
47
- add_header X-XSS-Protection "1; mode=block" always;
48
-
49
- # Gzip compression
50
- gzip on;
51
- gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
52
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json DELETED
@@ -1,3289 +0,0 @@
1
- {
2
- "name": "locallend-frontend",
3
- "version": "0.0.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "locallend-frontend",
9
- "version": "0.0.0",
10
- "dependencies": {
11
- "@emotion/react": "^11.13.3",
12
- "@emotion/styled": "^11.13.0",
13
- "@hookform/resolvers": "^3.9.1",
14
- "@mui/icons-material": "^6.1.7",
15
- "@mui/material": "^6.1.7",
16
- "@mui/x-date-pickers": "^7.20.0",
17
- "axios": "^1.7.7",
18
- "dayjs": "^1.11.13",
19
- "react": "^19.1.1",
20
- "react-dom": "^19.1.1",
21
- "react-hook-form": "^7.53.2",
22
- "react-hot-toast": "^2.4.1",
23
- "react-router-dom": "^6.27.0",
24
- "yup": "^1.4.0"
25
- },
26
- "devDependencies": {
27
- "@eslint/js": "^9.36.0",
28
- "@types/node": "^24.6.0",
29
- "@types/react": "^19.1.16",
30
- "@types/react-dom": "^19.1.9",
31
- "@vitejs/plugin-react": "^5.0.4",
32
- "eslint": "^9.36.0",
33
- "eslint-plugin-react-hooks": "^5.2.0",
34
- "eslint-plugin-react-refresh": "^0.4.22",
35
- "globals": "^16.4.0",
36
- "typescript": "~5.9.3",
37
- "typescript-eslint": "^8.45.0",
38
- "vite": "^7.1.7"
39
- }
40
- },
41
- "node_modules/@babel/code-frame": {
42
- "version": "7.27.1",
43
- "license": "MIT",
44
- "dependencies": {
45
- "@babel/helper-validator-identifier": "^7.27.1",
46
- "js-tokens": "^4.0.0",
47
- "picocolors": "^1.1.1"
48
- },
49
- "engines": {
50
- "node": ">=6.9.0"
51
- }
52
- },
53
- "node_modules/@babel/compat-data": {
54
- "version": "7.28.5",
55
- "dev": true,
56
- "license": "MIT",
57
- "engines": {
58
- "node": ">=6.9.0"
59
- }
60
- },
61
- "node_modules/@babel/core": {
62
- "version": "7.28.5",
63
- "dev": true,
64
- "license": "MIT",
65
- "dependencies": {
66
- "@babel/code-frame": "^7.27.1",
67
- "@babel/generator": "^7.28.5",
68
- "@babel/helper-compilation-targets": "^7.27.2",
69
- "@babel/helper-module-transforms": "^7.28.3",
70
- "@babel/helpers": "^7.28.4",
71
- "@babel/parser": "^7.28.5",
72
- "@babel/template": "^7.27.2",
73
- "@babel/traverse": "^7.28.5",
74
- "@babel/types": "^7.28.5",
75
- "@jridgewell/remapping": "^2.3.5",
76
- "convert-source-map": "^2.0.0",
77
- "debug": "^4.1.0",
78
- "gensync": "^1.0.0-beta.2",
79
- "json5": "^2.2.3",
80
- "semver": "^6.3.1"
81
- },
82
- "engines": {
83
- "node": ">=6.9.0"
84
- },
85
- "funding": {
86
- "type": "opencollective",
87
- "url": "https://opencollective.com/babel"
88
- }
89
- },
90
- "node_modules/@babel/core/node_modules/convert-source-map": {
91
- "version": "2.0.0",
92
- "dev": true,
93
- "license": "MIT"
94
- },
95
- "node_modules/@babel/generator": {
96
- "version": "7.28.5",
97
- "license": "MIT",
98
- "dependencies": {
99
- "@babel/parser": "^7.28.5",
100
- "@babel/types": "^7.28.5",
101
- "@jridgewell/gen-mapping": "^0.3.12",
102
- "@jridgewell/trace-mapping": "^0.3.28",
103
- "jsesc": "^3.0.2"
104
- },
105
- "engines": {
106
- "node": ">=6.9.0"
107
- }
108
- },
109
- "node_modules/@babel/helper-compilation-targets": {
110
- "version": "7.27.2",
111
- "dev": true,
112
- "license": "MIT",
113
- "dependencies": {
114
- "@babel/compat-data": "^7.27.2",
115
- "@babel/helper-validator-option": "^7.27.1",
116
- "browserslist": "^4.24.0",
117
- "lru-cache": "^5.1.1",
118
- "semver": "^6.3.1"
119
- },
120
- "engines": {
121
- "node": ">=6.9.0"
122
- }
123
- },
124
- "node_modules/@babel/helper-globals": {
125
- "version": "7.28.0",
126
- "license": "MIT",
127
- "engines": {
128
- "node": ">=6.9.0"
129
- }
130
- },
131
- "node_modules/@babel/helper-module-imports": {
132
- "version": "7.27.1",
133
- "license": "MIT",
134
- "dependencies": {
135
- "@babel/traverse": "^7.27.1",
136
- "@babel/types": "^7.27.1"
137
- },
138
- "engines": {
139
- "node": ">=6.9.0"
140
- }
141
- },
142
- "node_modules/@babel/helper-module-transforms": {
143
- "version": "7.28.3",
144
- "dev": true,
145
- "license": "MIT",
146
- "dependencies": {
147
- "@babel/helper-module-imports": "^7.27.1",
148
- "@babel/helper-validator-identifier": "^7.27.1",
149
- "@babel/traverse": "^7.28.3"
150
- },
151
- "engines": {
152
- "node": ">=6.9.0"
153
- },
154
- "peerDependencies": {
155
- "@babel/core": "^7.0.0"
156
- }
157
- },
158
- "node_modules/@babel/helper-plugin-utils": {
159
- "version": "7.27.1",
160
- "dev": true,
161
- "license": "MIT",
162
- "engines": {
163
- "node": ">=6.9.0"
164
- }
165
- },
166
- "node_modules/@babel/helper-string-parser": {
167
- "version": "7.27.1",
168
- "license": "MIT",
169
- "engines": {
170
- "node": ">=6.9.0"
171
- }
172
- },
173
- "node_modules/@babel/helper-validator-identifier": {
174
- "version": "7.28.5",
175
- "license": "MIT",
176
- "engines": {
177
- "node": ">=6.9.0"
178
- }
179
- },
180
- "node_modules/@babel/helper-validator-option": {
181
- "version": "7.27.1",
182
- "dev": true,
183
- "license": "MIT",
184
- "engines": {
185
- "node": ">=6.9.0"
186
- }
187
- },
188
- "node_modules/@babel/helpers": {
189
- "version": "7.28.4",
190
- "dev": true,
191
- "license": "MIT",
192
- "dependencies": {
193
- "@babel/template": "^7.27.2",
194
- "@babel/types": "^7.28.4"
195
- },
196
- "engines": {
197
- "node": ">=6.9.0"
198
- }
199
- },
200
- "node_modules/@babel/parser": {
201
- "version": "7.28.5",
202
- "license": "MIT",
203
- "dependencies": {
204
- "@babel/types": "^7.28.5"
205
- },
206
- "bin": {
207
- "parser": "bin/babel-parser.js"
208
- },
209
- "engines": {
210
- "node": ">=6.0.0"
211
- }
212
- },
213
- "node_modules/@babel/plugin-transform-react-jsx-self": {
214
- "version": "7.27.1",
215
- "dev": true,
216
- "license": "MIT",
217
- "dependencies": {
218
- "@babel/helper-plugin-utils": "^7.27.1"
219
- },
220
- "engines": {
221
- "node": ">=6.9.0"
222
- },
223
- "peerDependencies": {
224
- "@babel/core": "^7.0.0-0"
225
- }
226
- },
227
- "node_modules/@babel/plugin-transform-react-jsx-source": {
228
- "version": "7.27.1",
229
- "dev": true,
230
- "license": "MIT",
231
- "dependencies": {
232
- "@babel/helper-plugin-utils": "^7.27.1"
233
- },
234
- "engines": {
235
- "node": ">=6.9.0"
236
- },
237
- "peerDependencies": {
238
- "@babel/core": "^7.0.0-0"
239
- }
240
- },
241
- "node_modules/@babel/runtime": {
242
- "version": "7.28.4",
243
- "license": "MIT",
244
- "engines": {
245
- "node": ">=6.9.0"
246
- }
247
- },
248
- "node_modules/@babel/template": {
249
- "version": "7.27.2",
250
- "license": "MIT",
251
- "dependencies": {
252
- "@babel/code-frame": "^7.27.1",
253
- "@babel/parser": "^7.27.2",
254
- "@babel/types": "^7.27.1"
255
- },
256
- "engines": {
257
- "node": ">=6.9.0"
258
- }
259
- },
260
- "node_modules/@babel/traverse": {
261
- "version": "7.28.5",
262
- "license": "MIT",
263
- "dependencies": {
264
- "@babel/code-frame": "^7.27.1",
265
- "@babel/generator": "^7.28.5",
266
- "@babel/helper-globals": "^7.28.0",
267
- "@babel/parser": "^7.28.5",
268
- "@babel/template": "^7.27.2",
269
- "@babel/types": "^7.28.5",
270
- "debug": "^4.3.1"
271
- },
272
- "engines": {
273
- "node": ">=6.9.0"
274
- }
275
- },
276
- "node_modules/@babel/types": {
277
- "version": "7.28.5",
278
- "license": "MIT",
279
- "dependencies": {
280
- "@babel/helper-string-parser": "^7.27.1",
281
- "@babel/helper-validator-identifier": "^7.28.5"
282
- },
283
- "engines": {
284
- "node": ">=6.9.0"
285
- }
286
- },
287
- "node_modules/@emotion/babel-plugin": {
288
- "version": "11.13.5",
289
- "license": "MIT",
290
- "dependencies": {
291
- "@babel/helper-module-imports": "^7.16.7",
292
- "@babel/runtime": "^7.18.3",
293
- "@emotion/hash": "^0.9.2",
294
- "@emotion/memoize": "^0.9.0",
295
- "@emotion/serialize": "^1.3.3",
296
- "babel-plugin-macros": "^3.1.0",
297
- "convert-source-map": "^1.5.0",
298
- "escape-string-regexp": "^4.0.0",
299
- "find-root": "^1.1.0",
300
- "source-map": "^0.5.7",
301
- "stylis": "4.2.0"
302
- }
303
- },
304
- "node_modules/@emotion/cache": {
305
- "version": "11.14.0",
306
- "license": "MIT",
307
- "dependencies": {
308
- "@emotion/memoize": "^0.9.0",
309
- "@emotion/sheet": "^1.4.0",
310
- "@emotion/utils": "^1.4.2",
311
- "@emotion/weak-memoize": "^0.4.0",
312
- "stylis": "4.2.0"
313
- }
314
- },
315
- "node_modules/@emotion/hash": {
316
- "version": "0.9.2",
317
- "license": "MIT"
318
- },
319
- "node_modules/@emotion/is-prop-valid": {
320
- "version": "1.4.0",
321
- "license": "MIT",
322
- "dependencies": {
323
- "@emotion/memoize": "^0.9.0"
324
- }
325
- },
326
- "node_modules/@emotion/memoize": {
327
- "version": "0.9.0",
328
- "license": "MIT"
329
- },
330
- "node_modules/@emotion/react": {
331
- "version": "11.14.0",
332
- "license": "MIT",
333
- "dependencies": {
334
- "@babel/runtime": "^7.18.3",
335
- "@emotion/babel-plugin": "^11.13.5",
336
- "@emotion/cache": "^11.14.0",
337
- "@emotion/serialize": "^1.3.3",
338
- "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
339
- "@emotion/utils": "^1.4.2",
340
- "@emotion/weak-memoize": "^0.4.0",
341
- "hoist-non-react-statics": "^3.3.1"
342
- },
343
- "peerDependencies": {
344
- "react": ">=16.8.0"
345
- },
346
- "peerDependenciesMeta": {
347
- "@types/react": {
348
- "optional": true
349
- }
350
- }
351
- },
352
- "node_modules/@emotion/serialize": {
353
- "version": "1.3.3",
354
- "license": "MIT",
355
- "dependencies": {
356
- "@emotion/hash": "^0.9.2",
357
- "@emotion/memoize": "^0.9.0",
358
- "@emotion/unitless": "^0.10.0",
359
- "@emotion/utils": "^1.4.2",
360
- "csstype": "^3.0.2"
361
- }
362
- },
363
- "node_modules/@emotion/sheet": {
364
- "version": "1.4.0",
365
- "license": "MIT"
366
- },
367
- "node_modules/@emotion/styled": {
368
- "version": "11.14.1",
369
- "license": "MIT",
370
- "dependencies": {
371
- "@babel/runtime": "^7.18.3",
372
- "@emotion/babel-plugin": "^11.13.5",
373
- "@emotion/is-prop-valid": "^1.3.0",
374
- "@emotion/serialize": "^1.3.3",
375
- "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
376
- "@emotion/utils": "^1.4.2"
377
- },
378
- "peerDependencies": {
379
- "@emotion/react": "^11.0.0-rc.0",
380
- "react": ">=16.8.0"
381
- },
382
- "peerDependenciesMeta": {
383
- "@types/react": {
384
- "optional": true
385
- }
386
- }
387
- },
388
- "node_modules/@emotion/unitless": {
389
- "version": "0.10.0",
390
- "license": "MIT"
391
- },
392
- "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
393
- "version": "1.2.0",
394
- "license": "MIT",
395
- "peerDependencies": {
396
- "react": ">=16.8.0"
397
- }
398
- },
399
- "node_modules/@emotion/utils": {
400
- "version": "1.4.2",
401
- "license": "MIT"
402
- },
403
- "node_modules/@emotion/weak-memoize": {
404
- "version": "0.4.0",
405
- "license": "MIT"
406
- },
407
- "node_modules/@esbuild/linux-x64": {
408
- "version": "0.25.12",
409
- "cpu": [
410
- "x64"
411
- ],
412
- "dev": true,
413
- "license": "MIT",
414
- "optional": true,
415
- "os": [
416
- "linux"
417
- ],
418
- "engines": {
419
- "node": ">=18"
420
- }
421
- },
422
- "node_modules/@eslint-community/eslint-utils": {
423
- "version": "4.9.0",
424
- "dev": true,
425
- "license": "MIT",
426
- "dependencies": {
427
- "eslint-visitor-keys": "^3.4.3"
428
- },
429
- "engines": {
430
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
431
- },
432
- "funding": {
433
- "url": "https://opencollective.com/eslint"
434
- },
435
- "peerDependencies": {
436
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
437
- }
438
- },
439
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
440
- "version": "3.4.3",
441
- "dev": true,
442
- "license": "Apache-2.0",
443
- "engines": {
444
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
445
- },
446
- "funding": {
447
- "url": "https://opencollective.com/eslint"
448
- }
449
- },
450
- "node_modules/@eslint-community/regexpp": {
451
- "version": "4.12.2",
452
- "dev": true,
453
- "license": "MIT",
454
- "engines": {
455
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
456
- }
457
- },
458
- "node_modules/@eslint/config-array": {
459
- "version": "0.21.1",
460
- "dev": true,
461
- "license": "Apache-2.0",
462
- "dependencies": {
463
- "@eslint/object-schema": "^2.1.7",
464
- "debug": "^4.3.1",
465
- "minimatch": "^3.1.2"
466
- },
467
- "engines": {
468
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
469
- }
470
- },
471
- "node_modules/@eslint/config-helpers": {
472
- "version": "0.4.2",
473
- "dev": true,
474
- "license": "Apache-2.0",
475
- "dependencies": {
476
- "@eslint/core": "^0.17.0"
477
- },
478
- "engines": {
479
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
480
- }
481
- },
482
- "node_modules/@eslint/core": {
483
- "version": "0.17.0",
484
- "dev": true,
485
- "license": "Apache-2.0",
486
- "dependencies": {
487
- "@types/json-schema": "^7.0.15"
488
- },
489
- "engines": {
490
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
491
- }
492
- },
493
- "node_modules/@eslint/eslintrc": {
494
- "version": "3.3.1",
495
- "dev": true,
496
- "license": "MIT",
497
- "dependencies": {
498
- "ajv": "^6.12.4",
499
- "debug": "^4.3.2",
500
- "espree": "^10.0.1",
501
- "globals": "^14.0.0",
502
- "ignore": "^5.2.0",
503
- "import-fresh": "^3.2.1",
504
- "js-yaml": "^4.1.0",
505
- "minimatch": "^3.1.2",
506
- "strip-json-comments": "^3.1.1"
507
- },
508
- "engines": {
509
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
510
- },
511
- "funding": {
512
- "url": "https://opencollective.com/eslint"
513
- }
514
- },
515
- "node_modules/@eslint/eslintrc/node_modules/globals": {
516
- "version": "14.0.0",
517
- "dev": true,
518
- "license": "MIT",
519
- "engines": {
520
- "node": ">=18"
521
- },
522
- "funding": {
523
- "url": "https://github.com/sponsors/sindresorhus"
524
- }
525
- },
526
- "node_modules/@eslint/js": {
527
- "version": "9.39.1",
528
- "dev": true,
529
- "license": "MIT",
530
- "engines": {
531
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
532
- },
533
- "funding": {
534
- "url": "https://eslint.org/donate"
535
- }
536
- },
537
- "node_modules/@eslint/object-schema": {
538
- "version": "2.1.7",
539
- "dev": true,
540
- "license": "Apache-2.0",
541
- "engines": {
542
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
543
- }
544
- },
545
- "node_modules/@eslint/plugin-kit": {
546
- "version": "0.4.1",
547
- "dev": true,
548
- "license": "Apache-2.0",
549
- "dependencies": {
550
- "@eslint/core": "^0.17.0",
551
- "levn": "^0.4.1"
552
- },
553
- "engines": {
554
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
555
- }
556
- },
557
- "node_modules/@hookform/resolvers": {
558
- "version": "3.10.0",
559
- "license": "MIT",
560
- "peerDependencies": {
561
- "react-hook-form": "^7.0.0"
562
- }
563
- },
564
- "node_modules/@humanfs/core": {
565
- "version": "0.19.1",
566
- "dev": true,
567
- "license": "Apache-2.0",
568
- "engines": {
569
- "node": ">=18.18.0"
570
- }
571
- },
572
- "node_modules/@humanfs/node": {
573
- "version": "0.16.7",
574
- "dev": true,
575
- "license": "Apache-2.0",
576
- "dependencies": {
577
- "@humanfs/core": "^0.19.1",
578
- "@humanwhocodes/retry": "^0.4.0"
579
- },
580
- "engines": {
581
- "node": ">=18.18.0"
582
- }
583
- },
584
- "node_modules/@humanwhocodes/module-importer": {
585
- "version": "1.0.1",
586
- "dev": true,
587
- "license": "Apache-2.0",
588
- "engines": {
589
- "node": ">=12.22"
590
- },
591
- "funding": {
592
- "type": "github",
593
- "url": "https://github.com/sponsors/nzakas"
594
- }
595
- },
596
- "node_modules/@humanwhocodes/retry": {
597
- "version": "0.4.3",
598
- "dev": true,
599
- "license": "Apache-2.0",
600
- "engines": {
601
- "node": ">=18.18"
602
- },
603
- "funding": {
604
- "type": "github",
605
- "url": "https://github.com/sponsors/nzakas"
606
- }
607
- },
608
- "node_modules/@jridgewell/gen-mapping": {
609
- "version": "0.3.13",
610
- "license": "MIT",
611
- "dependencies": {
612
- "@jridgewell/sourcemap-codec": "^1.5.0",
613
- "@jridgewell/trace-mapping": "^0.3.24"
614
- }
615
- },
616
- "node_modules/@jridgewell/remapping": {
617
- "version": "2.3.5",
618
- "dev": true,
619
- "license": "MIT",
620
- "dependencies": {
621
- "@jridgewell/gen-mapping": "^0.3.5",
622
- "@jridgewell/trace-mapping": "^0.3.24"
623
- }
624
- },
625
- "node_modules/@jridgewell/resolve-uri": {
626
- "version": "3.1.2",
627
- "license": "MIT",
628
- "engines": {
629
- "node": ">=6.0.0"
630
- }
631
- },
632
- "node_modules/@jridgewell/sourcemap-codec": {
633
- "version": "1.5.5",
634
- "license": "MIT"
635
- },
636
- "node_modules/@jridgewell/trace-mapping": {
637
- "version": "0.3.31",
638
- "license": "MIT",
639
- "dependencies": {
640
- "@jridgewell/resolve-uri": "^3.1.0",
641
- "@jridgewell/sourcemap-codec": "^1.4.14"
642
- }
643
- },
644
- "node_modules/@mui/core-downloads-tracker": {
645
- "version": "6.5.0",
646
- "license": "MIT",
647
- "funding": {
648
- "type": "opencollective",
649
- "url": "https://opencollective.com/mui-org"
650
- }
651
- },
652
- "node_modules/@mui/icons-material": {
653
- "version": "6.5.0",
654
- "license": "MIT",
655
- "dependencies": {
656
- "@babel/runtime": "^7.26.0"
657
- },
658
- "engines": {
659
- "node": ">=14.0.0"
660
- },
661
- "funding": {
662
- "type": "opencollective",
663
- "url": "https://opencollective.com/mui-org"
664
- },
665
- "peerDependencies": {
666
- "@mui/material": "^6.5.0",
667
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
668
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
669
- },
670
- "peerDependenciesMeta": {
671
- "@types/react": {
672
- "optional": true
673
- }
674
- }
675
- },
676
- "node_modules/@mui/material": {
677
- "version": "6.5.0",
678
- "license": "MIT",
679
- "dependencies": {
680
- "@babel/runtime": "^7.26.0",
681
- "@mui/core-downloads-tracker": "^6.5.0",
682
- "@mui/system": "^6.5.0",
683
- "@mui/types": "~7.2.24",
684
- "@mui/utils": "^6.4.9",
685
- "@popperjs/core": "^2.11.8",
686
- "@types/react-transition-group": "^4.4.12",
687
- "clsx": "^2.1.1",
688
- "csstype": "^3.1.3",
689
- "prop-types": "^15.8.1",
690
- "react-is": "^19.0.0",
691
- "react-transition-group": "^4.4.5"
692
- },
693
- "engines": {
694
- "node": ">=14.0.0"
695
- },
696
- "funding": {
697
- "type": "opencollective",
698
- "url": "https://opencollective.com/mui-org"
699
- },
700
- "peerDependencies": {
701
- "@emotion/react": "^11.5.0",
702
- "@emotion/styled": "^11.3.0",
703
- "@mui/material-pigment-css": "^6.5.0",
704
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
705
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
706
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
707
- },
708
- "peerDependenciesMeta": {
709
- "@emotion/react": {
710
- "optional": true
711
- },
712
- "@emotion/styled": {
713
- "optional": true
714
- },
715
- "@mui/material-pigment-css": {
716
- "optional": true
717
- },
718
- "@types/react": {
719
- "optional": true
720
- }
721
- }
722
- },
723
- "node_modules/@mui/private-theming": {
724
- "version": "6.4.9",
725
- "license": "MIT",
726
- "dependencies": {
727
- "@babel/runtime": "^7.26.0",
728
- "@mui/utils": "^6.4.9",
729
- "prop-types": "^15.8.1"
730
- },
731
- "engines": {
732
- "node": ">=14.0.0"
733
- },
734
- "funding": {
735
- "type": "opencollective",
736
- "url": "https://opencollective.com/mui-org"
737
- },
738
- "peerDependencies": {
739
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
740
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
741
- },
742
- "peerDependenciesMeta": {
743
- "@types/react": {
744
- "optional": true
745
- }
746
- }
747
- },
748
- "node_modules/@mui/styled-engine": {
749
- "version": "6.5.0",
750
- "license": "MIT",
751
- "dependencies": {
752
- "@babel/runtime": "^7.26.0",
753
- "@emotion/cache": "^11.13.5",
754
- "@emotion/serialize": "^1.3.3",
755
- "@emotion/sheet": "^1.4.0",
756
- "csstype": "^3.1.3",
757
- "prop-types": "^15.8.1"
758
- },
759
- "engines": {
760
- "node": ">=14.0.0"
761
- },
762
- "funding": {
763
- "type": "opencollective",
764
- "url": "https://opencollective.com/mui-org"
765
- },
766
- "peerDependencies": {
767
- "@emotion/react": "^11.4.1",
768
- "@emotion/styled": "^11.3.0",
769
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
770
- },
771
- "peerDependenciesMeta": {
772
- "@emotion/react": {
773
- "optional": true
774
- },
775
- "@emotion/styled": {
776
- "optional": true
777
- }
778
- }
779
- },
780
- "node_modules/@mui/system": {
781
- "version": "6.5.0",
782
- "license": "MIT",
783
- "dependencies": {
784
- "@babel/runtime": "^7.26.0",
785
- "@mui/private-theming": "^6.4.9",
786
- "@mui/styled-engine": "^6.5.0",
787
- "@mui/types": "~7.2.24",
788
- "@mui/utils": "^6.4.9",
789
- "clsx": "^2.1.1",
790
- "csstype": "^3.1.3",
791
- "prop-types": "^15.8.1"
792
- },
793
- "engines": {
794
- "node": ">=14.0.0"
795
- },
796
- "funding": {
797
- "type": "opencollective",
798
- "url": "https://opencollective.com/mui-org"
799
- },
800
- "peerDependencies": {
801
- "@emotion/react": "^11.5.0",
802
- "@emotion/styled": "^11.3.0",
803
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
804
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
805
- },
806
- "peerDependenciesMeta": {
807
- "@emotion/react": {
808
- "optional": true
809
- },
810
- "@emotion/styled": {
811
- "optional": true
812
- },
813
- "@types/react": {
814
- "optional": true
815
- }
816
- }
817
- },
818
- "node_modules/@mui/types": {
819
- "version": "7.2.24",
820
- "license": "MIT",
821
- "peerDependencies": {
822
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
823
- },
824
- "peerDependenciesMeta": {
825
- "@types/react": {
826
- "optional": true
827
- }
828
- }
829
- },
830
- "node_modules/@mui/utils": {
831
- "version": "6.4.9",
832
- "license": "MIT",
833
- "dependencies": {
834
- "@babel/runtime": "^7.26.0",
835
- "@mui/types": "~7.2.24",
836
- "@types/prop-types": "^15.7.14",
837
- "clsx": "^2.1.1",
838
- "prop-types": "^15.8.1",
839
- "react-is": "^19.0.0"
840
- },
841
- "engines": {
842
- "node": ">=14.0.0"
843
- },
844
- "funding": {
845
- "type": "opencollective",
846
- "url": "https://opencollective.com/mui-org"
847
- },
848
- "peerDependencies": {
849
- "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
850
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
851
- },
852
- "peerDependenciesMeta": {
853
- "@types/react": {
854
- "optional": true
855
- }
856
- }
857
- },
858
- "node_modules/@mui/x-date-pickers": {
859
- "version": "7.29.4",
860
- "license": "MIT",
861
- "dependencies": {
862
- "@babel/runtime": "^7.25.7",
863
- "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0",
864
- "@mui/x-internals": "7.29.0",
865
- "@types/react-transition-group": "^4.4.11",
866
- "clsx": "^2.1.1",
867
- "prop-types": "^15.8.1",
868
- "react-transition-group": "^4.4.5"
869
- },
870
- "engines": {
871
- "node": ">=14.0.0"
872
- },
873
- "funding": {
874
- "type": "opencollective",
875
- "url": "https://opencollective.com/mui-org"
876
- },
877
- "peerDependencies": {
878
- "@emotion/react": "^11.9.0",
879
- "@emotion/styled": "^11.8.1",
880
- "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
881
- "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
882
- "date-fns": "^2.25.0 || ^3.2.0 || ^4.0.0",
883
- "date-fns-jalali": "^2.13.0-0 || ^3.2.0-0 || ^4.0.0-0",
884
- "dayjs": "^1.10.7",
885
- "luxon": "^3.0.2",
886
- "moment": "^2.29.4",
887
- "moment-hijri": "^2.1.2 || ^3.0.0",
888
- "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0",
889
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
890
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
891
- },
892
- "peerDependenciesMeta": {
893
- "@emotion/react": {
894
- "optional": true
895
- },
896
- "@emotion/styled": {
897
- "optional": true
898
- },
899
- "date-fns": {
900
- "optional": true
901
- },
902
- "date-fns-jalali": {
903
- "optional": true
904
- },
905
- "dayjs": {
906
- "optional": true
907
- },
908
- "luxon": {
909
- "optional": true
910
- },
911
- "moment": {
912
- "optional": true
913
- },
914
- "moment-hijri": {
915
- "optional": true
916
- },
917
- "moment-jalaali": {
918
- "optional": true
919
- }
920
- }
921
- },
922
- "node_modules/@mui/x-internals": {
923
- "version": "7.29.0",
924
- "license": "MIT",
925
- "dependencies": {
926
- "@babel/runtime": "^7.25.7",
927
- "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0"
928
- },
929
- "engines": {
930
- "node": ">=14.0.0"
931
- },
932
- "funding": {
933
- "type": "opencollective",
934
- "url": "https://opencollective.com/mui-org"
935
- },
936
- "peerDependencies": {
937
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0"
938
- }
939
- },
940
- "node_modules/@nodelib/fs.scandir": {
941
- "version": "2.1.5",
942
- "dev": true,
943
- "license": "MIT",
944
- "dependencies": {
945
- "@nodelib/fs.stat": "2.0.5",
946
- "run-parallel": "^1.1.9"
947
- },
948
- "engines": {
949
- "node": ">= 8"
950
- }
951
- },
952
- "node_modules/@nodelib/fs.stat": {
953
- "version": "2.0.5",
954
- "dev": true,
955
- "license": "MIT",
956
- "engines": {
957
- "node": ">= 8"
958
- }
959
- },
960
- "node_modules/@nodelib/fs.walk": {
961
- "version": "1.2.8",
962
- "dev": true,
963
- "license": "MIT",
964
- "dependencies": {
965
- "@nodelib/fs.scandir": "2.1.5",
966
- "fastq": "^1.6.0"
967
- },
968
- "engines": {
969
- "node": ">= 8"
970
- }
971
- },
972
- "node_modules/@popperjs/core": {
973
- "version": "2.11.8",
974
- "license": "MIT",
975
- "funding": {
976
- "type": "opencollective",
977
- "url": "https://opencollective.com/popperjs"
978
- }
979
- },
980
- "node_modules/@remix-run/router": {
981
- "version": "1.23.1",
982
- "license": "MIT",
983
- "engines": {
984
- "node": ">=14.0.0"
985
- }
986
- },
987
- "node_modules/@rolldown/pluginutils": {
988
- "version": "1.0.0-beta.47",
989
- "dev": true,
990
- "license": "MIT"
991
- },
992
- "node_modules/@rollup/rollup-linux-x64-gnu": {
993
- "version": "4.53.2",
994
- "cpu": [
995
- "x64"
996
- ],
997
- "dev": true,
998
- "license": "MIT",
999
- "optional": true,
1000
- "os": [
1001
- "linux"
1002
- ]
1003
- },
1004
- "node_modules/@types/babel__core": {
1005
- "version": "7.20.5",
1006
- "dev": true,
1007
- "license": "MIT",
1008
- "dependencies": {
1009
- "@babel/parser": "^7.20.7",
1010
- "@babel/types": "^7.20.7",
1011
- "@types/babel__generator": "*",
1012
- "@types/babel__template": "*",
1013
- "@types/babel__traverse": "*"
1014
- }
1015
- },
1016
- "node_modules/@types/babel__generator": {
1017
- "version": "7.27.0",
1018
- "dev": true,
1019
- "license": "MIT",
1020
- "dependencies": {
1021
- "@babel/types": "^7.0.0"
1022
- }
1023
- },
1024
- "node_modules/@types/babel__template": {
1025
- "version": "7.4.4",
1026
- "dev": true,
1027
- "license": "MIT",
1028
- "dependencies": {
1029
- "@babel/parser": "^7.1.0",
1030
- "@babel/types": "^7.0.0"
1031
- }
1032
- },
1033
- "node_modules/@types/babel__traverse": {
1034
- "version": "7.28.0",
1035
- "dev": true,
1036
- "license": "MIT",
1037
- "dependencies": {
1038
- "@babel/types": "^7.28.2"
1039
- }
1040
- },
1041
- "node_modules/@types/estree": {
1042
- "version": "1.0.8",
1043
- "dev": true,
1044
- "license": "MIT"
1045
- },
1046
- "node_modules/@types/json-schema": {
1047
- "version": "7.0.15",
1048
- "dev": true,
1049
- "license": "MIT"
1050
- },
1051
- "node_modules/@types/node": {
1052
- "version": "24.10.1",
1053
- "dev": true,
1054
- "license": "MIT",
1055
- "dependencies": {
1056
- "undici-types": "~7.16.0"
1057
- }
1058
- },
1059
- "node_modules/@types/parse-json": {
1060
- "version": "4.0.2",
1061
- "license": "MIT"
1062
- },
1063
- "node_modules/@types/prop-types": {
1064
- "version": "15.7.15",
1065
- "license": "MIT"
1066
- },
1067
- "node_modules/@types/react": {
1068
- "version": "19.2.4",
1069
- "dev": true,
1070
- "license": "MIT",
1071
- "dependencies": {
1072
- "csstype": "^3.0.2"
1073
- }
1074
- },
1075
- "node_modules/@types/react-dom": {
1076
- "version": "19.2.3",
1077
- "dev": true,
1078
- "license": "MIT",
1079
- "peerDependencies": {
1080
- "@types/react": "^19.2.0"
1081
- }
1082
- },
1083
- "node_modules/@types/react-transition-group": {
1084
- "version": "4.4.12",
1085
- "license": "MIT",
1086
- "peerDependencies": {
1087
- "@types/react": "*"
1088
- }
1089
- },
1090
- "node_modules/@typescript-eslint/eslint-plugin": {
1091
- "version": "8.46.4",
1092
- "dev": true,
1093
- "license": "MIT",
1094
- "dependencies": {
1095
- "@eslint-community/regexpp": "^4.10.0",
1096
- "@typescript-eslint/scope-manager": "8.46.4",
1097
- "@typescript-eslint/type-utils": "8.46.4",
1098
- "@typescript-eslint/utils": "8.46.4",
1099
- "@typescript-eslint/visitor-keys": "8.46.4",
1100
- "graphemer": "^1.4.0",
1101
- "ignore": "^7.0.0",
1102
- "natural-compare": "^1.4.0",
1103
- "ts-api-utils": "^2.1.0"
1104
- },
1105
- "engines": {
1106
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1107
- },
1108
- "funding": {
1109
- "type": "opencollective",
1110
- "url": "https://opencollective.com/typescript-eslint"
1111
- },
1112
- "peerDependencies": {
1113
- "@typescript-eslint/parser": "^8.46.4",
1114
- "eslint": "^8.57.0 || ^9.0.0",
1115
- "typescript": ">=4.8.4 <6.0.0"
1116
- }
1117
- },
1118
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
1119
- "version": "7.0.5",
1120
- "dev": true,
1121
- "license": "MIT",
1122
- "engines": {
1123
- "node": ">= 4"
1124
- }
1125
- },
1126
- "node_modules/@typescript-eslint/parser": {
1127
- "version": "8.46.4",
1128
- "dev": true,
1129
- "license": "MIT",
1130
- "dependencies": {
1131
- "@typescript-eslint/scope-manager": "8.46.4",
1132
- "@typescript-eslint/types": "8.46.4",
1133
- "@typescript-eslint/typescript-estree": "8.46.4",
1134
- "@typescript-eslint/visitor-keys": "8.46.4",
1135
- "debug": "^4.3.4"
1136
- },
1137
- "engines": {
1138
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1139
- },
1140
- "funding": {
1141
- "type": "opencollective",
1142
- "url": "https://opencollective.com/typescript-eslint"
1143
- },
1144
- "peerDependencies": {
1145
- "eslint": "^8.57.0 || ^9.0.0",
1146
- "typescript": ">=4.8.4 <6.0.0"
1147
- }
1148
- },
1149
- "node_modules/@typescript-eslint/project-service": {
1150
- "version": "8.46.4",
1151
- "dev": true,
1152
- "license": "MIT",
1153
- "dependencies": {
1154
- "@typescript-eslint/tsconfig-utils": "^8.46.4",
1155
- "@typescript-eslint/types": "^8.46.4",
1156
- "debug": "^4.3.4"
1157
- },
1158
- "engines": {
1159
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1160
- },
1161
- "funding": {
1162
- "type": "opencollective",
1163
- "url": "https://opencollective.com/typescript-eslint"
1164
- },
1165
- "peerDependencies": {
1166
- "typescript": ">=4.8.4 <6.0.0"
1167
- }
1168
- },
1169
- "node_modules/@typescript-eslint/scope-manager": {
1170
- "version": "8.46.4",
1171
- "dev": true,
1172
- "license": "MIT",
1173
- "dependencies": {
1174
- "@typescript-eslint/types": "8.46.4",
1175
- "@typescript-eslint/visitor-keys": "8.46.4"
1176
- },
1177
- "engines": {
1178
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1179
- },
1180
- "funding": {
1181
- "type": "opencollective",
1182
- "url": "https://opencollective.com/typescript-eslint"
1183
- }
1184
- },
1185
- "node_modules/@typescript-eslint/tsconfig-utils": {
1186
- "version": "8.46.4",
1187
- "dev": true,
1188
- "license": "MIT",
1189
- "engines": {
1190
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1191
- },
1192
- "funding": {
1193
- "type": "opencollective",
1194
- "url": "https://opencollective.com/typescript-eslint"
1195
- },
1196
- "peerDependencies": {
1197
- "typescript": ">=4.8.4 <6.0.0"
1198
- }
1199
- },
1200
- "node_modules/@typescript-eslint/type-utils": {
1201
- "version": "8.46.4",
1202
- "dev": true,
1203
- "license": "MIT",
1204
- "dependencies": {
1205
- "@typescript-eslint/types": "8.46.4",
1206
- "@typescript-eslint/typescript-estree": "8.46.4",
1207
- "@typescript-eslint/utils": "8.46.4",
1208
- "debug": "^4.3.4",
1209
- "ts-api-utils": "^2.1.0"
1210
- },
1211
- "engines": {
1212
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1213
- },
1214
- "funding": {
1215
- "type": "opencollective",
1216
- "url": "https://opencollective.com/typescript-eslint"
1217
- },
1218
- "peerDependencies": {
1219
- "eslint": "^8.57.0 || ^9.0.0",
1220
- "typescript": ">=4.8.4 <6.0.0"
1221
- }
1222
- },
1223
- "node_modules/@typescript-eslint/types": {
1224
- "version": "8.46.4",
1225
- "dev": true,
1226
- "license": "MIT",
1227
- "engines": {
1228
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1229
- },
1230
- "funding": {
1231
- "type": "opencollective",
1232
- "url": "https://opencollective.com/typescript-eslint"
1233
- }
1234
- },
1235
- "node_modules/@typescript-eslint/typescript-estree": {
1236
- "version": "8.46.4",
1237
- "dev": true,
1238
- "license": "MIT",
1239
- "dependencies": {
1240
- "@typescript-eslint/project-service": "8.46.4",
1241
- "@typescript-eslint/tsconfig-utils": "8.46.4",
1242
- "@typescript-eslint/types": "8.46.4",
1243
- "@typescript-eslint/visitor-keys": "8.46.4",
1244
- "debug": "^4.3.4",
1245
- "fast-glob": "^3.3.2",
1246
- "is-glob": "^4.0.3",
1247
- "minimatch": "^9.0.4",
1248
- "semver": "^7.6.0",
1249
- "ts-api-utils": "^2.1.0"
1250
- },
1251
- "engines": {
1252
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1253
- },
1254
- "funding": {
1255
- "type": "opencollective",
1256
- "url": "https://opencollective.com/typescript-eslint"
1257
- },
1258
- "peerDependencies": {
1259
- "typescript": ">=4.8.4 <6.0.0"
1260
- }
1261
- },
1262
- "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
1263
- "version": "2.0.2",
1264
- "dev": true,
1265
- "license": "MIT",
1266
- "dependencies": {
1267
- "balanced-match": "^1.0.0"
1268
- }
1269
- },
1270
- "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
1271
- "version": "9.0.5",
1272
- "dev": true,
1273
- "license": "ISC",
1274
- "dependencies": {
1275
- "brace-expansion": "^2.0.1"
1276
- },
1277
- "engines": {
1278
- "node": ">=16 || 14 >=14.17"
1279
- },
1280
- "funding": {
1281
- "url": "https://github.com/sponsors/isaacs"
1282
- }
1283
- },
1284
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
1285
- "version": "7.7.3",
1286
- "dev": true,
1287
- "license": "ISC",
1288
- "bin": {
1289
- "semver": "bin/semver.js"
1290
- },
1291
- "engines": {
1292
- "node": ">=10"
1293
- }
1294
- },
1295
- "node_modules/@typescript-eslint/utils": {
1296
- "version": "8.46.4",
1297
- "dev": true,
1298
- "license": "MIT",
1299
- "dependencies": {
1300
- "@eslint-community/eslint-utils": "^4.7.0",
1301
- "@typescript-eslint/scope-manager": "8.46.4",
1302
- "@typescript-eslint/types": "8.46.4",
1303
- "@typescript-eslint/typescript-estree": "8.46.4"
1304
- },
1305
- "engines": {
1306
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1307
- },
1308
- "funding": {
1309
- "type": "opencollective",
1310
- "url": "https://opencollective.com/typescript-eslint"
1311
- },
1312
- "peerDependencies": {
1313
- "eslint": "^8.57.0 || ^9.0.0",
1314
- "typescript": ">=4.8.4 <6.0.0"
1315
- }
1316
- },
1317
- "node_modules/@typescript-eslint/visitor-keys": {
1318
- "version": "8.46.4",
1319
- "dev": true,
1320
- "license": "MIT",
1321
- "dependencies": {
1322
- "@typescript-eslint/types": "8.46.4",
1323
- "eslint-visitor-keys": "^4.2.1"
1324
- },
1325
- "engines": {
1326
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1327
- },
1328
- "funding": {
1329
- "type": "opencollective",
1330
- "url": "https://opencollective.com/typescript-eslint"
1331
- }
1332
- },
1333
- "node_modules/@vitejs/plugin-react": {
1334
- "version": "5.1.1",
1335
- "dev": true,
1336
- "license": "MIT",
1337
- "dependencies": {
1338
- "@babel/core": "^7.28.5",
1339
- "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1340
- "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1341
- "@rolldown/pluginutils": "1.0.0-beta.47",
1342
- "@types/babel__core": "^7.20.5",
1343
- "react-refresh": "^0.18.0"
1344
- },
1345
- "engines": {
1346
- "node": "^20.19.0 || >=22.12.0"
1347
- },
1348
- "peerDependencies": {
1349
- "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1350
- }
1351
- },
1352
- "node_modules/acorn": {
1353
- "version": "8.15.0",
1354
- "dev": true,
1355
- "license": "MIT",
1356
- "bin": {
1357
- "acorn": "bin/acorn"
1358
- },
1359
- "engines": {
1360
- "node": ">=0.4.0"
1361
- }
1362
- },
1363
- "node_modules/acorn-jsx": {
1364
- "version": "5.3.2",
1365
- "dev": true,
1366
- "license": "MIT",
1367
- "peerDependencies": {
1368
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
1369
- }
1370
- },
1371
- "node_modules/ajv": {
1372
- "version": "6.12.6",
1373
- "dev": true,
1374
- "license": "MIT",
1375
- "dependencies": {
1376
- "fast-deep-equal": "^3.1.1",
1377
- "fast-json-stable-stringify": "^2.0.0",
1378
- "json-schema-traverse": "^0.4.1",
1379
- "uri-js": "^4.2.2"
1380
- },
1381
- "funding": {
1382
- "type": "github",
1383
- "url": "https://github.com/sponsors/epoberezkin"
1384
- }
1385
- },
1386
- "node_modules/ansi-styles": {
1387
- "version": "4.3.0",
1388
- "dev": true,
1389
- "license": "MIT",
1390
- "dependencies": {
1391
- "color-convert": "^2.0.1"
1392
- },
1393
- "engines": {
1394
- "node": ">=8"
1395
- },
1396
- "funding": {
1397
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1398
- }
1399
- },
1400
- "node_modules/argparse": {
1401
- "version": "2.0.1",
1402
- "dev": true,
1403
- "license": "Python-2.0"
1404
- },
1405
- "node_modules/asynckit": {
1406
- "version": "0.4.0",
1407
- "license": "MIT"
1408
- },
1409
- "node_modules/axios": {
1410
- "version": "1.13.2",
1411
- "license": "MIT",
1412
- "dependencies": {
1413
- "follow-redirects": "^1.15.6",
1414
- "form-data": "^4.0.4",
1415
- "proxy-from-env": "^1.1.0"
1416
- }
1417
- },
1418
- "node_modules/babel-plugin-macros": {
1419
- "version": "3.1.0",
1420
- "license": "MIT",
1421
- "dependencies": {
1422
- "@babel/runtime": "^7.12.5",
1423
- "cosmiconfig": "^7.0.0",
1424
- "resolve": "^1.19.0"
1425
- },
1426
- "engines": {
1427
- "node": ">=10",
1428
- "npm": ">=6"
1429
- }
1430
- },
1431
- "node_modules/balanced-match": {
1432
- "version": "1.0.2",
1433
- "dev": true,
1434
- "license": "MIT"
1435
- },
1436
- "node_modules/baseline-browser-mapping": {
1437
- "version": "2.8.28",
1438
- "dev": true,
1439
- "license": "Apache-2.0",
1440
- "bin": {
1441
- "baseline-browser-mapping": "dist/cli.js"
1442
- }
1443
- },
1444
- "node_modules/brace-expansion": {
1445
- "version": "1.1.12",
1446
- "dev": true,
1447
- "license": "MIT",
1448
- "dependencies": {
1449
- "balanced-match": "^1.0.0",
1450
- "concat-map": "0.0.1"
1451
- }
1452
- },
1453
- "node_modules/braces": {
1454
- "version": "3.0.3",
1455
- "dev": true,
1456
- "license": "MIT",
1457
- "dependencies": {
1458
- "fill-range": "^7.1.1"
1459
- },
1460
- "engines": {
1461
- "node": ">=8"
1462
- }
1463
- },
1464
- "node_modules/browserslist": {
1465
- "version": "4.28.0",
1466
- "dev": true,
1467
- "funding": [
1468
- {
1469
- "type": "opencollective",
1470
- "url": "https://opencollective.com/browserslist"
1471
- },
1472
- {
1473
- "type": "tidelift",
1474
- "url": "https://tidelift.com/funding/github/npm/browserslist"
1475
- },
1476
- {
1477
- "type": "github",
1478
- "url": "https://github.com/sponsors/ai"
1479
- }
1480
- ],
1481
- "license": "MIT",
1482
- "dependencies": {
1483
- "baseline-browser-mapping": "^2.8.25",
1484
- "caniuse-lite": "^1.0.30001754",
1485
- "electron-to-chromium": "^1.5.249",
1486
- "node-releases": "^2.0.27",
1487
- "update-browserslist-db": "^1.1.4"
1488
- },
1489
- "bin": {
1490
- "browserslist": "cli.js"
1491
- },
1492
- "engines": {
1493
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1494
- }
1495
- },
1496
- "node_modules/call-bind-apply-helpers": {
1497
- "version": "1.0.2",
1498
- "license": "MIT",
1499
- "dependencies": {
1500
- "es-errors": "^1.3.0",
1501
- "function-bind": "^1.1.2"
1502
- },
1503
- "engines": {
1504
- "node": ">= 0.4"
1505
- }
1506
- },
1507
- "node_modules/callsites": {
1508
- "version": "3.1.0",
1509
- "license": "MIT",
1510
- "engines": {
1511
- "node": ">=6"
1512
- }
1513
- },
1514
- "node_modules/caniuse-lite": {
1515
- "version": "1.0.30001754",
1516
- "dev": true,
1517
- "funding": [
1518
- {
1519
- "type": "opencollective",
1520
- "url": "https://opencollective.com/browserslist"
1521
- },
1522
- {
1523
- "type": "tidelift",
1524
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1525
- },
1526
- {
1527
- "type": "github",
1528
- "url": "https://github.com/sponsors/ai"
1529
- }
1530
- ],
1531
- "license": "CC-BY-4.0"
1532
- },
1533
- "node_modules/chalk": {
1534
- "version": "4.1.2",
1535
- "dev": true,
1536
- "license": "MIT",
1537
- "dependencies": {
1538
- "ansi-styles": "^4.1.0",
1539
- "supports-color": "^7.1.0"
1540
- },
1541
- "engines": {
1542
- "node": ">=10"
1543
- },
1544
- "funding": {
1545
- "url": "https://github.com/chalk/chalk?sponsor=1"
1546
- }
1547
- },
1548
- "node_modules/clsx": {
1549
- "version": "2.1.1",
1550
- "license": "MIT",
1551
- "engines": {
1552
- "node": ">=6"
1553
- }
1554
- },
1555
- "node_modules/color-convert": {
1556
- "version": "2.0.1",
1557
- "dev": true,
1558
- "license": "MIT",
1559
- "dependencies": {
1560
- "color-name": "~1.1.4"
1561
- },
1562
- "engines": {
1563
- "node": ">=7.0.0"
1564
- }
1565
- },
1566
- "node_modules/color-name": {
1567
- "version": "1.1.4",
1568
- "dev": true,
1569
- "license": "MIT"
1570
- },
1571
- "node_modules/combined-stream": {
1572
- "version": "1.0.8",
1573
- "license": "MIT",
1574
- "dependencies": {
1575
- "delayed-stream": "~1.0.0"
1576
- },
1577
- "engines": {
1578
- "node": ">= 0.8"
1579
- }
1580
- },
1581
- "node_modules/concat-map": {
1582
- "version": "0.0.1",
1583
- "dev": true,
1584
- "license": "MIT"
1585
- },
1586
- "node_modules/convert-source-map": {
1587
- "version": "1.9.0",
1588
- "license": "MIT"
1589
- },
1590
- "node_modules/cosmiconfig": {
1591
- "version": "7.1.0",
1592
- "license": "MIT",
1593
- "dependencies": {
1594
- "@types/parse-json": "^4.0.0",
1595
- "import-fresh": "^3.2.1",
1596
- "parse-json": "^5.0.0",
1597
- "path-type": "^4.0.0",
1598
- "yaml": "^1.10.0"
1599
- },
1600
- "engines": {
1601
- "node": ">=10"
1602
- }
1603
- },
1604
- "node_modules/cross-spawn": {
1605
- "version": "7.0.6",
1606
- "dev": true,
1607
- "license": "MIT",
1608
- "dependencies": {
1609
- "path-key": "^3.1.0",
1610
- "shebang-command": "^2.0.0",
1611
- "which": "^2.0.1"
1612
- },
1613
- "engines": {
1614
- "node": ">= 8"
1615
- }
1616
- },
1617
- "node_modules/csstype": {
1618
- "version": "3.1.3",
1619
- "license": "MIT"
1620
- },
1621
- "node_modules/dayjs": {
1622
- "version": "1.11.19",
1623
- "license": "MIT"
1624
- },
1625
- "node_modules/debug": {
1626
- "version": "4.4.3",
1627
- "license": "MIT",
1628
- "dependencies": {
1629
- "ms": "^2.1.3"
1630
- },
1631
- "engines": {
1632
- "node": ">=6.0"
1633
- },
1634
- "peerDependenciesMeta": {
1635
- "supports-color": {
1636
- "optional": true
1637
- }
1638
- }
1639
- },
1640
- "node_modules/deep-is": {
1641
- "version": "0.1.4",
1642
- "dev": true,
1643
- "license": "MIT"
1644
- },
1645
- "node_modules/delayed-stream": {
1646
- "version": "1.0.0",
1647
- "license": "MIT",
1648
- "engines": {
1649
- "node": ">=0.4.0"
1650
- }
1651
- },
1652
- "node_modules/dom-helpers": {
1653
- "version": "5.2.1",
1654
- "license": "MIT",
1655
- "dependencies": {
1656
- "@babel/runtime": "^7.8.7",
1657
- "csstype": "^3.0.2"
1658
- }
1659
- },
1660
- "node_modules/dunder-proto": {
1661
- "version": "1.0.1",
1662
- "license": "MIT",
1663
- "dependencies": {
1664
- "call-bind-apply-helpers": "^1.0.1",
1665
- "es-errors": "^1.3.0",
1666
- "gopd": "^1.2.0"
1667
- },
1668
- "engines": {
1669
- "node": ">= 0.4"
1670
- }
1671
- },
1672
- "node_modules/electron-to-chromium": {
1673
- "version": "1.5.250",
1674
- "dev": true,
1675
- "license": "ISC"
1676
- },
1677
- "node_modules/error-ex": {
1678
- "version": "1.3.4",
1679
- "license": "MIT",
1680
- "dependencies": {
1681
- "is-arrayish": "^0.2.1"
1682
- }
1683
- },
1684
- "node_modules/es-define-property": {
1685
- "version": "1.0.1",
1686
- "license": "MIT",
1687
- "engines": {
1688
- "node": ">= 0.4"
1689
- }
1690
- },
1691
- "node_modules/es-errors": {
1692
- "version": "1.3.0",
1693
- "license": "MIT",
1694
- "engines": {
1695
- "node": ">= 0.4"
1696
- }
1697
- },
1698
- "node_modules/es-object-atoms": {
1699
- "version": "1.1.1",
1700
- "license": "MIT",
1701
- "dependencies": {
1702
- "es-errors": "^1.3.0"
1703
- },
1704
- "engines": {
1705
- "node": ">= 0.4"
1706
- }
1707
- },
1708
- "node_modules/es-set-tostringtag": {
1709
- "version": "2.1.0",
1710
- "license": "MIT",
1711
- "dependencies": {
1712
- "es-errors": "^1.3.0",
1713
- "get-intrinsic": "^1.2.6",
1714
- "has-tostringtag": "^1.0.2",
1715
- "hasown": "^2.0.2"
1716
- },
1717
- "engines": {
1718
- "node": ">= 0.4"
1719
- }
1720
- },
1721
- "node_modules/esbuild": {
1722
- "version": "0.25.12",
1723
- "dev": true,
1724
- "hasInstallScript": true,
1725
- "license": "MIT",
1726
- "bin": {
1727
- "esbuild": "bin/esbuild"
1728
- },
1729
- "engines": {
1730
- "node": ">=18"
1731
- },
1732
- "optionalDependencies": {
1733
- "@esbuild/aix-ppc64": "0.25.12",
1734
- "@esbuild/android-arm": "0.25.12",
1735
- "@esbuild/android-arm64": "0.25.12",
1736
- "@esbuild/android-x64": "0.25.12",
1737
- "@esbuild/darwin-arm64": "0.25.12",
1738
- "@esbuild/darwin-x64": "0.25.12",
1739
- "@esbuild/freebsd-arm64": "0.25.12",
1740
- "@esbuild/freebsd-x64": "0.25.12",
1741
- "@esbuild/linux-arm": "0.25.12",
1742
- "@esbuild/linux-arm64": "0.25.12",
1743
- "@esbuild/linux-ia32": "0.25.12",
1744
- "@esbuild/linux-loong64": "0.25.12",
1745
- "@esbuild/linux-mips64el": "0.25.12",
1746
- "@esbuild/linux-ppc64": "0.25.12",
1747
- "@esbuild/linux-riscv64": "0.25.12",
1748
- "@esbuild/linux-s390x": "0.25.12",
1749
- "@esbuild/linux-x64": "0.25.12",
1750
- "@esbuild/netbsd-arm64": "0.25.12",
1751
- "@esbuild/netbsd-x64": "0.25.12",
1752
- "@esbuild/openbsd-arm64": "0.25.12",
1753
- "@esbuild/openbsd-x64": "0.25.12",
1754
- "@esbuild/openharmony-arm64": "0.25.12",
1755
- "@esbuild/sunos-x64": "0.25.12",
1756
- "@esbuild/win32-arm64": "0.25.12",
1757
- "@esbuild/win32-ia32": "0.25.12",
1758
- "@esbuild/win32-x64": "0.25.12"
1759
- }
1760
- },
1761
- "node_modules/escalade": {
1762
- "version": "3.2.0",
1763
- "dev": true,
1764
- "license": "MIT",
1765
- "engines": {
1766
- "node": ">=6"
1767
- }
1768
- },
1769
- "node_modules/escape-string-regexp": {
1770
- "version": "4.0.0",
1771
- "license": "MIT",
1772
- "engines": {
1773
- "node": ">=10"
1774
- },
1775
- "funding": {
1776
- "url": "https://github.com/sponsors/sindresorhus"
1777
- }
1778
- },
1779
- "node_modules/eslint": {
1780
- "version": "9.39.1",
1781
- "dev": true,
1782
- "license": "MIT",
1783
- "dependencies": {
1784
- "@eslint-community/eslint-utils": "^4.8.0",
1785
- "@eslint-community/regexpp": "^4.12.1",
1786
- "@eslint/config-array": "^0.21.1",
1787
- "@eslint/config-helpers": "^0.4.2",
1788
- "@eslint/core": "^0.17.0",
1789
- "@eslint/eslintrc": "^3.3.1",
1790
- "@eslint/js": "9.39.1",
1791
- "@eslint/plugin-kit": "^0.4.1",
1792
- "@humanfs/node": "^0.16.6",
1793
- "@humanwhocodes/module-importer": "^1.0.1",
1794
- "@humanwhocodes/retry": "^0.4.2",
1795
- "@types/estree": "^1.0.6",
1796
- "ajv": "^6.12.4",
1797
- "chalk": "^4.0.0",
1798
- "cross-spawn": "^7.0.6",
1799
- "debug": "^4.3.2",
1800
- "escape-string-regexp": "^4.0.0",
1801
- "eslint-scope": "^8.4.0",
1802
- "eslint-visitor-keys": "^4.2.1",
1803
- "espree": "^10.4.0",
1804
- "esquery": "^1.5.0",
1805
- "esutils": "^2.0.2",
1806
- "fast-deep-equal": "^3.1.3",
1807
- "file-entry-cache": "^8.0.0",
1808
- "find-up": "^5.0.0",
1809
- "glob-parent": "^6.0.2",
1810
- "ignore": "^5.2.0",
1811
- "imurmurhash": "^0.1.4",
1812
- "is-glob": "^4.0.0",
1813
- "json-stable-stringify-without-jsonify": "^1.0.1",
1814
- "lodash.merge": "^4.6.2",
1815
- "minimatch": "^3.1.2",
1816
- "natural-compare": "^1.4.0",
1817
- "optionator": "^0.9.3"
1818
- },
1819
- "bin": {
1820
- "eslint": "bin/eslint.js"
1821
- },
1822
- "engines": {
1823
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1824
- },
1825
- "funding": {
1826
- "url": "https://eslint.org/donate"
1827
- },
1828
- "peerDependencies": {
1829
- "jiti": "*"
1830
- },
1831
- "peerDependenciesMeta": {
1832
- "jiti": {
1833
- "optional": true
1834
- }
1835
- }
1836
- },
1837
- "node_modules/eslint-plugin-react-hooks": {
1838
- "version": "5.2.0",
1839
- "dev": true,
1840
- "license": "MIT",
1841
- "engines": {
1842
- "node": ">=10"
1843
- },
1844
- "peerDependencies": {
1845
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
1846
- }
1847
- },
1848
- "node_modules/eslint-plugin-react-refresh": {
1849
- "version": "0.4.24",
1850
- "dev": true,
1851
- "license": "MIT",
1852
- "peerDependencies": {
1853
- "eslint": ">=8.40"
1854
- }
1855
- },
1856
- "node_modules/eslint-scope": {
1857
- "version": "8.4.0",
1858
- "dev": true,
1859
- "license": "BSD-2-Clause",
1860
- "dependencies": {
1861
- "esrecurse": "^4.3.0",
1862
- "estraverse": "^5.2.0"
1863
- },
1864
- "engines": {
1865
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1866
- },
1867
- "funding": {
1868
- "url": "https://opencollective.com/eslint"
1869
- }
1870
- },
1871
- "node_modules/eslint-visitor-keys": {
1872
- "version": "4.2.1",
1873
- "dev": true,
1874
- "license": "Apache-2.0",
1875
- "engines": {
1876
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1877
- },
1878
- "funding": {
1879
- "url": "https://opencollective.com/eslint"
1880
- }
1881
- },
1882
- "node_modules/espree": {
1883
- "version": "10.4.0",
1884
- "dev": true,
1885
- "license": "BSD-2-Clause",
1886
- "dependencies": {
1887
- "acorn": "^8.15.0",
1888
- "acorn-jsx": "^5.3.2",
1889
- "eslint-visitor-keys": "^4.2.1"
1890
- },
1891
- "engines": {
1892
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
1893
- },
1894
- "funding": {
1895
- "url": "https://opencollective.com/eslint"
1896
- }
1897
- },
1898
- "node_modules/esquery": {
1899
- "version": "1.6.0",
1900
- "dev": true,
1901
- "license": "BSD-3-Clause",
1902
- "dependencies": {
1903
- "estraverse": "^5.1.0"
1904
- },
1905
- "engines": {
1906
- "node": ">=0.10"
1907
- }
1908
- },
1909
- "node_modules/esrecurse": {
1910
- "version": "4.3.0",
1911
- "dev": true,
1912
- "license": "BSD-2-Clause",
1913
- "dependencies": {
1914
- "estraverse": "^5.2.0"
1915
- },
1916
- "engines": {
1917
- "node": ">=4.0"
1918
- }
1919
- },
1920
- "node_modules/estraverse": {
1921
- "version": "5.3.0",
1922
- "dev": true,
1923
- "license": "BSD-2-Clause",
1924
- "engines": {
1925
- "node": ">=4.0"
1926
- }
1927
- },
1928
- "node_modules/esutils": {
1929
- "version": "2.0.3",
1930
- "dev": true,
1931
- "license": "BSD-2-Clause",
1932
- "engines": {
1933
- "node": ">=0.10.0"
1934
- }
1935
- },
1936
- "node_modules/fast-deep-equal": {
1937
- "version": "3.1.3",
1938
- "dev": true,
1939
- "license": "MIT"
1940
- },
1941
- "node_modules/fast-glob": {
1942
- "version": "3.3.3",
1943
- "dev": true,
1944
- "license": "MIT",
1945
- "dependencies": {
1946
- "@nodelib/fs.stat": "^2.0.2",
1947
- "@nodelib/fs.walk": "^1.2.3",
1948
- "glob-parent": "^5.1.2",
1949
- "merge2": "^1.3.0",
1950
- "micromatch": "^4.0.8"
1951
- },
1952
- "engines": {
1953
- "node": ">=8.6.0"
1954
- }
1955
- },
1956
- "node_modules/fast-glob/node_modules/glob-parent": {
1957
- "version": "5.1.2",
1958
- "dev": true,
1959
- "license": "ISC",
1960
- "dependencies": {
1961
- "is-glob": "^4.0.1"
1962
- },
1963
- "engines": {
1964
- "node": ">= 6"
1965
- }
1966
- },
1967
- "node_modules/fast-json-stable-stringify": {
1968
- "version": "2.1.0",
1969
- "dev": true,
1970
- "license": "MIT"
1971
- },
1972
- "node_modules/fast-levenshtein": {
1973
- "version": "2.0.6",
1974
- "dev": true,
1975
- "license": "MIT"
1976
- },
1977
- "node_modules/fastq": {
1978
- "version": "1.19.1",
1979
- "dev": true,
1980
- "license": "ISC",
1981
- "dependencies": {
1982
- "reusify": "^1.0.4"
1983
- }
1984
- },
1985
- "node_modules/fdir": {
1986
- "version": "6.5.0",
1987
- "dev": true,
1988
- "license": "MIT",
1989
- "engines": {
1990
- "node": ">=12.0.0"
1991
- },
1992
- "peerDependencies": {
1993
- "picomatch": "^3 || ^4"
1994
- },
1995
- "peerDependenciesMeta": {
1996
- "picomatch": {
1997
- "optional": true
1998
- }
1999
- }
2000
- },
2001
- "node_modules/file-entry-cache": {
2002
- "version": "8.0.0",
2003
- "dev": true,
2004
- "license": "MIT",
2005
- "dependencies": {
2006
- "flat-cache": "^4.0.0"
2007
- },
2008
- "engines": {
2009
- "node": ">=16.0.0"
2010
- }
2011
- },
2012
- "node_modules/fill-range": {
2013
- "version": "7.1.1",
2014
- "dev": true,
2015
- "license": "MIT",
2016
- "dependencies": {
2017
- "to-regex-range": "^5.0.1"
2018
- },
2019
- "engines": {
2020
- "node": ">=8"
2021
- }
2022
- },
2023
- "node_modules/find-root": {
2024
- "version": "1.1.0",
2025
- "license": "MIT"
2026
- },
2027
- "node_modules/find-up": {
2028
- "version": "5.0.0",
2029
- "dev": true,
2030
- "license": "MIT",
2031
- "dependencies": {
2032
- "locate-path": "^6.0.0",
2033
- "path-exists": "^4.0.0"
2034
- },
2035
- "engines": {
2036
- "node": ">=10"
2037
- },
2038
- "funding": {
2039
- "url": "https://github.com/sponsors/sindresorhus"
2040
- }
2041
- },
2042
- "node_modules/flat-cache": {
2043
- "version": "4.0.1",
2044
- "dev": true,
2045
- "license": "MIT",
2046
- "dependencies": {
2047
- "flatted": "^3.2.9",
2048
- "keyv": "^4.5.4"
2049
- },
2050
- "engines": {
2051
- "node": ">=16"
2052
- }
2053
- },
2054
- "node_modules/flatted": {
2055
- "version": "3.3.3",
2056
- "dev": true,
2057
- "license": "ISC"
2058
- },
2059
- "node_modules/follow-redirects": {
2060
- "version": "1.15.11",
2061
- "funding": [
2062
- {
2063
- "type": "individual",
2064
- "url": "https://github.com/sponsors/RubenVerborgh"
2065
- }
2066
- ],
2067
- "license": "MIT",
2068
- "engines": {
2069
- "node": ">=4.0"
2070
- },
2071
- "peerDependenciesMeta": {
2072
- "debug": {
2073
- "optional": true
2074
- }
2075
- }
2076
- },
2077
- "node_modules/form-data": {
2078
- "version": "4.0.4",
2079
- "license": "MIT",
2080
- "dependencies": {
2081
- "asynckit": "^0.4.0",
2082
- "combined-stream": "^1.0.8",
2083
- "es-set-tostringtag": "^2.1.0",
2084
- "hasown": "^2.0.2",
2085
- "mime-types": "^2.1.12"
2086
- },
2087
- "engines": {
2088
- "node": ">= 6"
2089
- }
2090
- },
2091
- "node_modules/function-bind": {
2092
- "version": "1.1.2",
2093
- "license": "MIT",
2094
- "funding": {
2095
- "url": "https://github.com/sponsors/ljharb"
2096
- }
2097
- },
2098
- "node_modules/gensync": {
2099
- "version": "1.0.0-beta.2",
2100
- "dev": true,
2101
- "license": "MIT",
2102
- "engines": {
2103
- "node": ">=6.9.0"
2104
- }
2105
- },
2106
- "node_modules/get-intrinsic": {
2107
- "version": "1.3.0",
2108
- "license": "MIT",
2109
- "dependencies": {
2110
- "call-bind-apply-helpers": "^1.0.2",
2111
- "es-define-property": "^1.0.1",
2112
- "es-errors": "^1.3.0",
2113
- "es-object-atoms": "^1.1.1",
2114
- "function-bind": "^1.1.2",
2115
- "get-proto": "^1.0.1",
2116
- "gopd": "^1.2.0",
2117
- "has-symbols": "^1.1.0",
2118
- "hasown": "^2.0.2",
2119
- "math-intrinsics": "^1.1.0"
2120
- },
2121
- "engines": {
2122
- "node": ">= 0.4"
2123
- },
2124
- "funding": {
2125
- "url": "https://github.com/sponsors/ljharb"
2126
- }
2127
- },
2128
- "node_modules/get-proto": {
2129
- "version": "1.0.1",
2130
- "license": "MIT",
2131
- "dependencies": {
2132
- "dunder-proto": "^1.0.1",
2133
- "es-object-atoms": "^1.0.0"
2134
- },
2135
- "engines": {
2136
- "node": ">= 0.4"
2137
- }
2138
- },
2139
- "node_modules/glob-parent": {
2140
- "version": "6.0.2",
2141
- "dev": true,
2142
- "license": "ISC",
2143
- "dependencies": {
2144
- "is-glob": "^4.0.3"
2145
- },
2146
- "engines": {
2147
- "node": ">=10.13.0"
2148
- }
2149
- },
2150
- "node_modules/globals": {
2151
- "version": "16.5.0",
2152
- "dev": true,
2153
- "license": "MIT",
2154
- "engines": {
2155
- "node": ">=18"
2156
- },
2157
- "funding": {
2158
- "url": "https://github.com/sponsors/sindresorhus"
2159
- }
2160
- },
2161
- "node_modules/goober": {
2162
- "version": "2.1.18",
2163
- "license": "MIT",
2164
- "peerDependencies": {
2165
- "csstype": "^3.0.10"
2166
- }
2167
- },
2168
- "node_modules/gopd": {
2169
- "version": "1.2.0",
2170
- "license": "MIT",
2171
- "engines": {
2172
- "node": ">= 0.4"
2173
- },
2174
- "funding": {
2175
- "url": "https://github.com/sponsors/ljharb"
2176
- }
2177
- },
2178
- "node_modules/graphemer": {
2179
- "version": "1.4.0",
2180
- "dev": true,
2181
- "license": "MIT"
2182
- },
2183
- "node_modules/has-flag": {
2184
- "version": "4.0.0",
2185
- "dev": true,
2186
- "license": "MIT",
2187
- "engines": {
2188
- "node": ">=8"
2189
- }
2190
- },
2191
- "node_modules/has-symbols": {
2192
- "version": "1.1.0",
2193
- "license": "MIT",
2194
- "engines": {
2195
- "node": ">= 0.4"
2196
- },
2197
- "funding": {
2198
- "url": "https://github.com/sponsors/ljharb"
2199
- }
2200
- },
2201
- "node_modules/has-tostringtag": {
2202
- "version": "1.0.2",
2203
- "license": "MIT",
2204
- "dependencies": {
2205
- "has-symbols": "^1.0.3"
2206
- },
2207
- "engines": {
2208
- "node": ">= 0.4"
2209
- },
2210
- "funding": {
2211
- "url": "https://github.com/sponsors/ljharb"
2212
- }
2213
- },
2214
- "node_modules/hasown": {
2215
- "version": "2.0.2",
2216
- "license": "MIT",
2217
- "dependencies": {
2218
- "function-bind": "^1.1.2"
2219
- },
2220
- "engines": {
2221
- "node": ">= 0.4"
2222
- }
2223
- },
2224
- "node_modules/hoist-non-react-statics": {
2225
- "version": "3.3.2",
2226
- "license": "BSD-3-Clause",
2227
- "dependencies": {
2228
- "react-is": "^16.7.0"
2229
- }
2230
- },
2231
- "node_modules/hoist-non-react-statics/node_modules/react-is": {
2232
- "version": "16.13.1",
2233
- "license": "MIT"
2234
- },
2235
- "node_modules/ignore": {
2236
- "version": "5.3.2",
2237
- "dev": true,
2238
- "license": "MIT",
2239
- "engines": {
2240
- "node": ">= 4"
2241
- }
2242
- },
2243
- "node_modules/import-fresh": {
2244
- "version": "3.3.1",
2245
- "license": "MIT",
2246
- "dependencies": {
2247
- "parent-module": "^1.0.0",
2248
- "resolve-from": "^4.0.0"
2249
- },
2250
- "engines": {
2251
- "node": ">=6"
2252
- },
2253
- "funding": {
2254
- "url": "https://github.com/sponsors/sindresorhus"
2255
- }
2256
- },
2257
- "node_modules/imurmurhash": {
2258
- "version": "0.1.4",
2259
- "dev": true,
2260
- "license": "MIT",
2261
- "engines": {
2262
- "node": ">=0.8.19"
2263
- }
2264
- },
2265
- "node_modules/is-arrayish": {
2266
- "version": "0.2.1",
2267
- "license": "MIT"
2268
- },
2269
- "node_modules/is-core-module": {
2270
- "version": "2.16.1",
2271
- "license": "MIT",
2272
- "dependencies": {
2273
- "hasown": "^2.0.2"
2274
- },
2275
- "engines": {
2276
- "node": ">= 0.4"
2277
- },
2278
- "funding": {
2279
- "url": "https://github.com/sponsors/ljharb"
2280
- }
2281
- },
2282
- "node_modules/is-extglob": {
2283
- "version": "2.1.1",
2284
- "dev": true,
2285
- "license": "MIT",
2286
- "engines": {
2287
- "node": ">=0.10.0"
2288
- }
2289
- },
2290
- "node_modules/is-glob": {
2291
- "version": "4.0.3",
2292
- "dev": true,
2293
- "license": "MIT",
2294
- "dependencies": {
2295
- "is-extglob": "^2.1.1"
2296
- },
2297
- "engines": {
2298
- "node": ">=0.10.0"
2299
- }
2300
- },
2301
- "node_modules/is-number": {
2302
- "version": "7.0.0",
2303
- "dev": true,
2304
- "license": "MIT",
2305
- "engines": {
2306
- "node": ">=0.12.0"
2307
- }
2308
- },
2309
- "node_modules/isexe": {
2310
- "version": "2.0.0",
2311
- "dev": true,
2312
- "license": "ISC"
2313
- },
2314
- "node_modules/js-tokens": {
2315
- "version": "4.0.0",
2316
- "license": "MIT"
2317
- },
2318
- "node_modules/js-yaml": {
2319
- "version": "4.1.1",
2320
- "dev": true,
2321
- "license": "MIT",
2322
- "dependencies": {
2323
- "argparse": "^2.0.1"
2324
- },
2325
- "bin": {
2326
- "js-yaml": "bin/js-yaml.js"
2327
- }
2328
- },
2329
- "node_modules/jsesc": {
2330
- "version": "3.1.0",
2331
- "license": "MIT",
2332
- "bin": {
2333
- "jsesc": "bin/jsesc"
2334
- },
2335
- "engines": {
2336
- "node": ">=6"
2337
- }
2338
- },
2339
- "node_modules/json-buffer": {
2340
- "version": "3.0.1",
2341
- "dev": true,
2342
- "license": "MIT"
2343
- },
2344
- "node_modules/json-parse-even-better-errors": {
2345
- "version": "2.3.1",
2346
- "license": "MIT"
2347
- },
2348
- "node_modules/json-schema-traverse": {
2349
- "version": "0.4.1",
2350
- "dev": true,
2351
- "license": "MIT"
2352
- },
2353
- "node_modules/json-stable-stringify-without-jsonify": {
2354
- "version": "1.0.1",
2355
- "dev": true,
2356
- "license": "MIT"
2357
- },
2358
- "node_modules/json5": {
2359
- "version": "2.2.3",
2360
- "dev": true,
2361
- "license": "MIT",
2362
- "bin": {
2363
- "json5": "lib/cli.js"
2364
- },
2365
- "engines": {
2366
- "node": ">=6"
2367
- }
2368
- },
2369
- "node_modules/keyv": {
2370
- "version": "4.5.4",
2371
- "dev": true,
2372
- "license": "MIT",
2373
- "dependencies": {
2374
- "json-buffer": "3.0.1"
2375
- }
2376
- },
2377
- "node_modules/levn": {
2378
- "version": "0.4.1",
2379
- "dev": true,
2380
- "license": "MIT",
2381
- "dependencies": {
2382
- "prelude-ls": "^1.2.1",
2383
- "type-check": "~0.4.0"
2384
- },
2385
- "engines": {
2386
- "node": ">= 0.8.0"
2387
- }
2388
- },
2389
- "node_modules/lines-and-columns": {
2390
- "version": "1.2.4",
2391
- "license": "MIT"
2392
- },
2393
- "node_modules/locate-path": {
2394
- "version": "6.0.0",
2395
- "dev": true,
2396
- "license": "MIT",
2397
- "dependencies": {
2398
- "p-locate": "^5.0.0"
2399
- },
2400
- "engines": {
2401
- "node": ">=10"
2402
- },
2403
- "funding": {
2404
- "url": "https://github.com/sponsors/sindresorhus"
2405
- }
2406
- },
2407
- "node_modules/lodash.merge": {
2408
- "version": "4.6.2",
2409
- "dev": true,
2410
- "license": "MIT"
2411
- },
2412
- "node_modules/loose-envify": {
2413
- "version": "1.4.0",
2414
- "license": "MIT",
2415
- "dependencies": {
2416
- "js-tokens": "^3.0.0 || ^4.0.0"
2417
- },
2418
- "bin": {
2419
- "loose-envify": "cli.js"
2420
- }
2421
- },
2422
- "node_modules/lru-cache": {
2423
- "version": "5.1.1",
2424
- "dev": true,
2425
- "license": "ISC",
2426
- "dependencies": {
2427
- "yallist": "^3.0.2"
2428
- }
2429
- },
2430
- "node_modules/math-intrinsics": {
2431
- "version": "1.1.0",
2432
- "license": "MIT",
2433
- "engines": {
2434
- "node": ">= 0.4"
2435
- }
2436
- },
2437
- "node_modules/merge2": {
2438
- "version": "1.4.1",
2439
- "dev": true,
2440
- "license": "MIT",
2441
- "engines": {
2442
- "node": ">= 8"
2443
- }
2444
- },
2445
- "node_modules/micromatch": {
2446
- "version": "4.0.8",
2447
- "dev": true,
2448
- "license": "MIT",
2449
- "dependencies": {
2450
- "braces": "^3.0.3",
2451
- "picomatch": "^2.3.1"
2452
- },
2453
- "engines": {
2454
- "node": ">=8.6"
2455
- }
2456
- },
2457
- "node_modules/mime-db": {
2458
- "version": "1.52.0",
2459
- "license": "MIT",
2460
- "engines": {
2461
- "node": ">= 0.6"
2462
- }
2463
- },
2464
- "node_modules/mime-types": {
2465
- "version": "2.1.35",
2466
- "license": "MIT",
2467
- "dependencies": {
2468
- "mime-db": "1.52.0"
2469
- },
2470
- "engines": {
2471
- "node": ">= 0.6"
2472
- }
2473
- },
2474
- "node_modules/minimatch": {
2475
- "version": "3.1.2",
2476
- "dev": true,
2477
- "license": "ISC",
2478
- "dependencies": {
2479
- "brace-expansion": "^1.1.7"
2480
- },
2481
- "engines": {
2482
- "node": "*"
2483
- }
2484
- },
2485
- "node_modules/ms": {
2486
- "version": "2.1.3",
2487
- "license": "MIT"
2488
- },
2489
- "node_modules/nanoid": {
2490
- "version": "3.3.11",
2491
- "dev": true,
2492
- "funding": [
2493
- {
2494
- "type": "github",
2495
- "url": "https://github.com/sponsors/ai"
2496
- }
2497
- ],
2498
- "license": "MIT",
2499
- "bin": {
2500
- "nanoid": "bin/nanoid.cjs"
2501
- },
2502
- "engines": {
2503
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
2504
- }
2505
- },
2506
- "node_modules/natural-compare": {
2507
- "version": "1.4.0",
2508
- "dev": true,
2509
- "license": "MIT"
2510
- },
2511
- "node_modules/node-releases": {
2512
- "version": "2.0.27",
2513
- "dev": true,
2514
- "license": "MIT"
2515
- },
2516
- "node_modules/object-assign": {
2517
- "version": "4.1.1",
2518
- "license": "MIT",
2519
- "engines": {
2520
- "node": ">=0.10.0"
2521
- }
2522
- },
2523
- "node_modules/optionator": {
2524
- "version": "0.9.4",
2525
- "dev": true,
2526
- "license": "MIT",
2527
- "dependencies": {
2528
- "deep-is": "^0.1.3",
2529
- "fast-levenshtein": "^2.0.6",
2530
- "levn": "^0.4.1",
2531
- "prelude-ls": "^1.2.1",
2532
- "type-check": "^0.4.0",
2533
- "word-wrap": "^1.2.5"
2534
- },
2535
- "engines": {
2536
- "node": ">= 0.8.0"
2537
- }
2538
- },
2539
- "node_modules/p-limit": {
2540
- "version": "3.1.0",
2541
- "dev": true,
2542
- "license": "MIT",
2543
- "dependencies": {
2544
- "yocto-queue": "^0.1.0"
2545
- },
2546
- "engines": {
2547
- "node": ">=10"
2548
- },
2549
- "funding": {
2550
- "url": "https://github.com/sponsors/sindresorhus"
2551
- }
2552
- },
2553
- "node_modules/p-locate": {
2554
- "version": "5.0.0",
2555
- "dev": true,
2556
- "license": "MIT",
2557
- "dependencies": {
2558
- "p-limit": "^3.0.2"
2559
- },
2560
- "engines": {
2561
- "node": ">=10"
2562
- },
2563
- "funding": {
2564
- "url": "https://github.com/sponsors/sindresorhus"
2565
- }
2566
- },
2567
- "node_modules/parent-module": {
2568
- "version": "1.0.1",
2569
- "license": "MIT",
2570
- "dependencies": {
2571
- "callsites": "^3.0.0"
2572
- },
2573
- "engines": {
2574
- "node": ">=6"
2575
- }
2576
- },
2577
- "node_modules/parse-json": {
2578
- "version": "5.2.0",
2579
- "license": "MIT",
2580
- "dependencies": {
2581
- "@babel/code-frame": "^7.0.0",
2582
- "error-ex": "^1.3.1",
2583
- "json-parse-even-better-errors": "^2.3.0",
2584
- "lines-and-columns": "^1.1.6"
2585
- },
2586
- "engines": {
2587
- "node": ">=8"
2588
- },
2589
- "funding": {
2590
- "url": "https://github.com/sponsors/sindresorhus"
2591
- }
2592
- },
2593
- "node_modules/path-exists": {
2594
- "version": "4.0.0",
2595
- "dev": true,
2596
- "license": "MIT",
2597
- "engines": {
2598
- "node": ">=8"
2599
- }
2600
- },
2601
- "node_modules/path-key": {
2602
- "version": "3.1.1",
2603
- "dev": true,
2604
- "license": "MIT",
2605
- "engines": {
2606
- "node": ">=8"
2607
- }
2608
- },
2609
- "node_modules/path-parse": {
2610
- "version": "1.0.7",
2611
- "license": "MIT"
2612
- },
2613
- "node_modules/path-type": {
2614
- "version": "4.0.0",
2615
- "license": "MIT",
2616
- "engines": {
2617
- "node": ">=8"
2618
- }
2619
- },
2620
- "node_modules/picocolors": {
2621
- "version": "1.1.1",
2622
- "license": "ISC"
2623
- },
2624
- "node_modules/picomatch": {
2625
- "version": "2.3.1",
2626
- "dev": true,
2627
- "license": "MIT",
2628
- "engines": {
2629
- "node": ">=8.6"
2630
- },
2631
- "funding": {
2632
- "url": "https://github.com/sponsors/jonschlinkert"
2633
- }
2634
- },
2635
- "node_modules/postcss": {
2636
- "version": "8.5.6",
2637
- "dev": true,
2638
- "funding": [
2639
- {
2640
- "type": "opencollective",
2641
- "url": "https://opencollective.com/postcss/"
2642
- },
2643
- {
2644
- "type": "tidelift",
2645
- "url": "https://tidelift.com/funding/github/npm/postcss"
2646
- },
2647
- {
2648
- "type": "github",
2649
- "url": "https://github.com/sponsors/ai"
2650
- }
2651
- ],
2652
- "license": "MIT",
2653
- "dependencies": {
2654
- "nanoid": "^3.3.11",
2655
- "picocolors": "^1.1.1",
2656
- "source-map-js": "^1.2.1"
2657
- },
2658
- "engines": {
2659
- "node": "^10 || ^12 || >=14"
2660
- }
2661
- },
2662
- "node_modules/prelude-ls": {
2663
- "version": "1.2.1",
2664
- "dev": true,
2665
- "license": "MIT",
2666
- "engines": {
2667
- "node": ">= 0.8.0"
2668
- }
2669
- },
2670
- "node_modules/prop-types": {
2671
- "version": "15.8.1",
2672
- "license": "MIT",
2673
- "dependencies": {
2674
- "loose-envify": "^1.4.0",
2675
- "object-assign": "^4.1.1",
2676
- "react-is": "^16.13.1"
2677
- }
2678
- },
2679
- "node_modules/prop-types/node_modules/react-is": {
2680
- "version": "16.13.1",
2681
- "license": "MIT"
2682
- },
2683
- "node_modules/property-expr": {
2684
- "version": "2.0.6",
2685
- "license": "MIT"
2686
- },
2687
- "node_modules/proxy-from-env": {
2688
- "version": "1.1.0",
2689
- "license": "MIT"
2690
- },
2691
- "node_modules/punycode": {
2692
- "version": "2.3.1",
2693
- "dev": true,
2694
- "license": "MIT",
2695
- "engines": {
2696
- "node": ">=6"
2697
- }
2698
- },
2699
- "node_modules/queue-microtask": {
2700
- "version": "1.2.3",
2701
- "dev": true,
2702
- "funding": [
2703
- {
2704
- "type": "github",
2705
- "url": "https://github.com/sponsors/feross"
2706
- },
2707
- {
2708
- "type": "patreon",
2709
- "url": "https://www.patreon.com/feross"
2710
- },
2711
- {
2712
- "type": "consulting",
2713
- "url": "https://feross.org/support"
2714
- }
2715
- ],
2716
- "license": "MIT"
2717
- },
2718
- "node_modules/react": {
2719
- "version": "19.2.0",
2720
- "license": "MIT",
2721
- "engines": {
2722
- "node": ">=0.10.0"
2723
- }
2724
- },
2725
- "node_modules/react-dom": {
2726
- "version": "19.2.0",
2727
- "license": "MIT",
2728
- "dependencies": {
2729
- "scheduler": "^0.27.0"
2730
- },
2731
- "peerDependencies": {
2732
- "react": "^19.2.0"
2733
- }
2734
- },
2735
- "node_modules/react-hook-form": {
2736
- "version": "7.66.0",
2737
- "license": "MIT",
2738
- "engines": {
2739
- "node": ">=18.0.0"
2740
- },
2741
- "funding": {
2742
- "type": "opencollective",
2743
- "url": "https://opencollective.com/react-hook-form"
2744
- },
2745
- "peerDependencies": {
2746
- "react": "^16.8.0 || ^17 || ^18 || ^19"
2747
- }
2748
- },
2749
- "node_modules/react-hot-toast": {
2750
- "version": "2.6.0",
2751
- "license": "MIT",
2752
- "dependencies": {
2753
- "csstype": "^3.1.3",
2754
- "goober": "^2.1.16"
2755
- },
2756
- "engines": {
2757
- "node": ">=10"
2758
- },
2759
- "peerDependencies": {
2760
- "react": ">=16",
2761
- "react-dom": ">=16"
2762
- }
2763
- },
2764
- "node_modules/react-is": {
2765
- "version": "19.2.0",
2766
- "license": "MIT"
2767
- },
2768
- "node_modules/react-refresh": {
2769
- "version": "0.18.0",
2770
- "dev": true,
2771
- "license": "MIT",
2772
- "engines": {
2773
- "node": ">=0.10.0"
2774
- }
2775
- },
2776
- "node_modules/react-router": {
2777
- "version": "6.30.2",
2778
- "license": "MIT",
2779
- "dependencies": {
2780
- "@remix-run/router": "1.23.1"
2781
- },
2782
- "engines": {
2783
- "node": ">=14.0.0"
2784
- },
2785
- "peerDependencies": {
2786
- "react": ">=16.8"
2787
- }
2788
- },
2789
- "node_modules/react-router-dom": {
2790
- "version": "6.30.2",
2791
- "license": "MIT",
2792
- "dependencies": {
2793
- "@remix-run/router": "1.23.1",
2794
- "react-router": "6.30.2"
2795
- },
2796
- "engines": {
2797
- "node": ">=14.0.0"
2798
- },
2799
- "peerDependencies": {
2800
- "react": ">=16.8",
2801
- "react-dom": ">=16.8"
2802
- }
2803
- },
2804
- "node_modules/react-transition-group": {
2805
- "version": "4.4.5",
2806
- "license": "BSD-3-Clause",
2807
- "dependencies": {
2808
- "@babel/runtime": "^7.5.5",
2809
- "dom-helpers": "^5.0.1",
2810
- "loose-envify": "^1.4.0",
2811
- "prop-types": "^15.6.2"
2812
- },
2813
- "peerDependencies": {
2814
- "react": ">=16.6.0",
2815
- "react-dom": ">=16.6.0"
2816
- }
2817
- },
2818
- "node_modules/resolve": {
2819
- "version": "1.22.11",
2820
- "license": "MIT",
2821
- "dependencies": {
2822
- "is-core-module": "^2.16.1",
2823
- "path-parse": "^1.0.7",
2824
- "supports-preserve-symlinks-flag": "^1.0.0"
2825
- },
2826
- "bin": {
2827
- "resolve": "bin/resolve"
2828
- },
2829
- "engines": {
2830
- "node": ">= 0.4"
2831
- },
2832
- "funding": {
2833
- "url": "https://github.com/sponsors/ljharb"
2834
- }
2835
- },
2836
- "node_modules/resolve-from": {
2837
- "version": "4.0.0",
2838
- "license": "MIT",
2839
- "engines": {
2840
- "node": ">=4"
2841
- }
2842
- },
2843
- "node_modules/reusify": {
2844
- "version": "1.1.0",
2845
- "dev": true,
2846
- "license": "MIT",
2847
- "engines": {
2848
- "iojs": ">=1.0.0",
2849
- "node": ">=0.10.0"
2850
- }
2851
- },
2852
- "node_modules/rollup": {
2853
- "version": "4.53.2",
2854
- "dev": true,
2855
- "license": "MIT",
2856
- "dependencies": {
2857
- "@types/estree": "1.0.8"
2858
- },
2859
- "bin": {
2860
- "rollup": "dist/bin/rollup"
2861
- },
2862
- "engines": {
2863
- "node": ">=18.0.0",
2864
- "npm": ">=8.0.0"
2865
- },
2866
- "optionalDependencies": {
2867
- "@rollup/rollup-android-arm-eabi": "4.53.2",
2868
- "@rollup/rollup-android-arm64": "4.53.2",
2869
- "@rollup/rollup-darwin-arm64": "4.53.2",
2870
- "@rollup/rollup-darwin-x64": "4.53.2",
2871
- "@rollup/rollup-freebsd-arm64": "4.53.2",
2872
- "@rollup/rollup-freebsd-x64": "4.53.2",
2873
- "@rollup/rollup-linux-arm-gnueabihf": "4.53.2",
2874
- "@rollup/rollup-linux-arm-musleabihf": "4.53.2",
2875
- "@rollup/rollup-linux-arm64-gnu": "4.53.2",
2876
- "@rollup/rollup-linux-arm64-musl": "4.53.2",
2877
- "@rollup/rollup-linux-loong64-gnu": "4.53.2",
2878
- "@rollup/rollup-linux-ppc64-gnu": "4.53.2",
2879
- "@rollup/rollup-linux-riscv64-gnu": "4.53.2",
2880
- "@rollup/rollup-linux-riscv64-musl": "4.53.2",
2881
- "@rollup/rollup-linux-s390x-gnu": "4.53.2",
2882
- "@rollup/rollup-linux-x64-gnu": "4.53.2",
2883
- "@rollup/rollup-linux-x64-musl": "4.53.2",
2884
- "@rollup/rollup-openharmony-arm64": "4.53.2",
2885
- "@rollup/rollup-win32-arm64-msvc": "4.53.2",
2886
- "@rollup/rollup-win32-ia32-msvc": "4.53.2",
2887
- "@rollup/rollup-win32-x64-gnu": "4.53.2",
2888
- "@rollup/rollup-win32-x64-msvc": "4.53.2",
2889
- "fsevents": "~2.3.2"
2890
- }
2891
- },
2892
- "node_modules/run-parallel": {
2893
- "version": "1.2.0",
2894
- "dev": true,
2895
- "funding": [
2896
- {
2897
- "type": "github",
2898
- "url": "https://github.com/sponsors/feross"
2899
- },
2900
- {
2901
- "type": "patreon",
2902
- "url": "https://www.patreon.com/feross"
2903
- },
2904
- {
2905
- "type": "consulting",
2906
- "url": "https://feross.org/support"
2907
- }
2908
- ],
2909
- "license": "MIT",
2910
- "dependencies": {
2911
- "queue-microtask": "^1.2.2"
2912
- }
2913
- },
2914
- "node_modules/scheduler": {
2915
- "version": "0.27.0",
2916
- "license": "MIT"
2917
- },
2918
- "node_modules/semver": {
2919
- "version": "6.3.1",
2920
- "dev": true,
2921
- "license": "ISC",
2922
- "bin": {
2923
- "semver": "bin/semver.js"
2924
- }
2925
- },
2926
- "node_modules/shebang-command": {
2927
- "version": "2.0.0",
2928
- "dev": true,
2929
- "license": "MIT",
2930
- "dependencies": {
2931
- "shebang-regex": "^3.0.0"
2932
- },
2933
- "engines": {
2934
- "node": ">=8"
2935
- }
2936
- },
2937
- "node_modules/shebang-regex": {
2938
- "version": "3.0.0",
2939
- "dev": true,
2940
- "license": "MIT",
2941
- "engines": {
2942
- "node": ">=8"
2943
- }
2944
- },
2945
- "node_modules/source-map": {
2946
- "version": "0.5.7",
2947
- "license": "BSD-3-Clause",
2948
- "engines": {
2949
- "node": ">=0.10.0"
2950
- }
2951
- },
2952
- "node_modules/source-map-js": {
2953
- "version": "1.2.1",
2954
- "dev": true,
2955
- "license": "BSD-3-Clause",
2956
- "engines": {
2957
- "node": ">=0.10.0"
2958
- }
2959
- },
2960
- "node_modules/strip-json-comments": {
2961
- "version": "3.1.1",
2962
- "dev": true,
2963
- "license": "MIT",
2964
- "engines": {
2965
- "node": ">=8"
2966
- },
2967
- "funding": {
2968
- "url": "https://github.com/sponsors/sindresorhus"
2969
- }
2970
- },
2971
- "node_modules/stylis": {
2972
- "version": "4.2.0",
2973
- "license": "MIT"
2974
- },
2975
- "node_modules/supports-color": {
2976
- "version": "7.2.0",
2977
- "dev": true,
2978
- "license": "MIT",
2979
- "dependencies": {
2980
- "has-flag": "^4.0.0"
2981
- },
2982
- "engines": {
2983
- "node": ">=8"
2984
- }
2985
- },
2986
- "node_modules/supports-preserve-symlinks-flag": {
2987
- "version": "1.0.0",
2988
- "license": "MIT",
2989
- "engines": {
2990
- "node": ">= 0.4"
2991
- },
2992
- "funding": {
2993
- "url": "https://github.com/sponsors/ljharb"
2994
- }
2995
- },
2996
- "node_modules/tiny-case": {
2997
- "version": "1.0.3",
2998
- "license": "MIT"
2999
- },
3000
- "node_modules/tinyglobby": {
3001
- "version": "0.2.15",
3002
- "dev": true,
3003
- "license": "MIT",
3004
- "dependencies": {
3005
- "fdir": "^6.5.0",
3006
- "picomatch": "^4.0.3"
3007
- },
3008
- "engines": {
3009
- "node": ">=12.0.0"
3010
- },
3011
- "funding": {
3012
- "url": "https://github.com/sponsors/SuperchupuDev"
3013
- }
3014
- },
3015
- "node_modules/tinyglobby/node_modules/picomatch": {
3016
- "version": "4.0.3",
3017
- "dev": true,
3018
- "license": "MIT",
3019
- "engines": {
3020
- "node": ">=12"
3021
- },
3022
- "funding": {
3023
- "url": "https://github.com/sponsors/jonschlinkert"
3024
- }
3025
- },
3026
- "node_modules/to-regex-range": {
3027
- "version": "5.0.1",
3028
- "dev": true,
3029
- "license": "MIT",
3030
- "dependencies": {
3031
- "is-number": "^7.0.0"
3032
- },
3033
- "engines": {
3034
- "node": ">=8.0"
3035
- }
3036
- },
3037
- "node_modules/toposort": {
3038
- "version": "2.0.2",
3039
- "license": "MIT"
3040
- },
3041
- "node_modules/ts-api-utils": {
3042
- "version": "2.1.0",
3043
- "dev": true,
3044
- "license": "MIT",
3045
- "engines": {
3046
- "node": ">=18.12"
3047
- },
3048
- "peerDependencies": {
3049
- "typescript": ">=4.8.4"
3050
- }
3051
- },
3052
- "node_modules/type-check": {
3053
- "version": "0.4.0",
3054
- "dev": true,
3055
- "license": "MIT",
3056
- "dependencies": {
3057
- "prelude-ls": "^1.2.1"
3058
- },
3059
- "engines": {
3060
- "node": ">= 0.8.0"
3061
- }
3062
- },
3063
- "node_modules/type-fest": {
3064
- "version": "2.19.0",
3065
- "license": "(MIT OR CC0-1.0)",
3066
- "engines": {
3067
- "node": ">=12.20"
3068
- },
3069
- "funding": {
3070
- "url": "https://github.com/sponsors/sindresorhus"
3071
- }
3072
- },
3073
- "node_modules/typescript": {
3074
- "version": "5.9.3",
3075
- "dev": true,
3076
- "license": "Apache-2.0",
3077
- "bin": {
3078
- "tsc": "bin/tsc",
3079
- "tsserver": "bin/tsserver"
3080
- },
3081
- "engines": {
3082
- "node": ">=14.17"
3083
- }
3084
- },
3085
- "node_modules/typescript-eslint": {
3086
- "version": "8.46.4",
3087
- "dev": true,
3088
- "license": "MIT",
3089
- "dependencies": {
3090
- "@typescript-eslint/eslint-plugin": "8.46.4",
3091
- "@typescript-eslint/parser": "8.46.4",
3092
- "@typescript-eslint/typescript-estree": "8.46.4",
3093
- "@typescript-eslint/utils": "8.46.4"
3094
- },
3095
- "engines": {
3096
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
3097
- },
3098
- "funding": {
3099
- "type": "opencollective",
3100
- "url": "https://opencollective.com/typescript-eslint"
3101
- },
3102
- "peerDependencies": {
3103
- "eslint": "^8.57.0 || ^9.0.0",
3104
- "typescript": ">=4.8.4 <6.0.0"
3105
- }
3106
- },
3107
- "node_modules/undici-types": {
3108
- "version": "7.16.0",
3109
- "dev": true,
3110
- "license": "MIT"
3111
- },
3112
- "node_modules/update-browserslist-db": {
3113
- "version": "1.1.4",
3114
- "dev": true,
3115
- "funding": [
3116
- {
3117
- "type": "opencollective",
3118
- "url": "https://opencollective.com/browserslist"
3119
- },
3120
- {
3121
- "type": "tidelift",
3122
- "url": "https://tidelift.com/funding/github/npm/browserslist"
3123
- },
3124
- {
3125
- "type": "github",
3126
- "url": "https://github.com/sponsors/ai"
3127
- }
3128
- ],
3129
- "license": "MIT",
3130
- "dependencies": {
3131
- "escalade": "^3.2.0",
3132
- "picocolors": "^1.1.1"
3133
- },
3134
- "bin": {
3135
- "update-browserslist-db": "cli.js"
3136
- },
3137
- "peerDependencies": {
3138
- "browserslist": ">= 4.21.0"
3139
- }
3140
- },
3141
- "node_modules/uri-js": {
3142
- "version": "4.4.1",
3143
- "dev": true,
3144
- "license": "BSD-2-Clause",
3145
- "dependencies": {
3146
- "punycode": "^2.1.0"
3147
- }
3148
- },
3149
- "node_modules/vite": {
3150
- "version": "7.2.2",
3151
- "dev": true,
3152
- "license": "MIT",
3153
- "dependencies": {
3154
- "esbuild": "^0.25.0",
3155
- "fdir": "^6.5.0",
3156
- "picomatch": "^4.0.3",
3157
- "postcss": "^8.5.6",
3158
- "rollup": "^4.43.0",
3159
- "tinyglobby": "^0.2.15"
3160
- },
3161
- "bin": {
3162
- "vite": "bin/vite.js"
3163
- },
3164
- "engines": {
3165
- "node": "^20.19.0 || >=22.12.0"
3166
- },
3167
- "funding": {
3168
- "url": "https://github.com/vitejs/vite?sponsor=1"
3169
- },
3170
- "optionalDependencies": {
3171
- "fsevents": "~2.3.3"
3172
- },
3173
- "peerDependencies": {
3174
- "@types/node": "^20.19.0 || >=22.12.0",
3175
- "jiti": ">=1.21.0",
3176
- "less": "^4.0.0",
3177
- "lightningcss": "^1.21.0",
3178
- "sass": "^1.70.0",
3179
- "sass-embedded": "^1.70.0",
3180
- "stylus": ">=0.54.8",
3181
- "sugarss": "^5.0.0",
3182
- "terser": "^5.16.0",
3183
- "tsx": "^4.8.1",
3184
- "yaml": "^2.4.2"
3185
- },
3186
- "peerDependenciesMeta": {
3187
- "@types/node": {
3188
- "optional": true
3189
- },
3190
- "jiti": {
3191
- "optional": true
3192
- },
3193
- "less": {
3194
- "optional": true
3195
- },
3196
- "lightningcss": {
3197
- "optional": true
3198
- },
3199
- "sass": {
3200
- "optional": true
3201
- },
3202
- "sass-embedded": {
3203
- "optional": true
3204
- },
3205
- "stylus": {
3206
- "optional": true
3207
- },
3208
- "sugarss": {
3209
- "optional": true
3210
- },
3211
- "terser": {
3212
- "optional": true
3213
- },
3214
- "tsx": {
3215
- "optional": true
3216
- },
3217
- "yaml": {
3218
- "optional": true
3219
- }
3220
- }
3221
- },
3222
- "node_modules/vite/node_modules/picomatch": {
3223
- "version": "4.0.3",
3224
- "dev": true,
3225
- "license": "MIT",
3226
- "engines": {
3227
- "node": ">=12"
3228
- },
3229
- "funding": {
3230
- "url": "https://github.com/sponsors/jonschlinkert"
3231
- }
3232
- },
3233
- "node_modules/which": {
3234
- "version": "2.0.2",
3235
- "dev": true,
3236
- "license": "ISC",
3237
- "dependencies": {
3238
- "isexe": "^2.0.0"
3239
- },
3240
- "bin": {
3241
- "node-which": "bin/node-which"
3242
- },
3243
- "engines": {
3244
- "node": ">= 8"
3245
- }
3246
- },
3247
- "node_modules/word-wrap": {
3248
- "version": "1.2.5",
3249
- "dev": true,
3250
- "license": "MIT",
3251
- "engines": {
3252
- "node": ">=0.10.0"
3253
- }
3254
- },
3255
- "node_modules/yallist": {
3256
- "version": "3.1.1",
3257
- "dev": true,
3258
- "license": "ISC"
3259
- },
3260
- "node_modules/yaml": {
3261
- "version": "1.10.2",
3262
- "license": "ISC",
3263
- "engines": {
3264
- "node": ">= 6"
3265
- }
3266
- },
3267
- "node_modules/yocto-queue": {
3268
- "version": "0.1.0",
3269
- "dev": true,
3270
- "license": "MIT",
3271
- "engines": {
3272
- "node": ">=10"
3273
- },
3274
- "funding": {
3275
- "url": "https://github.com/sponsors/sindresorhus"
3276
- }
3277
- },
3278
- "node_modules/yup": {
3279
- "version": "1.7.1",
3280
- "license": "MIT",
3281
- "dependencies": {
3282
- "property-expr": "^2.0.5",
3283
- "tiny-case": "^1.0.3",
3284
- "toposort": "^2.0.2",
3285
- "type-fest": "^2.19.0"
3286
- }
3287
- }
3288
- }
3289
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package.json DELETED
@@ -1,50 +0,0 @@
1
- {
2
- "name": "locallend-frontend",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "tsc -b && vite build",
9
- "lint": "eslint .",
10
- "preview": "vite preview",
11
- "docker:build": "docker build -t locallend-frontend .",
12
- "docker:run": "docker run -p 3000:80 locallend-frontend",
13
- "docker:compose": "docker-compose up -d",
14
- "docker:compose:build": "docker-compose up --build -d",
15
- "docker:stop": "docker-compose down",
16
- "docker:dev": "docker-compose -f docker-compose.dev.yml up --build -d",
17
- "docker:dev:stop": "docker-compose -f docker-compose.dev.yml down",
18
- "docker:dev:logs": "docker-compose -f docker-compose.dev.yml logs -f"
19
- },
20
- "dependencies": {
21
- "react": "^19.1.1",
22
- "react-dom": "^19.1.1",
23
- "react-router-dom": "^6.27.0",
24
- "axios": "^1.7.7",
25
- "@mui/material": "^6.1.7",
26
- "@emotion/react": "^11.13.3",
27
- "@emotion/styled": "^11.13.0",
28
- "@mui/icons-material": "^6.1.7",
29
- "@mui/x-date-pickers": "^7.20.0",
30
- "dayjs": "^1.11.13",
31
- "react-hook-form": "^7.53.2",
32
- "@hookform/resolvers": "^3.9.1",
33
- "yup": "^1.4.0",
34
- "react-hot-toast": "^2.4.1"
35
- },
36
- "devDependencies": {
37
- "@eslint/js": "^9.36.0",
38
- "@types/node": "^24.6.0",
39
- "@types/react": "^19.1.16",
40
- "@types/react-dom": "^19.1.9",
41
- "@vitejs/plugin-react": "^5.0.4",
42
- "eslint": "^9.36.0",
43
- "eslint-plugin-react-hooks": "^5.2.0",
44
- "eslint-plugin-react-refresh": "^0.4.22",
45
- "globals": "^16.4.0",
46
- "typescript": "~5.9.3",
47
- "typescript-eslint": "^8.45.0",
48
- "vite": "^7.1.7"
49
- }
50
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/public/vite.svg DELETED
frontend/src/App.css DELETED
@@ -1,42 +0,0 @@
1
- #root {
2
- max-width: 1280px;
3
- margin: 0 auto;
4
- padding: 2rem;
5
- text-align: center;
6
- }
7
-
8
- .logo {
9
- height: 6em;
10
- padding: 1.5em;
11
- will-change: filter;
12
- transition: filter 300ms;
13
- }
14
- .logo:hover {
15
- filter: drop-shadow(0 0 2em #646cffaa);
16
- }
17
- .logo.react:hover {
18
- filter: drop-shadow(0 0 2em #61dafbaa);
19
- }
20
-
21
- @keyframes logo-spin {
22
- from {
23
- transform: rotate(0deg);
24
- }
25
- to {
26
- transform: rotate(360deg);
27
- }
28
- }
29
-
30
- @media (prefers-reduced-motion: no-preference) {
31
- a:nth-of-type(2) .logo {
32
- animation: logo-spin infinite 20s linear;
33
- }
34
- }
35
-
36
- .card {
37
- padding: 2em;
38
- }
39
-
40
- .read-the-docs {
41
- color: #888;
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/App.tsx DELETED
@@ -1,232 +0,0 @@
1
- // App.tsx - Main application component for LocalLend
2
-
3
- import React, { useState, useEffect } from 'react';
4
- import { useAuth } from './context/AuthContext';
5
- import { LoginPage } from './pages/LoginPage';
6
- import { RegisterPage } from './pages/RegisterPage';
7
- import { BrowsePage } from './pages/BrowsePage';
8
- import { DashboardPage } from './pages/DashboardPage';
9
- import { MyItemsPage } from './pages/MyItemsPage';
10
- import { MyBookingsPage } from './pages/MyBookingsPage';
11
- import { AddItemPage } from './pages/AddItemPage';
12
- import { SearchResultsPage } from './pages/SearchResultsPage';
13
- import { BookItemPage } from './pages/BookItemPage';
14
- import { categoryService } from './services/categoryService';
15
- import type { Category } from './types';
16
-
17
- type Page = 'home' | 'login' | 'register' | 'browse' | 'search' | 'dashboard' | 'my-items' | 'my-bookings' | 'add-item' | 'book-item';
18
-
19
- const App = () => {
20
- const { user, isAuthenticated, logout } = useAuth();
21
- const [searchQuery, setSearchQuery] = useState('');
22
- const [currentPage, setCurrentPage] = useState<Page>('home');
23
- const [categories, setCategories] = useState<Category[]>([]);
24
-
25
- // Restore last page from localStorage (simple persistence)
26
- useEffect(() => {
27
- const saved = localStorage.getItem('currentPage') as Page | null;
28
- if (saved) {
29
- // If page requires auth and user not logged in, fallback to home
30
- const authPages: Page[] = ['dashboard', 'my-items', 'my-bookings', 'add-item', 'book-item'];
31
- if (authPages.includes(saved) && !user) {
32
- setCurrentPage('home');
33
- } else {
34
- setCurrentPage(saved);
35
- // Restore selected item for book-item page
36
- if (saved === 'book-item') {
37
- const savedItem = localStorage.getItem('selectedItem');
38
- if (savedItem) {
39
- try {
40
- setSelectedItem(JSON.parse(savedItem));
41
- } catch {}
42
- }
43
- }
44
- }
45
- }
46
- }, [user]);
47
-
48
- // Persist page on change
49
- useEffect(() => {
50
- localStorage.setItem('currentPage', currentPage);
51
- if (currentPage !== 'book-item') {
52
- localStorage.removeItem('selectedItem');
53
- }
54
- }, [currentPage]);
55
-
56
- // Fetch categories for dropdown
57
- useEffect(() => {
58
- const fetchCategories = async () => {
59
- try {
60
- const response = await categoryService.getCategories();
61
- if (response && response.data) {
62
- setCategories(response.data);
63
- }
64
- } catch (error) {
65
- console.error('Failed to fetch categories:', error);
66
- }
67
- };
68
- fetchCategories();
69
- }, []);
70
-
71
- const [selectedItem, setSelectedItem] = useState<any>(null);
72
-
73
- // Auto-route to dashboard after successful login
74
- useEffect(() => {
75
- if (isAuthenticated && user && currentPage === 'login') {
76
- setCurrentPage('dashboard');
77
- }
78
- }, [isAuthenticated, user, currentPage]);
79
-
80
- const handleLogin = () => setCurrentPage('login');
81
-
82
- const handleRegister = () => setCurrentPage('register');
83
-
84
- const handleBrowse = () => setCurrentPage('browse');
85
-
86
- const handleDashboard = () => setCurrentPage('dashboard');
87
-
88
- const _handleMyItems = () => setCurrentPage('my-items');
89
-
90
- const _handleMyBookings = () => setCurrentPage('my-bookings');
91
-
92
- const handleAddItem = () => setCurrentPage('add-item');
93
-
94
- const handleBookItem = (item: any) => {
95
- if (!user) {
96
- alert('Please log in to book items');
97
- return;
98
- }
99
- // Prevent booking own item
100
- if (item?.ownerId === user.id || item?.owner?.id === user.id) {
101
- alert("You can't book your own item.");
102
- return;
103
- }
104
-
105
- setSelectedItem(item);
106
- try { localStorage.setItem('selectedItem', JSON.stringify(item)); } catch {}
107
- setCurrentPage('book-item');
108
- };
109
-
110
- const handleLogout = () => {
111
- logout();
112
- setCurrentPage('home');
113
- };
114
-
115
- const handleSearch = (e: React.FormEvent) => {
116
- e.preventDefault();
117
- if (searchQuery.trim()) {
118
- setCurrentPage('search');
119
- }
120
- };
121
-
122
- const navigateToHome = () => {
123
- setCurrentPage('home');
124
- };
125
-
126
- // Render different pages based on current page state
127
- if (currentPage === 'login') {
128
- return <LoginPage onBack={navigateToHome} />;
129
- }
130
-
131
- if (currentPage === 'register') {
132
- return <RegisterPage onBack={navigateToHome} />;
133
- }
134
-
135
- if (currentPage === 'browse') {
136
- return <BrowsePage onBack={navigateToHome} />;
137
- }
138
-
139
- if (currentPage === 'search') {
140
- return <SearchResultsPage onBack={navigateToHome} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} onBookItem={handleBookItem} />;
141
- }
142
-
143
- if (currentPage === 'dashboard') {
144
- return <DashboardPage onBack={navigateToHome} onAddItem={handleAddItem} />;
145
- }
146
-
147
- if (currentPage === 'my-items') {
148
- return <MyItemsPage onBack={navigateToHome} />;
149
- }
150
-
151
- if (currentPage === 'my-bookings') {
152
- return <MyBookingsPage onBack={navigateToHome} />;
153
- }
154
-
155
- if (currentPage === 'add-item') {
156
- return <AddItemPage onBack={navigateToHome} />;
157
- }
158
-
159
- if (currentPage === 'book-item') {
160
- return <BookItemPage onBack={() => setCurrentPage('search')} item={selectedItem} />;
161
- }
162
-
163
- return (
164
- <div style={{ fontFamily: 'Roboto, Arial, sans-serif', margin: '0', padding: '0', backgroundColor: '#ffffff', color: '#213547', minHeight: '100vh' }}>
165
- <nav style={{ backgroundColor: '#1976d2', color: 'white', padding: '12px 0', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', marginBottom: '0' }}>
166
- <div style={{ maxWidth: '1200px', margin: '0 auto', padding: '0 20px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '20px' }}>
167
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
168
- <button onClick={navigateToHome} style={{ color: 'white', textDecoration: 'none', background: 'none', border: 'none', cursor: 'pointer', fontSize: '1.5rem', fontWeight: 'bold' }}>LocalLend</button>
169
- </div>
170
-
171
- <form onSubmit={handleSearch} style={{ display: 'flex', alignItems: 'center', gap: '8px', flex: 1, maxWidth: '500px' }}>
172
- <input type="search" placeholder="Search items..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} style={{ flex: 1, padding: '8px 12px', border: 'none', borderRadius: '4px', fontSize: '14px' }} />
173
- <select style={{ padding: '8px', border: 'none', borderRadius: '4px', fontSize: '14px' }}>
174
- <option value="">All Categories</option>
175
- {categories.map((category) => (
176
- <option key={category.id} value={category.id}>{category.name}</option>
177
- ))}
178
- </select>
179
- <button type="submit" style={{ padding: '8px 16px', backgroundColor: '#1565c0', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' }}>Search</button>
180
- </form>
181
-
182
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
183
- {isAuthenticated && user ? (
184
- <>
185
- <span style={{ color: 'white', fontSize: '14px', padding: '8px 12px', backgroundColor: 'rgba(255, 255, 255, 0.1)', borderRadius: '4px' }}>Welcome, {user.name}!</span>
186
- <button onClick={handleDashboard} style={{ color: 'white', textDecoration: 'none', padding: '8px 12px', background: 'none', border: 'none', cursor: 'pointer' }}>Dashboard</button>
187
- <button onClick={handleLogout} style={{ padding: '8px 16px', backgroundColor: '#d32f2f', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' }}>Logout</button>
188
- </>
189
- ) : (
190
- <>
191
- <button onClick={handleBrowse} style={{ color: 'white', textDecoration: 'none', padding: '8px 12px', background: 'none', border: 'none', cursor: 'pointer' }}>Browse Items</button>
192
- <button onClick={handleLogin} style={{ padding: '8px 16px', backgroundColor: 'transparent', color: 'white', border: '1px solid white', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' }}>Login</button>
193
- <button onClick={handleRegister} style={{ padding: '8px 16px', backgroundColor: '#4caf50', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' }}>Register</button>
194
- </>
195
- )}
196
- </div>
197
- </div>
198
- </nav>
199
-
200
- <div style={{ padding: '20px' }}>
201
- <header style={{ backgroundColor: '#1976d2', color: 'white', padding: '30px', borderRadius: '12px', marginBottom: '30px', textAlign: 'center', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }}>
202
- <h1 style={{ margin: '0 0 10px 0', fontSize: '2.5rem' }}>LocalLend - Peer-to-Peer Item Sharing</h1>
203
- <p style={{ margin: '0', fontSize: '1.2rem', opacity: '0.9' }}>A community platform for borrowing and lending items</p>
204
- </header>
205
-
206
- <main style={{ maxWidth: '1200px', margin: '0 auto' }}>
207
- <section style={{ marginBottom: '40px', backgroundColor: '#ffffff', padding: '30px', borderRadius: '12px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
208
- <h2 style={{ color: '#1976d2', marginBottom: '20px' }}>Welcome to LocalLend</h2>
209
- <p style={{ fontSize: '1.1rem', lineHeight: '1.6', color: '#424242' }}>LocalLend is a peer-to-peer item sharing platform where users can:</p>
210
- <ul style={{ fontSize: '1rem', lineHeight: '1.8', color: '#424242' }}>
211
- <li>List items they own for others to borrow</li>
212
- <li>Browse and search items available in their community</li>
213
- <li>Request to borrow items from other users</li>
214
- <li>Manage bookings (approve, track, complete)</li>
215
- <li>Rate users and items after completed transactions</li>
216
- <li>Build trust scores through positive interactions</li>
217
- </ul>
218
- </section>
219
-
220
- </main>
221
-
222
- <footer style={{ marginTop: '50px', textAlign: 'center', color: '#666', borderTop: '2px solid #e9ecef', paddingTop: '30px', backgroundColor: '#f8f9fa', marginLeft: '-20px', marginRight: '-20px', padding: '30px 20px' }}>
223
- <p style={{ margin: '10px 0', fontSize: '1rem' }}>LocalLend Frontend - Built with React + TypeScript + Vite</p>
224
- <p style={{ margin: '10px 0', fontSize: '0.9rem' }}>Backend API: http://localhost:8080</p>
225
- <p style={{ margin: '10px 0', fontSize: '0.9rem' }}>Frontend: http://localhost:5173</p>
226
- </footer>
227
- </div>
228
- </div>
229
- );
230
- };
231
-
232
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/App.tsx.backup DELETED
@@ -1,2048 +0,0 @@
1
- // App.tsx - Main application component for LocalLend
2
- import React, { useState } from 'react';
3
- import { useAuth } from './context/AuthContext';
4
- import { LoginPage } from './pages/LoginPage';
5
- import { RegisterPage } from './pages/RegisterPage';
6
- import { BrowsePage } from './pages/BrowsePage';
7
- import { DashboardPage } from './pages/DashboardPage';
8
- import { MyItemsPage } from './pages/MyItemsPage';
9
- import { MyBookingsPage } from './pages/MyBookingsPage';
10
- import { AddItemPage } from './pages/AddItemPage';
11
- import { SearchResultsPage } from './pages/SearchResultsPage';
12
- import { BookItemPage } from './pages/BookItemPage';
13
-
14
- type Page = 'home' | 'login' | 'register' | 'browse' | 'search' | 'dashboard' | 'my-items' | 'my-bookings' | 'add-item' | 'book-item';
15
-
16
- const App = () => {
17
- const { user } = useAuth();
18
- const [myItems, setMyItems] = React.useState<any[]>([]);
19
- const [myBookings, setMyBookings] = React.useState<any[]>([]);
20
- const [receivedBookings, setReceivedBookings] = React.useState<any[]>([]);
21
- const [isLoading, setIsLoading] = React.useState(true);
22
-
23
- React.useEffect(() => {
24
- const loadDashboardData = async () => {
25
- try {
26
- setIsLoading(true);
27
- console.log('=== DASHBOARD DATA LOADING START ===');
28
- console.log('Current user:', user);
29
- console.log('Current user ID:', user?.id, 'Type:', typeof user?.id);
30
- console.log('JWT Token exists:', !!localStorage.getItem('token'));
31
-
32
- // Load items I'm lending (owned by me)
33
- const itemsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items?ownerId=${user?.id}`, {
34
- headers: {
35
- 'Content-Type': 'application/json',
36
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
37
- 'X-User-Id': user?.id?.toString() || ''
38
- }
39
- });
40
- if (itemsResponse.ok) {
41
- const itemsData = await itemsResponse.json();
42
- console.log('Items API response:', itemsData);
43
- // Backend returns paginated data with 'content' array, not 'data' array
44
- const allItems = itemsData.content || itemsData.data || [];
45
- console.log('All items from API:', allItems);
46
- console.log('Query URL was:', `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items?ownerId=${user?.id}`);
47
-
48
- // Filter items to only show items owned by current user (frontend filtering as backup)
49
- const myOwnedItems = allItems.filter((item: any) => {
50
- console.log(`Comparing item.ownerId (${item.ownerId}) === user.id (${user?.id}):`, item.ownerId === user?.id);
51
- return item.ownerId === user?.id || item.owner_id === user?.id;
52
- });
53
- console.log('Filtered myItems to:', myOwnedItems);
54
- setMyItems(myOwnedItems);
55
- } else {
56
- console.log('Items API failed:', itemsResponse.status);
57
- }
58
-
59
- // Try to load my bookings (items I've borrowed) - gracefully handle server errors
60
- try {
61
- console.log('Fetching my bookings for user:', user?.id);
62
-
63
- // Try multiple possible endpoints for my bookings
64
- let bookingsResponse;
65
- let endpointUsed = '';
66
-
67
- // Try endpoint 1: /api/bookings/my-bookings (confirmed working by backend)
68
- try {
69
- endpointUsed = '/api/bookings/my-bookings';
70
- bookingsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/my-bookings`, {
71
- headers: {
72
- 'Content-Type': 'application/json',
73
- 'X-User-Id': user?.id?.toString() || ''
74
- }
75
- });
76
-
77
- if (!bookingsResponse.ok && bookingsResponse.status === 404) {
78
- // Try endpoint 2: /api/bookings/user/{userId} (alternative pattern)
79
- endpointUsed = `/api/bookings/user/${user?.id}`;
80
- bookingsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/user/${user?.id}`, {
81
- headers: {
82
- 'Content-Type': 'application/json',
83
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
84
- 'X-User-Id': user?.id?.toString() || ''
85
- }
86
- });
87
- }
88
-
89
- if (!bookingsResponse.ok && bookingsResponse.status === 404) {
90
- // Try endpoint 3: /api/bookings (with query param)
91
- endpointUsed = `/api/bookings?borrowerId=${user?.id}`;
92
- bookingsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings?borrowerId=${user?.id}`, {
93
- headers: {
94
- 'Content-Type': 'application/json',
95
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
96
- 'X-User-Id': user?.id?.toString() || ''
97
- }
98
- });
99
- }
100
- } catch (fetchError) {
101
- console.log('Error trying booking endpoints:', fetchError);
102
- setMyBookings([]);
103
- return;
104
- }
105
-
106
- console.log('Bookings response status:', bookingsResponse.status, 'from endpoint:', endpointUsed);
107
-
108
- if (bookingsResponse.ok) {
109
- const bookingsData = await bookingsResponse.json();
110
- console.log('✅ BOOKINGS SUCCESS - API response:', bookingsData);
111
- console.log('Bookings response type:', typeof bookingsData, 'is array:', Array.isArray(bookingsData));
112
- // Handle both paginated (content) and direct array responses
113
- const bookings = bookingsData.content || bookingsData.data || bookingsData || [];
114
- console.log('Final bookings array:', bookings, 'length:', bookings.length);
115
-
116
- // Let's also try to fetch ALL bookings to see what exists in the system
117
- try {
118
- const allBookingsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings`, {
119
- headers: {
120
- 'Content-Type': 'application/json',
121
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
122
- 'X-User-Id': user?.id?.toString() || ''
123
- }
124
- });
125
- if (allBookingsResponse.ok) {
126
- const allBookingsData = await allBookingsResponse.json();
127
- console.log('🔍 ALL BOOKINGS in system:', allBookingsData);
128
- }
129
- } catch (e) {
130
- console.log('Could not fetch all bookings for debugging');
131
- }
132
-
133
- setMyBookings(bookings);
134
- } else {
135
- const errorText = await bookingsResponse.text();
136
- console.log('All booking endpoints failed. Last error:', bookingsResponse.status, 'response:', errorText);
137
- setMyBookings([]);
138
- }
139
- } catch (error) {
140
- console.log('Bookings endpoint failed:', error);
141
- setMyBookings([]);
142
- }
143
-
144
- // Try to load bookings received (people wanting to borrow my items) - gracefully handle server errors
145
- try {
146
- console.log('Fetching received bookings for owner:', user?.id);
147
-
148
- // Try multiple possible endpoints for received bookings
149
- let receivedResponse;
150
- let ownerEndpointUsed = '';
151
-
152
- // Try endpoint 1: /api/bookings/my-owned (confirmed working by backend)
153
- try {
154
- ownerEndpointUsed = '/api/bookings/my-owned';
155
- receivedResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/my-owned`, {
156
- headers: {
157
- 'Content-Type': 'application/json',
158
- 'X-User-Id': user?.id?.toString() || ''
159
- }
160
- });
161
-
162
- if (!receivedResponse.ok && receivedResponse.status === 404) {
163
- // Try endpoint 2: /api/bookings/my-owned (alternative from earlier conversation)
164
- ownerEndpointUsed = '/api/bookings/my-owned';
165
- receivedResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/my-owned`, {
166
- headers: {
167
- 'Content-Type': 'application/json',
168
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
169
- 'X-User-Id': user?.id?.toString() || ''
170
- }
171
- });
172
- }
173
-
174
- if (!receivedResponse.ok && receivedResponse.status === 404) {
175
- // Try endpoint 3: /api/bookings/owner/{userId}
176
- ownerEndpointUsed = `/api/bookings/owner/${user?.id}`;
177
- receivedResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/owner/${user?.id}`, {
178
- headers: {
179
- 'Content-Type': 'application/json',
180
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
181
- 'X-User-Id': user?.id?.toString() || ''
182
- }
183
- });
184
- }
185
-
186
- if (!receivedResponse.ok && receivedResponse.status === 404) {
187
- // Try endpoint 4: /api/bookings (with query param)
188
- ownerEndpointUsed = `/api/bookings?ownerId=${user?.id}`;
189
- receivedResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings?ownerId=${user?.id}`, {
190
- headers: {
191
- 'Content-Type': 'application/json',
192
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
193
- 'X-User-Id': user?.id?.toString() || ''
194
- }
195
- });
196
- }
197
- } catch (fetchError) {
198
- console.log('Error trying owner booking endpoints:', fetchError);
199
- setReceivedBookings([]);
200
- return;
201
- }
202
-
203
- console.log('Received bookings response status:', receivedResponse.status, 'from endpoint:', ownerEndpointUsed);
204
-
205
- if (receivedResponse.ok) {
206
- const receivedData = await receivedResponse.json();
207
- console.log('Received bookings API response:', receivedData);
208
- console.log('Received bookings response type:', typeof receivedData, 'is array:', Array.isArray(receivedData));
209
- // Handle both paginated (content) and direct array responses
210
- const bookings = receivedData.content || receivedData.data || receivedData || [];
211
- console.log('Setting receivedBookings to:', bookings, 'length:', bookings.length);
212
- setReceivedBookings(bookings);
213
- } else {
214
- const errorText = await receivedResponse.text();
215
- console.log('All owner booking endpoints failed. Last error:', receivedResponse.status, 'response:', errorText);
216
- setReceivedBookings([]);
217
- }
218
- } catch (error) {
219
- console.log('Received bookings endpoint failed:', error);
220
- setReceivedBookings([]);
221
- }
222
- } catch (error) {
223
- console.error('Error loading dashboard data:', error);
224
- // Set empty arrays if there's a general error
225
- setMyItems([]);
226
- setMyBookings([]);
227
- setReceivedBookings([]);
228
- } finally {
229
- setIsLoading(false);
230
- }
231
- };
232
-
233
- if (user) {
234
- loadDashboardData();
235
- }
236
- }, [user]);
237
-
238
- // Debug: Log current state values
239
- console.log('Dashboard render - myItems:', myItems, 'myBookings:', myBookings, 'receivedBookings:', receivedBookings);
240
-
241
- return (
242
- <div style={{
243
- fontFamily: 'Roboto, Arial, sans-serif',
244
- margin: '0',
245
- padding: '0',
246
- backgroundColor: '#f5f5f5',
247
- color: '#213547',
248
- minHeight: '100vh'
249
- }}>
250
- {/* Navigation Bar */}
251
- <nav style={{
252
- backgroundColor: '#1976d2',
253
- color: 'white',
254
- padding: '12px 0',
255
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
256
- marginBottom: '20px'
257
- }}>
258
- <div style={{
259
- maxWidth: '1200px',
260
- margin: '0 auto',
261
- padding: '0 20px',
262
- display: 'flex',
263
- alignItems: 'center',
264
- justifyContent: 'space-between'
265
- }}>
266
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
267
- <button
268
- onClick={onBack}
269
- style={{
270
- color: 'white',
271
- textDecoration: 'none',
272
- background: 'none',
273
- border: 'none',
274
- cursor: 'pointer',
275
- fontSize: '1.5rem',
276
- fontWeight: 'bold'
277
- }}
278
- >
279
- LocalLend
280
- </button>
281
- </div>
282
-
283
- <span style={{
284
- color: 'white',
285
- fontSize: '14px',
286
- padding: '8px 12px',
287
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
288
- borderRadius: '4px'
289
- }}>
290
- Welcome, {user?.name}!
291
- </span>
292
- </div>
293
- </nav>
294
-
295
- {/* Dashboard Content */}
296
- <div style={{ padding: '0 20px', maxWidth: '1200px', margin: '0 auto' }}>
297
- <h1 style={{ marginBottom: '30px', color: '#1976d2' }}>Dashboard</h1>
298
-
299
- {isLoading ? (
300
- <div style={{ textAlign: 'center', padding: '40px' }}>
301
- <p>Loading your dashboard...</p>
302
- </div>
303
- ) : (
304
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '20px' }}>
305
-
306
- {/* My Items Section */}
307
- <div style={{
308
- backgroundColor: 'white',
309
- padding: '20px',
310
- borderRadius: '8px',
311
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
312
- }}>
313
- <h2 style={{ color: '#1976d2', marginBottom: '15px' }}>My Items ({myItems.length})</h2>
314
- <p style={{ color: '#666', marginBottom: '15px', fontSize: '14px' }}>Items you're lending to others</p>
315
-
316
- {myItems.length === 0 ? (
317
- <p style={{ color: '#888', fontStyle: 'italic' }}>No items listed yet. Start lending!</p>
318
- ) : (
319
- <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
320
- {myItems.map((item) => (
321
- <div key={item.id} style={{
322
- border: '1px solid #e0e0e0',
323
- borderRadius: '4px',
324
- padding: '12px',
325
- marginBottom: '10px',
326
- backgroundColor: '#fafafa'
327
- }}>
328
- <h4 style={{ margin: '0 0 5px 0', color: '#333' }}>{item.name || item.title}</h4>
329
- <p style={{ margin: '0 0 8px 0', color: '#666', fontSize: '14px' }}>{item.description}</p>
330
- <div style={{ display: 'flex', gap: '10px', fontSize: '12px' }}>
331
- <span style={{
332
- padding: '2px 8px',
333
- borderRadius: '12px',
334
- backgroundColor: item.status === 'AVAILABLE' ? '#4caf50' : '#ff9800',
335
- color: 'white'
336
- }}>
337
- {item.status}
338
- </span>
339
- <span style={{
340
- padding: '2px 8px',
341
- borderRadius: '12px',
342
- backgroundColor: '#2196f3',
343
- color: 'white'
344
- }}>
345
- {item.condition}
346
- </span>
347
- </div>
348
- </div>
349
- ))}
350
- </div>
351
- )}
352
- </div>
353
-
354
- {/* My Bookings Section */}
355
- <div style={{
356
- backgroundColor: 'white',
357
- padding: '20px',
358
- borderRadius: '8px',
359
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
360
- }}>
361
- <h2 style={{ color: '#1976d2', marginBottom: '15px' }}>My Bookings ({myBookings.length})</h2>
362
- <p style={{ color: '#666', marginBottom: '15px', fontSize: '14px' }}>Items you've borrowed from others</p>
363
-
364
- {myBookings.length === 0 ? (
365
- <p style={{ color: '#888', fontStyle: 'italic' }}>No active bookings. Browse items to borrow!</p>
366
- ) : (
367
- <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
368
- {myBookings.map((booking) => (
369
- <div key={booking.id} style={{
370
- border: '1px solid #e0e0e0',
371
- borderRadius: '4px',
372
- padding: '12px',
373
- marginBottom: '10px',
374
- backgroundColor: '#fafafa'
375
- }}>
376
- <h4 style={{ margin: '0 0 5px 0', color: '#333' }}>{booking.itemTitle}</h4>
377
- <p style={{ margin: '0 0 8px 0', color: '#666', fontSize: '14px' }}>
378
- {booking.startDate} to {booking.endDate}
379
- </p>
380
- <span style={{
381
- padding: '2px 8px',
382
- borderRadius: '12px',
383
- backgroundColor: booking.status === 'APPROVED' ? '#4caf50' : '#ff9800',
384
- color: 'white',
385
- fontSize: '12px'
386
- }}>
387
- {booking.status}
388
- </span>
389
- </div>
390
- ))}
391
- </div>
392
- )}
393
- </div>
394
-
395
- {/* Booking Requests Section */}
396
- <div style={{
397
- backgroundColor: 'white',
398
- padding: '20px',
399
- borderRadius: '8px',
400
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
401
- gridColumn: 'span 2'
402
- }}>
403
- <h2 style={{ color: '#1976d2', marginBottom: '15px' }}>Booking Requests ({receivedBookings.length})</h2>
404
- <p style={{ color: '#666', marginBottom: '15px', fontSize: '14px' }}>People wanting to borrow your items</p>
405
-
406
- {receivedBookings.length === 0 ? (
407
- <p style={{ color: '#888', fontStyle: 'italic' }}>No pending booking requests.</p>
408
- ) : (
409
- <div style={{ maxHeight: '300px', overflowY: 'auto' }}>
410
- {receivedBookings.map((booking) => (
411
- <div key={booking.id} style={{
412
- border: '1px solid #e0e0e0',
413
- borderRadius: '4px',
414
- padding: '15px',
415
- marginBottom: '10px',
416
- backgroundColor: '#fafafa',
417
- display: 'flex',
418
- justifyContent: 'space-between',
419
- alignItems: 'center'
420
- }}>
421
- <div>
422
- <h4 style={{ margin: '0 0 5px 0', color: '#333' }}>{booking.itemTitle}</h4>
423
- <p style={{ margin: '0 0 5px 0', color: '#666', fontSize: '14px' }}>
424
- Requested by: {booking.borrowerName}
425
- </p>
426
- <p style={{ margin: '0', color: '#666', fontSize: '14px' }}>
427
- From: {booking.startDate}
428
- </p>
429
- </div>
430
- <div style={{ display: 'flex', gap: '10px' }}>
431
- <button style={{
432
- padding: '6px 12px',
433
- backgroundColor: '#4caf50',
434
- color: 'white',
435
- border: 'none',
436
- borderRadius: '4px',
437
- cursor: 'pointer',
438
- fontSize: '14px'
439
- }}>
440
- Approve
441
- </button>
442
- <button style={{
443
- padding: '6px 12px',
444
- backgroundColor: '#f44336',
445
- color: 'white',
446
- border: 'none',
447
- borderRadius: '4px',
448
- cursor: 'pointer',
449
- fontSize: '14px'
450
- }}>
451
- Decline
452
- </button>
453
- </div>
454
- </div>
455
- ))}
456
- </div>
457
- )}
458
- </div>
459
-
460
- </div>
461
- )}
462
-
463
- {/* Quick Actions */}
464
- <div style={{
465
- marginTop: '30px',
466
- display: 'flex',
467
- gap: '15px',
468
- flexWrap: 'wrap'
469
- }}>
470
- <button
471
- onClick={onAddItem}
472
- style={{
473
- padding: '12px 24px',
474
- backgroundColor: '#4caf50',
475
- color: 'white',
476
- border: 'none',
477
- borderRadius: '4px',
478
- cursor: 'pointer',
479
- fontSize: '16px',
480
- fontWeight: 'bold'
481
- }}>
482
- + Add New Item
483
- </button>
484
-
485
- <button style={{
486
- padding: '12px 24px',
487
- backgroundColor: '#2196f3',
488
- color: 'white',
489
- border: 'none',
490
- borderRadius: '4px',
491
- cursor: 'pointer',
492
- fontSize: '16px'
493
- }}>
494
- Browse Items
495
- </button>
496
-
497
- <button onClick={onBack} style={{
498
- padding: '12px 24px',
499
- backgroundColor: '#757575',
500
- color: 'white',
501
- border: 'none',
502
- borderRadius: '4px',
503
- cursor: 'pointer',
504
- fontSize: '16px'
505
- }}>
506
- Back to Home
507
- </button>
508
- </div>
509
- </div>
510
- </div>
511
- );
512
- };
513
-
514
- const MyItemsPage: React.FC<PageProps> = ({ onBack }) => (
515
- <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
516
- <h1>My Items</h1>
517
- <p>Here you can manage the items you're lending out to others.</p>
518
- <button onClick={onBack} style={{ padding: '10px 20px', backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
519
- Back to Home
520
- </button>
521
- </div>
522
- );
523
-
524
- const MyBookingsPage: React.FC<PageProps> = ({ onBack }) => (
525
- <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
526
- <h1>My Bookings</h1>
527
- <p>Here you can see all the items you've booked from other users.</p>
528
- <button onClick={onBack} style={{ padding: '10px 20px', backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
529
- Back to Home
530
- </button>
531
- </div>
532
- );
533
-
534
- const AddItemPage: React.FC<PageProps> = ({ onBack }) => {
535
- const { user } = useAuth();
536
- const [isLoading, setIsLoading] = React.useState(false);
537
- const [categories, setCategories] = React.useState<any[]>([]);
538
- const [formData, setFormData] = React.useState({
539
- title: '',
540
- description: '',
541
- category: '',
542
- condition: 'GOOD',
543
- pricePerDay: '',
544
- availableFrom: '',
545
- availableTo: '',
546
- pickupLocation: '',
547
- specialInstructions: ''
548
- });
549
-
550
- // Load categories from backend on component mount
551
- React.useEffect(() => {
552
- const fetchCategories = async () => {
553
- try {
554
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/categories`);
555
- if (response.ok) {
556
- const categoryResponse = await response.json();
557
- console.log('Categories loaded:', categoryResponse);
558
- // Extract the data array from the response
559
- const categoryData = categoryResponse.data || categoryResponse;
560
- setCategories(categoryData);
561
- } else {
562
- console.error('Failed to load categories');
563
- // Fallback categories if API fails
564
- setCategories([
565
- { id: '1', name: 'Electronics' },
566
- { id: '2', name: 'Tools' },
567
- { id: '3', name: 'Sports' },
568
- { id: '4', name: 'Books' },
569
- { id: '5', name: 'Furniture' },
570
- { id: '6', name: 'Vehicles' },
571
- { id: '7', name: 'Appliances' },
572
- { id: '8', name: 'Other' }
573
- ]);
574
- }
575
- } catch (error) {
576
- console.error('Error fetching categories:', error);
577
- // Fallback categories if API fails
578
- setCategories([
579
- { id: '1', name: 'Electronics' },
580
- { id: '2', name: 'Tools' },
581
- { id: '3', name: 'Sports' },
582
- { id: '4', name: 'Books' },
583
- { id: '5', name: 'Furniture' },
584
- { id: '6', name: 'Vehicles' },
585
- { id: '7', name: 'Appliances' },
586
- { id: '8', name: 'Other' }
587
- ]);
588
- }
589
- };
590
-
591
- fetchCategories();
592
- }, []);
593
-
594
- const conditions = [
595
- { value: 'EXCELLENT', label: 'Excellent - Like new' },
596
- { value: 'GOOD', label: 'Good - Minor wear' },
597
- { value: 'FAIR', label: 'Fair - Noticeable wear' },
598
- { value: 'POOR', label: 'Poor - Significant wear' }
599
- ];
600
-
601
- const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
602
- const { name, value } = e.target;
603
- setFormData(prev => ({
604
- ...prev,
605
- [name]: value
606
- }));
607
- };
608
-
609
- const handleSubmit = async (e: React.FormEvent) => {
610
- e.preventDefault();
611
-
612
- if (!user) {
613
- alert('You must be logged in to add items');
614
- return;
615
- }
616
-
617
- // Client-side validation
618
- if (formData.title.length < 3 || formData.title.length > 100) {
619
- alert('Item title must be between 3 and 100 characters long');
620
- return;
621
- }
622
-
623
- if (formData.description.length < 10 || formData.description.length > 500) {
624
- alert('Item description must be between 10 and 500 characters long');
625
- return;
626
- }
627
-
628
- if (!formData.category) {
629
- alert('Please select a category');
630
- return;
631
- }
632
-
633
- if (formData.pricePerDay && parseFloat(formData.pricePerDay) < 0) {
634
- alert('Price per day must be a positive number');
635
- return;
636
- }
637
-
638
- setIsLoading(true);
639
-
640
- try {
641
- // Prepare item data matching the backend CreateItemRequest interface
642
- const itemData = {
643
- name: formData.title, // Backend expects 'name', not 'title'
644
- description: formData.description,
645
- categoryId: formData.category, // This is now the actual category ID from the backend
646
- condition: formData.condition,
647
- deposit: formData.pricePerDay ? parseFloat(formData.pricePerDay) : undefined,
648
- images: [] // No image upload implemented yet
649
- };
650
-
651
- console.log('Submitting item data:', itemData);
652
-
653
- // Use the itemService to create the item (it handles auth headers properly)
654
- const newItem = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items`, {
655
- method: 'POST',
656
- headers: {
657
- 'Content-Type': 'application/json',
658
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
659
- 'X-User-Id': user.id.toString()
660
- },
661
- body: JSON.stringify(itemData)
662
- });
663
-
664
- if (newItem.ok) {
665
- const createdItem = await newItem.json();
666
- console.log('Item created successfully:', createdItem);
667
- alert(`Item "${createdItem.name || formData.title}" added successfully!`);
668
-
669
- // Reset form
670
- setFormData({
671
- title: '',
672
- description: '',
673
- category: '',
674
- condition: 'GOOD',
675
- pricePerDay: '',
676
- availableFrom: '',
677
- availableTo: '',
678
- pickupLocation: '',
679
- specialInstructions: ''
680
- });
681
-
682
- onBack(); // Go back to dashboard/home
683
- } else {
684
- const errorResponse = await newItem.text();
685
- console.error('Server error response:', errorResponse);
686
-
687
- let errorMessage = 'Failed to add item';
688
- try {
689
- const errorData = JSON.parse(errorResponse);
690
- errorMessage = errorData.message || errorData.error || errorMessage;
691
- } catch (e) {
692
- errorMessage = errorResponse || errorMessage;
693
- }
694
-
695
- throw new Error(`Server error (${newItem.status}): ${errorMessage}`);
696
- }
697
- } catch (error: any) {
698
- console.error('Error adding item:', error);
699
-
700
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
701
- alert('Network error: Cannot connect to backend server. Please ensure the backend is running on http://localhost:8080');
702
- } else {
703
- alert(`Error adding item: ${error.message}`);
704
- }
705
- } finally {
706
- setIsLoading(false);
707
- }
708
- };
709
-
710
- return (
711
- <div style={{
712
- fontFamily: 'Roboto, Arial, sans-serif',
713
- margin: '0',
714
- padding: '0',
715
- backgroundColor: '#f5f5f5',
716
- color: '#213547',
717
- minHeight: '100vh'
718
- }}>
719
- {/* Navigation Bar */}
720
- <nav style={{
721
- backgroundColor: '#1976d2',
722
- color: 'white',
723
- padding: '12px 0',
724
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
725
- marginBottom: '20px'
726
- }}>
727
- <div style={{
728
- maxWidth: '1200px',
729
- margin: '0 auto',
730
- padding: '0 20px',
731
- display: 'flex',
732
- alignItems: 'center',
733
- justifyContent: 'space-between'
734
- }}>
735
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
736
- <button
737
- onClick={onBack}
738
- style={{
739
- color: 'white',
740
- textDecoration: 'none',
741
- background: 'none',
742
- border: 'none',
743
- cursor: 'pointer',
744
- fontSize: '1.5rem',
745
- fontWeight: 'bold'
746
- }}
747
- >
748
- LocalLend
749
- </button>
750
- </div>
751
-
752
- <span style={{
753
- color: 'white',
754
- fontSize: '14px',
755
- padding: '8px 12px',
756
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
757
- borderRadius: '4px'
758
- }}>
759
- Welcome, {user?.name}!
760
- </span>
761
- </div>
762
- </nav>
763
-
764
- {/* Add Item Form */}
765
- <div style={{ padding: '0 20px', maxWidth: '800px', margin: '0 auto' }}>
766
- <h1 style={{ marginBottom: '10px', color: '#1976d2' }}>Add New Item</h1>
767
- <p style={{ color: '#666', marginBottom: '30px' }}>
768
- List an item you'd like to lend to others in your community.
769
- </p>
770
-
771
- <div style={{
772
- backgroundColor: 'white',
773
- padding: '30px',
774
- borderRadius: '8px',
775
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
776
- }}>
777
- <form onSubmit={handleSubmit}>
778
- {/* Title */}
779
- <div style={{ marginBottom: '20px' }}>
780
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
781
- Item Title * (3-100 characters)
782
- </label>
783
- <input
784
- type="text"
785
- name="title"
786
- value={formData.title}
787
- onChange={handleChange}
788
- required
789
- minLength={3}
790
- maxLength={100}
791
- placeholder="e.g., Canon DSLR Camera, Mountain Bike, Power Drill"
792
- style={{
793
- width: '100%',
794
- padding: '12px',
795
- border: '1px solid #ddd',
796
- borderRadius: '4px',
797
- fontSize: '16px',
798
- boxSizing: 'border-box'
799
- }}
800
- />
801
- {formData.title.length > 0 && formData.title.length < 3 && (
802
- <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
803
- Title must be at least 3 characters long
804
- </div>
805
- )}
806
- {formData.title.length > 100 && (
807
- <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
808
- Title must be no more than 100 characters long
809
- </div>
810
- )}
811
- </div>
812
-
813
- {/* Description */}
814
- <div style={{ marginBottom: '20px' }}>
815
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
816
- Description * (10-500 characters)
817
- </label>
818
- <textarea
819
- name="description"
820
- value={formData.description}
821
- onChange={handleChange}
822
- required
823
- minLength={10}
824
- maxLength={500}
825
- rows={4}
826
- placeholder="Describe your item in detail - condition, features, what's included..."
827
- style={{
828
- width: '100%',
829
- padding: '12px',
830
- border: '1px solid #ddd',
831
- borderRadius: '4px',
832
- fontSize: '16px',
833
- boxSizing: 'border-box',
834
- resize: 'vertical'
835
- }}
836
- />
837
- <div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
838
- {formData.description.length}/500 characters
839
- </div>
840
- {formData.description.length > 0 && formData.description.length < 10 && (
841
- <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
842
- Description must be at least 10 characters long
843
- </div>
844
- )}
845
- </div>
846
-
847
- {/* Category and Condition Row */}
848
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', marginBottom: '20px' }}>
849
- <div>
850
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
851
- Category *
852
- </label>
853
- <select
854
- name="category"
855
- value={formData.category}
856
- onChange={handleChange}
857
- required
858
- style={{
859
- width: '100%',
860
- padding: '12px',
861
- border: '1px solid #ddd',
862
- borderRadius: '4px',
863
- fontSize: '16px',
864
- boxSizing: 'border-box'
865
- }}
866
- >
867
- <option value="">Select a category</option>
868
- {categories.map(cat => (
869
- <option key={cat.id} value={cat.id}>{cat.name}</option>
870
- ))}
871
- </select>
872
- </div>
873
-
874
- <div>
875
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
876
- Condition *
877
- </label>
878
- <select
879
- name="condition"
880
- value={formData.condition}
881
- onChange={handleChange}
882
- required
883
- style={{
884
- width: '100%',
885
- padding: '12px',
886
- border: '1px solid #ddd',
887
- borderRadius: '4px',
888
- fontSize: '16px',
889
- boxSizing: 'border-box'
890
- }}
891
- >
892
- {conditions.map(condition => (
893
- <option key={condition.value} value={condition.value}>
894
- {condition.label}
895
- </option>
896
- ))}
897
- </select>
898
- </div>
899
- </div>
900
-
901
- {/* Price */}
902
- <div style={{ marginBottom: '20px' }}>
903
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
904
- Price per Day ($) *
905
- </label>
906
- <input
907
- type="number"
908
- name="pricePerDay"
909
- value={formData.pricePerDay}
910
- onChange={handleChange}
911
- required
912
- min="0"
913
- step="0.01"
914
- placeholder="25.00"
915
- style={{
916
- width: '200px',
917
- padding: '12px',
918
- border: '1px solid #ddd',
919
- borderRadius: '4px',
920
- fontSize: '16px',
921
- boxSizing: 'border-box'
922
- }}
923
- />
924
- </div>
925
-
926
- {/* Availability Dates */}
927
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', marginBottom: '20px' }}>
928
- <div>
929
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
930
- Available From
931
- </label>
932
- <input
933
- type="date"
934
- name="availableFrom"
935
- value={formData.availableFrom}
936
- onChange={handleChange}
937
- style={{
938
- width: '100%',
939
- padding: '12px',
940
- border: '1px solid #ddd',
941
- borderRadius: '4px',
942
- fontSize: '16px',
943
- boxSizing: 'border-box'
944
- }}
945
- />
946
- </div>
947
-
948
- <div>
949
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
950
- Available Until
951
- </label>
952
- <input
953
- type="date"
954
- name="availableTo"
955
- value={formData.availableTo}
956
- onChange={handleChange}
957
- style={{
958
- width: '100%',
959
- padding: '12px',
960
- border: '1px solid #ddd',
961
- borderRadius: '4px',
962
- fontSize: '16px',
963
- boxSizing: 'border-box'
964
- }}
965
- />
966
- </div>
967
- </div>
968
-
969
- {/* Pickup Location */}
970
- <div style={{ marginBottom: '20px' }}>
971
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
972
- Pickup Location
973
- </label>
974
- <input
975
- type="text"
976
- name="pickupLocation"
977
- value={formData.pickupLocation}
978
- onChange={handleChange}
979
- placeholder="e.g., Downtown area, My office, Near Metro Station"
980
- style={{
981
- width: '100%',
982
- padding: '12px',
983
- border: '1px solid #ddd',
984
- borderRadius: '4px',
985
- fontSize: '16px',
986
- boxSizing: 'border-box'
987
- }}
988
- />
989
- </div>
990
-
991
- {/* Special Instructions */}
992
- <div style={{ marginBottom: '30px' }}>
993
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
994
- Special Instructions
995
- </label>
996
- <textarea
997
- name="specialInstructions"
998
- value={formData.specialInstructions}
999
- onChange={handleChange}
1000
- rows={3}
1001
- placeholder="Any special care instructions, usage notes, or requirements..."
1002
- style={{
1003
- width: '100%',
1004
- padding: '12px',
1005
- border: '1px solid #ddd',
1006
- borderRadius: '4px',
1007
- fontSize: '16px',
1008
- boxSizing: 'border-box',
1009
- resize: 'vertical'
1010
- }}
1011
- />
1012
- </div>
1013
-
1014
- {/* Submit Buttons */}
1015
- <div style={{ display: 'flex', gap: '15px' }}>
1016
- <button
1017
- type="submit"
1018
- disabled={isLoading}
1019
- style={{
1020
- padding: '12px 30px',
1021
- backgroundColor: isLoading ? '#ccc' : '#4caf50',
1022
- color: 'white',
1023
- border: 'none',
1024
- borderRadius: '4px',
1025
- cursor: isLoading ? 'not-allowed' : 'pointer',
1026
- fontSize: '16px',
1027
- fontWeight: 'bold'
1028
- }}
1029
- >
1030
- {isLoading ? 'Adding Item...' : 'Add Item'}
1031
- </button>
1032
-
1033
- <button
1034
- type="button"
1035
- onClick={onBack}
1036
- disabled={isLoading}
1037
- style={{
1038
- padding: '12px 30px',
1039
- backgroundColor: '#757575',
1040
- color: 'white',
1041
- border: 'none',
1042
- borderRadius: '4px',
1043
- cursor: isLoading ? 'not-allowed' : 'pointer',
1044
- fontSize: '16px'
1045
- }}
1046
- >
1047
- Cancel
1048
- </button>
1049
- </div>
1050
- </form>
1051
- </div>
1052
- </div>
1053
- </div>
1054
- );
1055
- };
1056
-
1057
- const SearchResultsPage: React.FC<PageProps & { searchQuery: string; onSearchQueryChange: (query: string) => void; onBookItem: (itemId: string, itemTitle: string) => void }> = ({ onBack, searchQuery, onSearchQueryChange, onBookItem }) => {
1058
- console.log('🔍🔍🔍 SearchResultsPage rendered with onBookItem:', onBookItem);
1059
- console.log('🔍🔍🔍 SearchResultsPage component loaded successfully');
1060
-
1061
- React.useEffect(() => {
1062
- console.log('🔍🔍🔍 SearchResultsPage useEffect - this should show when page loads');
1063
- }, []);
1064
- const { user } = useAuth();
1065
- const [searchResults, setSearchResults] = React.useState<any[]>([]);
1066
- const [isLoading, setIsLoading] = React.useState(true);
1067
- const [selectedCategory, setSelectedCategory] = React.useState('');
1068
-
1069
- React.useEffect(() => {
1070
- const performSearch = async () => {
1071
- try {
1072
- setIsLoading(true);
1073
-
1074
- // Search using the backend API
1075
- const queryParams = new URLSearchParams();
1076
- if (searchQuery) queryParams.append('name', searchQuery); // Backend expects 'name' parameter for search
1077
- if (selectedCategory) queryParams.append('categoryId', selectedCategory);
1078
- queryParams.append('page', '0');
1079
- queryParams.append('size', '20'); // Get more results for search
1080
-
1081
- console.log('Performing search with query:', searchQuery, 'category:', selectedCategory);
1082
-
1083
- const searchResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items?${queryParams.toString()}`, {
1084
- headers: {
1085
- 'Content-Type': 'application/json',
1086
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
1087
- 'X-User-Id': user?.id?.toString() || ''
1088
- }
1089
- });
1090
-
1091
- if (searchResponse.ok) {
1092
- const searchData = await searchResponse.json();
1093
- console.log('Search API response:', searchData);
1094
- // Handle paginated response structure
1095
- const results = searchData.content || searchData.data || [];
1096
- console.log('Setting search results to:', results);
1097
- setSearchResults(results);
1098
- } else {
1099
- console.log('Search API failed:', searchResponse.status);
1100
- setSearchResults([]);
1101
- }
1102
- } catch (error) {
1103
- console.error('Search error:', error);
1104
- // Set empty results on error
1105
- setSearchResults([]);
1106
- } finally {
1107
- setIsLoading(false);
1108
- }
1109
- };
1110
-
1111
- performSearch();
1112
- }, [searchQuery, selectedCategory]);
1113
-
1114
- const handleNewSearch = (e: React.FormEvent) => {
1115
- e.preventDefault();
1116
- // Search will be triggered by useEffect when searchQuery changes
1117
- };
1118
-
1119
-
1120
-
1121
- return (
1122
- <div style={{
1123
- fontFamily: 'Roboto, Arial, sans-serif',
1124
- margin: '0',
1125
- padding: '0',
1126
- backgroundColor: '#f5f5f5',
1127
- color: '#213547',
1128
- minHeight: '100vh'
1129
- }}>
1130
- {/* Navigation Bar */}
1131
- <nav style={{
1132
- backgroundColor: '#1976d2',
1133
- color: 'white',
1134
- padding: '12px 0',
1135
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1136
- marginBottom: '20px'
1137
- }}>
1138
- <div style={{
1139
- maxWidth: '1200px',
1140
- margin: '0 auto',
1141
- padding: '0 20px',
1142
- display: 'flex',
1143
- alignItems: 'center',
1144
- justifyContent: 'space-between',
1145
- gap: '20px'
1146
- }}>
1147
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
1148
- <button
1149
- onClick={onBack}
1150
- style={{
1151
- color: 'white',
1152
- textDecoration: 'none',
1153
- background: 'none',
1154
- border: 'none',
1155
- cursor: 'pointer',
1156
- fontSize: '1.5rem',
1157
- fontWeight: 'bold'
1158
- }}
1159
- >
1160
- LocalLend
1161
- </button>
1162
- </div>
1163
-
1164
- {/* Search Bar */}
1165
- <form onSubmit={handleNewSearch} style={{
1166
- display: 'flex',
1167
- alignItems: 'center',
1168
- gap: '8px',
1169
- flex: 1,
1170
- maxWidth: '500px'
1171
- }}>
1172
- <input
1173
- type="search"
1174
- placeholder="Search items..."
1175
- value={searchQuery}
1176
- onChange={(e) => onSearchQueryChange(e.target.value)}
1177
- style={{
1178
- flex: 1,
1179
- padding: '8px 12px',
1180
- border: 'none',
1181
- borderRadius: '4px',
1182
- fontSize: '14px'
1183
- }}
1184
- />
1185
- <select
1186
- value={selectedCategory}
1187
- onChange={(e) => setSelectedCategory(e.target.value)}
1188
- style={{
1189
- padding: '8px',
1190
- border: 'none',
1191
- borderRadius: '4px',
1192
- fontSize: '14px'
1193
- }}
1194
- >
1195
- <option value="">All Categories</option>
1196
- <option value="electronics">Electronics</option>
1197
- <option value="tools">Tools</option>
1198
- <option value="sports">Sports</option>
1199
- <option value="books">Books</option>
1200
- </select>
1201
- <button type="submit" style={{
1202
- padding: '8px 16px',
1203
- backgroundColor: '#1565c0',
1204
- color: 'white',
1205
- border: 'none',
1206
- borderRadius: '4px',
1207
- cursor: 'pointer',
1208
- fontSize: '14px'
1209
- }}>
1210
- Search
1211
- </button>
1212
- </form>
1213
-
1214
- {user && (
1215
- <span style={{
1216
- color: 'white',
1217
- fontSize: '14px',
1218
- padding: '8px 12px',
1219
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
1220
- borderRadius: '4px'
1221
- }}>
1222
- Welcome, {user.name}!
1223
- </span>
1224
- )}
1225
- </div>
1226
- </nav>
1227
-
1228
- {/* Search Results Content */}
1229
- <div style={{ padding: '0 20px', maxWidth: '1200px', margin: '0 auto' }}>
1230
- <div style={{ marginBottom: '20px' }}>
1231
- <h1 style={{ color: '#1976d2', marginBottom: '10px' }}>
1232
- Search Results {searchQuery && `for "${searchQuery}"`}
1233
- </h1>
1234
- {selectedCategory && (
1235
- <p style={{ color: '#666', margin: '0' }}>
1236
- Category: {selectedCategory}
1237
- </p>
1238
- )}
1239
- </div>
1240
-
1241
- {isLoading ? (
1242
- <div style={{ textAlign: 'center', padding: '40px' }}>
1243
- <p>Searching for items...</p>
1244
- </div>
1245
- ) : (
1246
- <>
1247
- <div style={{ marginBottom: '20px', color: '#666' }}>
1248
- Found {searchResults.length} item{searchResults.length !== 1 ? 's' : ''}
1249
- </div>
1250
-
1251
- {searchResults.length === 0 ? (
1252
- <div style={{
1253
- backgroundColor: 'white',
1254
- padding: '40px',
1255
- borderRadius: '8px',
1256
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1257
- textAlign: 'center'
1258
- }}>
1259
- <h3 style={{ color: '#666', marginBottom: '10px' }}>No items found</h3>
1260
- <p style={{ color: '#888' }}>
1261
- Try adjusting your search terms or browse all available items.
1262
- </p>
1263
- <button
1264
- onClick={() => onSearchQueryChange('')}
1265
- style={{
1266
- padding: '10px 20px',
1267
- backgroundColor: '#1976d2',
1268
- color: 'white',
1269
- border: 'none',
1270
- borderRadius: '4px',
1271
- cursor: 'pointer',
1272
- marginTop: '15px'
1273
- }}
1274
- >
1275
- Clear Search
1276
- </button>
1277
- </div>
1278
- ) : (
1279
- <div style={{
1280
- display: 'grid',
1281
- gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
1282
- gap: '20px'
1283
- }}>
1284
- {searchResults.map((item) => (
1285
- <div key={item.id} style={{
1286
- backgroundColor: 'white',
1287
- padding: '20px',
1288
- borderRadius: '8px',
1289
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1290
- display: 'flex',
1291
- flexDirection: 'column',
1292
- height: 'fit-content'
1293
- }}>
1294
- {/* Item Image Placeholder */}
1295
- <div style={{
1296
- width: '100%',
1297
- height: '200px',
1298
- backgroundColor: '#e0e0e0',
1299
- borderRadius: '4px',
1300
- marginBottom: '15px',
1301
- display: 'flex',
1302
- alignItems: 'center',
1303
- justifyContent: 'center',
1304
- color: '#666'
1305
- }}>
1306
- {item.imageUrl || item.images?.[0] ? (
1307
- <img src={item.imageUrl || item.images?.[0]} alt={item.name || item.title} style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '4px' }} />
1308
- ) : (
1309
- <span>No Image</span>
1310
- )}
1311
- </div>
1312
-
1313
- <h3 style={{ margin: '0 0 8px 0', color: '#333' }}>{item.name || item.title}</h3>
1314
- <p style={{ margin: '0 0 10px 0', color: '#666', fontSize: '14px', flexGrow: 1 }}>{item.description}</p>
1315
-
1316
- <div style={{ marginBottom: '10px', fontSize: '14px' }}>
1317
- <span style={{ color: '#666' }}>Owner: </span>
1318
- <span style={{ color: '#333', fontWeight: 'bold' }}>{item.owner?.name || item.ownerName || 'Unknown'}</span>
1319
- </div>
1320
-
1321
- <div style={{ display: 'flex', gap: '8px', marginBottom: '15px', fontSize: '12px' }}>
1322
- <span style={{
1323
- padding: '2px 8px',
1324
- borderRadius: '12px',
1325
- backgroundColor: '#4caf50',
1326
- color: 'white'
1327
- }}>
1328
- ${item.pricePerDay}/day
1329
- </span>
1330
- <span style={{
1331
- padding: '2px 8px',
1332
- borderRadius: '12px',
1333
- backgroundColor: '#2196f3',
1334
- color: 'white'
1335
- }}>
1336
- {item.condition}
1337
- </span>
1338
- <span style={{
1339
- padding: '2px 8px',
1340
- borderRadius: '12px',
1341
- backgroundColor: '#ff9800',
1342
- color: 'white'
1343
- }}>
1344
- {item.category}
1345
- </span>
1346
- </div>
1347
-
1348
- <button
1349
- onClick={(e) => {
1350
- e.preventDefault();
1351
- e.stopPropagation();
1352
- console.log('🔥🔥🔥 BUTTON CLICKED! Item:', item);
1353
- console.log('🔥🔥🔥 onBookItem function:', onBookItem);
1354
- console.log('🔥🔥🔥 item.id:', item.id, 'item.name:', item.name);
1355
- try {
1356
- onBookItem(item.id, item.name || item.title);
1357
- } catch (error) {
1358
- console.error('🔥🔥🔥 Error calling onBookItem:', error);
1359
- }
1360
- }}
1361
- style={{
1362
- width: '100%',
1363
- padding: '10px',
1364
- backgroundColor: '#1976d2',
1365
- color: 'white',
1366
- border: 'none',
1367
- borderRadius: '4px',
1368
- cursor: 'pointer',
1369
- fontSize: '14px',
1370
- fontWeight: 'bold'
1371
- }}
1372
- >
1373
- Book This Item
1374
- </button>
1375
- </div>
1376
- ))}
1377
- </div>
1378
- )}
1379
- </>
1380
- )}
1381
-
1382
- <div style={{ marginTop: '30px', textAlign: 'center' }}>
1383
- <button onClick={onBack} style={{
1384
- padding: '12px 24px',
1385
- backgroundColor: '#757575',
1386
- color: 'white',
1387
- border: 'none',
1388
- borderRadius: '4px',
1389
- cursor: 'pointer',
1390
- fontSize: '16px'
1391
- }}>
1392
- Back to Home
1393
- </button>
1394
- </div>
1395
- </div>
1396
- </div>
1397
- );
1398
- };
1399
-
1400
- type Page = 'home' | 'login' | 'register' | 'browse' | 'search' | 'dashboard' | 'my-items' | 'my-bookings' | 'add-item' | 'book-item';
1401
-
1402
- const BookItemPage: React.FC<PageProps & { item: any }> = ({ onBack, item }) => {
1403
- const { user } = useAuth();
1404
- const [isLoading, setIsLoading] = React.useState(false);
1405
- const [formData, setFormData] = React.useState({
1406
- startDate: '',
1407
- endDate: '',
1408
- bookingNotes: '',
1409
- depositAmount: 0,
1410
- acceptTerms: false
1411
- });
1412
-
1413
- const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
1414
- const { name, value, type } = e.target;
1415
- setFormData(prev => ({
1416
- ...prev,
1417
- [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
1418
- }));
1419
- };
1420
-
1421
- const handleSubmit = async (e: React.FormEvent) => {
1422
- e.preventDefault();
1423
-
1424
- if (!formData.acceptTerms) {
1425
- alert('Please accept the terms and conditions');
1426
- return;
1427
- }
1428
-
1429
- if (new Date(formData.startDate) <= new Date()) {
1430
- alert('Start date must be in the future');
1431
- return;
1432
- }
1433
-
1434
- if (new Date(formData.endDate) <= new Date(formData.startDate)) {
1435
- alert('End date must be after start date');
1436
- return;
1437
- }
1438
-
1439
- setIsLoading(true);
1440
-
1441
- try {
1442
- console.log('🚀 Creating booking for item:', item.id, 'user:', user?.id);
1443
-
1444
- const startDateTime = new Date(formData.startDate + 'T10:00:00');
1445
- const endDateTime = new Date(formData.endDate + 'T18:00:00');
1446
-
1447
- const bookingData = {
1448
- itemId: item.id,
1449
- startDate: startDateTime.toISOString(),
1450
- endDate: endDateTime.toISOString(),
1451
- bookingNotes: formData.bookingNotes || '',
1452
- depositAmount: parseFloat(formData.depositAmount.toString()) || 0,
1453
- requestedDurationDays: Math.ceil((endDateTime.getTime() - startDateTime.getTime()) / (1000 * 60 * 60 * 24)),
1454
- acceptTerms: formData.acceptTerms
1455
- };
1456
-
1457
- console.log('📝 Booking data to send:', bookingData);
1458
-
1459
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings`, {
1460
- method: 'POST',
1461
- headers: {
1462
- 'Content-Type': 'application/json',
1463
- 'X-User-Id': user?.id?.toString() || ''
1464
- },
1465
- body: JSON.stringify(bookingData)
1466
- });
1467
-
1468
- console.log('📡 Booking response status:', response.status);
1469
-
1470
- if (response.ok) {
1471
- const bookingResponse = await response.json();
1472
- console.log('✅ Booking created successfully:', bookingResponse);
1473
- alert(`✅ Booking request sent for "${item.name}"! You can track it in your dashboard.`);
1474
- onBack(); // Navigate back to previous page
1475
- } else {
1476
- const errorText = await response.text();
1477
- console.error('❌ Booking failed:', response.status, errorText);
1478
-
1479
- let errorMessage;
1480
- switch (response.status) {
1481
- case 400:
1482
- errorMessage = 'Invalid booking data. Please check your information and try again.';
1483
- break;
1484
- case 401:
1485
- errorMessage = 'Not authenticated. Please log in again.';
1486
- break;
1487
- case 403:
1488
- errorMessage = 'Not authorized to create booking.';
1489
- break;
1490
- case 404:
1491
- errorMessage = 'Booking endpoint not found. Backend may not be running properly.';
1492
- break;
1493
- case 500:
1494
- errorMessage = 'Server error. Please try again later.';
1495
- break;
1496
- default:
1497
- errorMessage = `Server returned ${response.status}: ${errorText}`;
1498
- }
1499
-
1500
- alert(`❌ Failed to create booking: ${errorMessage}`);
1501
- }
1502
- } catch (error) {
1503
- console.error('💥 Error creating booking:', error);
1504
- alert(`❌ Network error creating booking: ${error}`);
1505
- } finally {
1506
- setIsLoading(false);
1507
- }
1508
- };
1509
-
1510
- if (!item) {
1511
- return (
1512
- <div style={{ padding: '20px', textAlign: 'center' }}>
1513
- <h2>Item not found</h2>
1514
- <button onClick={onBack} style={{ padding: '10px 20px', backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
1515
- Back
1516
- </button>
1517
- </div>
1518
- );
1519
- }
1520
-
1521
- return (
1522
- <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
1523
- <div style={{ marginBottom: '20px' }}>
1524
- <button onClick={onBack} style={{ padding: '8px 16px', backgroundColor: '#666', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
1525
- ← Back
1526
- </button>
1527
- </div>
1528
-
1529
- <div style={{ backgroundColor: 'white', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', marginBottom: '20px' }}>
1530
- <h2 style={{ color: '#1976d2', marginBottom: '10px' }}>Request to Book Item</h2>
1531
- <div style={{ display: 'flex', alignItems: 'center', marginBottom: '20px' }}>
1532
- {item.images?.[0] && (
1533
- <img src={item.images[0]} alt={item.name} style={{ width: '60px', height: '60px', objectFit: 'cover', borderRadius: '4px', marginRight: '15px' }} />
1534
- )}
1535
- <div>
1536
- <h3 style={{ margin: '0 0 5px 0', color: '#333' }}>{item.name}</h3>
1537
- <p style={{ margin: '0', color: '#666', fontSize: '14px' }}>{item.description}</p>
1538
- <p style={{ margin: '5px 0 0 0', color: '#1976d2', fontSize: '14px', fontWeight: 'bold' }}>Owner: {item.ownerName}</p>
1539
- </div>
1540
- </div>
1541
- </div>
1542
-
1543
- <form onSubmit={handleSubmit} style={{ backgroundColor: 'white', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
1544
- <div style={{ marginBottom: '20px' }}>
1545
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
1546
- Start Date *
1547
- </label>
1548
- <input
1549
- type="date"
1550
- name="startDate"
1551
- value={formData.startDate}
1552
- onChange={handleChange}
1553
- required
1554
- min={new Date().toISOString().split('T')[0]}
1555
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px' }}
1556
- />
1557
- </div>
1558
-
1559
- <div style={{ marginBottom: '20px' }}>
1560
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
1561
- End Date *
1562
- </label>
1563
- <input
1564
- type="date"
1565
- name="endDate"
1566
- value={formData.endDate}
1567
- onChange={handleChange}
1568
- required
1569
- min={formData.startDate || new Date().toISOString().split('T')[0]}
1570
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px' }}
1571
- />
1572
- </div>
1573
-
1574
- <div style={{ marginBottom: '20px' }}>
1575
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
1576
- Deposit Amount (Optional)
1577
- </label>
1578
- <input
1579
- type="number"
1580
- name="depositAmount"
1581
- value={formData.depositAmount}
1582
- onChange={handleChange}
1583
- min="0"
1584
- step="0.01"
1585
- placeholder="0.00"
1586
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px' }}
1587
- />
1588
- </div>
1589
-
1590
- <div style={{ marginBottom: '20px' }}>
1591
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
1592
- Notes to Owner (Optional)
1593
- </label>
1594
- <textarea
1595
- name="bookingNotes"
1596
- value={formData.bookingNotes}
1597
- onChange={handleChange}
1598
- rows={4}
1599
- placeholder="Any special requests or information for the owner..."
1600
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px', resize: 'vertical' }}
1601
- />
1602
- </div>
1603
-
1604
- <div style={{ marginBottom: '20px' }}>
1605
- <label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
1606
- <input
1607
- type="checkbox"
1608
- name="acceptTerms"
1609
- checked={formData.acceptTerms}
1610
- onChange={handleChange}
1611
- required
1612
- style={{ marginRight: '8px' }}
1613
- />
1614
- <span style={{ color: '#333' }}>I accept the terms and conditions for booking this item *</span>
1615
- </label>
1616
- </div>
1617
-
1618
- <div style={{ display: 'flex', gap: '10px' }}>
1619
- <button
1620
- type="button"
1621
- onClick={onBack}
1622
- style={{
1623
- flex: 1,
1624
- padding: '12px',
1625
- backgroundColor: '#666',
1626
- color: 'white',
1627
- border: 'none',
1628
- borderRadius: '4px',
1629
- cursor: 'pointer',
1630
- fontSize: '16px'
1631
- }}
1632
- >
1633
- Cancel
1634
- </button>
1635
- <button
1636
- type="submit"
1637
- disabled={isLoading || !formData.acceptTerms}
1638
- style={{
1639
- flex: 2,
1640
- padding: '12px',
1641
- backgroundColor: isLoading || !formData.acceptTerms ? '#ccc' : '#1976d2',
1642
- color: 'white',
1643
- border: 'none',
1644
- borderRadius: '4px',
1645
- cursor: isLoading || !formData.acceptTerms ? 'not-allowed' : 'pointer',
1646
- fontSize: '16px',
1647
- fontWeight: 'bold'
1648
- }}
1649
- >
1650
- {isLoading ? 'Creating Booking...' : 'Send Booking Request'}
1651
- </button>
1652
- </div>
1653
- </form>
1654
- </div>
1655
- );
1656
- };
1657
-
1658
- const App = () => {
1659
- const { user, isAuthenticated, logout } = useAuth();
1660
- const [searchQuery, setSearchQuery] = useState('');
1661
- const [currentPage, setCurrentPage] = useState<Page>('home');
1662
- const [selectedItem, setSelectedItem] = useState<any>(null);
1663
-
1664
- const handleLogin = () => {
1665
- setCurrentPage('login');
1666
- };
1667
-
1668
- const handleRegister = () => {
1669
- setCurrentPage('register');
1670
- };
1671
-
1672
- const handleBrowse = () => {
1673
- setCurrentPage('browse');
1674
- };
1675
-
1676
- const handleDashboard = () => {
1677
- setCurrentPage('dashboard');
1678
- };
1679
-
1680
- const handleMyItems = () => {
1681
- setCurrentPage('my-items');
1682
- };
1683
-
1684
- const handleMyBookings = () => {
1685
- setCurrentPage('my-bookings');
1686
- };
1687
-
1688
- const handleAddItem = () => {
1689
- setCurrentPage('add-item');
1690
- };
1691
-
1692
- const handleBookItem = (itemId: string, itemTitle: string) => {
1693
- console.log('🎯🎯🎯 NEW HANDLE BOOK ITEM CALLED - Item ID:', itemId, 'Item Title:', itemTitle);
1694
- console.log('🎯🎯🎯 User:', user);
1695
-
1696
- if (!user) {
1697
- console.log('🎯🎯🎯 No user - showing login alert');
1698
- alert('Please log in to book items');
1699
- return;
1700
- }
1701
-
1702
- console.log('🎯🎯🎯 Setting selected item and navigating to book-item page');
1703
- // Find the full item object from search results or fetch it
1704
- setSelectedItem({ id: itemId, name: itemTitle });
1705
- setCurrentPage('book-item');
1706
- };
1707
-
1708
- const handleLogout = () => {
1709
- logout();
1710
- setCurrentPage('home');
1711
- console.log('User logged out');
1712
- };
1713
-
1714
- const handleSearch = (e: React.FormEvent) => {
1715
- e.preventDefault();
1716
- if (searchQuery.trim()) {
1717
- setCurrentPage('search');
1718
- }
1719
- };
1720
-
1721
- const navigateToHome = () => {
1722
- setCurrentPage('home');
1723
- };
1724
-
1725
- // Render different pages based on current page state
1726
- if (currentPage === 'login') {
1727
- return <LoginPage onBack={navigateToHome} />;
1728
- }
1729
-
1730
- if (currentPage === 'register') {
1731
- return <RegisterPage onBack={navigateToHome} />;
1732
- }
1733
-
1734
- if (currentPage === 'browse') {
1735
- return <BrowsePage onBack={navigateToHome} />;
1736
- }
1737
-
1738
- if (currentPage === 'search') {
1739
- return <SearchResultsPage onBack={navigateToHome} searchQuery={searchQuery} onSearchQueryChange={setSearchQuery} onBookItem={handleBookItem} />;
1740
- }
1741
-
1742
- if (currentPage === 'dashboard') {
1743
- return <DashboardPage onBack={navigateToHome} onAddItem={handleAddItem} />;
1744
- }
1745
-
1746
- if (currentPage === 'my-items') {
1747
- return <MyItemsPage onBack={navigateToHome} />;
1748
- }
1749
-
1750
- if (currentPage === 'my-bookings') {
1751
- return <MyBookingsPage onBack={navigateToHome} />;
1752
- }
1753
-
1754
- if (currentPage === 'add-item') {
1755
- return <AddItemPage onBack={navigateToHome} />;
1756
- }
1757
-
1758
- if (currentPage === 'book-item') {
1759
- return <BookItemPage onBack={() => setCurrentPage('search')} item={selectedItem} />;
1760
- }
1761
-
1762
- return (
1763
- <div style={{
1764
- fontFamily: 'Roboto, Arial, sans-serif',
1765
- margin: '0',
1766
- padding: '0',
1767
- backgroundColor: '#ffffff',
1768
- color: '#213547',
1769
- minHeight: '100vh'
1770
- }}>
1771
- {/* Navigation Bar */}
1772
- <nav style={{
1773
- backgroundColor: '#1976d2',
1774
- color: 'white',
1775
- padding: '12px 0',
1776
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1777
- marginBottom: '0'
1778
- }}>
1779
- <div style={{
1780
- maxWidth: '1200px',
1781
- margin: '0 auto',
1782
- padding: '0 20px',
1783
- display: 'flex',
1784
- alignItems: 'center',
1785
- justifyContent: 'space-between',
1786
- gap: '20px'
1787
- }}>
1788
- {/* Brand Logo */}
1789
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
1790
- <button
1791
- onClick={navigateToHome}
1792
- style={{
1793
- color: 'white',
1794
- textDecoration: 'none',
1795
- background: 'none',
1796
- border: 'none',
1797
- cursor: 'pointer',
1798
- fontSize: '1.5rem',
1799
- fontWeight: 'bold'
1800
- }}
1801
- >
1802
- LocalLend
1803
- </button>
1804
- </div>
1805
-
1806
- {/* Search Bar */}
1807
- <form onSubmit={handleSearch} style={{
1808
- display: 'flex',
1809
- alignItems: 'center',
1810
- gap: '8px',
1811
- flex: 1,
1812
- maxWidth: '500px'
1813
- }}>
1814
- <input
1815
- type="search"
1816
- placeholder="Search items..."
1817
- value={searchQuery}
1818
- onChange={(e) => setSearchQuery(e.target.value)}
1819
- style={{
1820
- flex: 1,
1821
- padding: '8px 12px',
1822
- border: 'none',
1823
- borderRadius: '4px',
1824
- fontSize: '14px'
1825
- }}
1826
- />
1827
- <select style={{
1828
- padding: '8px',
1829
- border: 'none',
1830
- borderRadius: '4px',
1831
- fontSize: '14px'
1832
- }}>
1833
- <option value="">All Categories</option>
1834
- <option value="electronics">Electronics</option>
1835
- <option value="tools">Tools</option>
1836
- <option value="sports">Sports</option>
1837
- <option value="books">Books</option>
1838
- </select>
1839
- <button type="submit" style={{
1840
- padding: '8px 16px',
1841
- backgroundColor: '#1565c0',
1842
- color: 'white',
1843
- border: 'none',
1844
- borderRadius: '4px',
1845
- cursor: 'pointer',
1846
- fontSize: '14px'
1847
- }}>
1848
- Search
1849
- </button>
1850
- </form>
1851
-
1852
- {/* Navigation Menu */}
1853
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
1854
- {isAuthenticated && user ? (
1855
- <>
1856
- <span style={{
1857
- color: 'white',
1858
- fontSize: '14px',
1859
- padding: '8px 12px',
1860
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
1861
- borderRadius: '4px'
1862
- }}>
1863
- Welcome, {user.name}!
1864
- </span>
1865
- <button onClick={handleDashboard} style={{ color: 'white', textDecoration: 'none', padding: '8px 12px', background: 'none', border: 'none', cursor: 'pointer' }}>
1866
- Dashboard
1867
- </button>
1868
- <button onClick={handleMyItems} style={{ color: 'white', textDecoration: 'none', padding: '8px 12px', background: 'none', border: 'none', cursor: 'pointer' }}>
1869
- My Items
1870
- </button>
1871
- <button onClick={handleMyBookings} style={{ color: 'white', textDecoration: 'none', padding: '8px 12px', background: 'none', border: 'none', cursor: 'pointer' }}>
1872
- Bookings
1873
- </button>
1874
- <button
1875
- onClick={handleLogout}
1876
- style={{
1877
- padding: '8px 16px',
1878
- backgroundColor: '#d32f2f',
1879
- color: 'white',
1880
- border: 'none',
1881
- borderRadius: '4px',
1882
- cursor: 'pointer',
1883
- fontSize: '14px'
1884
- }}
1885
- >
1886
- Logout
1887
- </button>
1888
- </>
1889
- ) : (
1890
- <>
1891
- <button
1892
- onClick={handleBrowse}
1893
- style={{
1894
- color: 'white',
1895
- textDecoration: 'none',
1896
- padding: '8px 12px',
1897
- background: 'none',
1898
- border: 'none',
1899
- cursor: 'pointer'
1900
- }}
1901
- >
1902
- Browse Items
1903
- </button>
1904
- <button
1905
- onClick={handleLogin}
1906
- style={{
1907
- padding: '8px 16px',
1908
- backgroundColor: 'transparent',
1909
- color: 'white',
1910
- border: '1px solid white',
1911
- borderRadius: '4px',
1912
- cursor: 'pointer',
1913
- fontSize: '14px'
1914
- }}
1915
- >
1916
- Login
1917
- </button>
1918
- <button
1919
- onClick={handleRegister}
1920
- style={{
1921
- padding: '8px 16px',
1922
- backgroundColor: '#4caf50',
1923
- color: 'white',
1924
- border: 'none',
1925
- borderRadius: '4px',
1926
- cursor: 'pointer',
1927
- fontSize: '14px'
1928
- }}
1929
- >
1930
- Register
1931
- </button>
1932
- </>
1933
- )}
1934
- </div>
1935
- </div>
1936
- </nav>
1937
-
1938
- {/* Main Content */}
1939
- <div style={{ padding: '20px' }}>
1940
- <header style={{
1941
- backgroundColor: '#1976d2',
1942
- color: 'white',
1943
- padding: '30px',
1944
- borderRadius: '12px',
1945
- marginBottom: '30px',
1946
- textAlign: 'center',
1947
- boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
1948
- }}>
1949
- <h1 style={{ margin: '0 0 10px 0', fontSize: '2.5rem' }}>LocalLend - Peer-to-Peer Item Sharing</h1>
1950
- <p style={{ margin: '0', fontSize: '1.2rem', opacity: '0.9' }}>A community platform for borrowing and lending items</p>
1951
- </header>
1952
-
1953
- <main style={{ maxWidth: '1200px', margin: '0 auto' }}>
1954
- <section style={{
1955
- marginBottom: '40px',
1956
- backgroundColor: '#ffffff',
1957
- padding: '30px',
1958
- borderRadius: '12px',
1959
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
1960
- }}>
1961
- <h2 style={{ color: '#1976d2', marginBottom: '20px' }}>Welcome to LocalLend</h2>
1962
- <p style={{ fontSize: '1.1rem', lineHeight: '1.6', color: '#424242' }}>
1963
- LocalLend is a peer-to-peer item sharing platform where users can:
1964
- </p>
1965
- <ul style={{ fontSize: '1rem', lineHeight: '1.8', color: '#424242' }}>
1966
- <li>List items they own for others to borrow</li>
1967
- <li>Browse and search items available in their community</li>
1968
- <li>Request to borrow items from other users</li>
1969
- <li>Manage bookings (approve, track, complete)</li>
1970
- <li>Rate users and items after completed transactions</li>
1971
- <li>Build trust scores through positive interactions</li>
1972
- </ul>
1973
- </section>
1974
-
1975
- <section style={{
1976
- backgroundColor: '#f8f9fa',
1977
- padding: '30px',
1978
- borderRadius: '12px',
1979
- marginBottom: '30px',
1980
- border: '1px solid #e9ecef'
1981
- }}>
1982
- <h3 style={{ color: '#1976d2', marginBottom: '25px' }}>Application Features (In Development)</h3>
1983
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '20px' }}>
1984
- <div style={{
1985
- backgroundColor: 'white',
1986
- padding: '20px',
1987
- borderRadius: '8px',
1988
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1989
- border: '1px solid #e9ecef'
1990
- }}>
1991
- <h4 style={{ color: '#333', marginBottom: '10px' }}>🔐 Authentication</h4>
1992
- <p style={{ color: '#666', margin: '0' }}>User registration, login, JWT token management</p>
1993
- </div>
1994
- <div style={{
1995
- backgroundColor: 'white',
1996
- padding: '20px',
1997
- borderRadius: '8px',
1998
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1999
- border: '1px solid #e9ecef'
2000
- }}>
2001
- <h4 style={{ color: '#333', marginBottom: '10px' }}>📱 Item Management</h4>
2002
- <p style={{ color: '#666', margin: '0' }}>Create, edit, browse items with categories and conditions</p>
2003
- </div>
2004
- <div style={{
2005
- backgroundColor: 'white',
2006
- padding: '20px',
2007
- borderRadius: '8px',
2008
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
2009
- border: '1px solid #e9ecef'
2010
- }}>
2011
- <h4 style={{ color: '#333', marginBottom: '10px' }}>📅 Booking System</h4>
2012
- <p style={{ color: '#666', margin: '0' }}>Request, approve, track borrowing periods</p>
2013
- </div>
2014
- <div style={{
2015
- backgroundColor: 'white',
2016
- padding: '20px',
2017
- borderRadius: '8px',
2018
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
2019
- border: '1px solid #e9ecef'
2020
- }}>
2021
- <h4 style={{ color: '#333', marginBottom: '10px' }}>⭐ Rating System</h4>
2022
- <p style={{ color: '#666', margin: '0' }}>Rate users and items to build community trust</p>
2023
- </div>
2024
- </div>
2025
- </section>
2026
- </main>
2027
-
2028
- <footer style={{
2029
- marginTop: '50px',
2030
- textAlign: 'center',
2031
- color: '#666',
2032
- borderTop: '2px solid #e9ecef',
2033
- paddingTop: '30px',
2034
- backgroundColor: '#f8f9fa',
2035
- marginLeft: '-20px',
2036
- marginRight: '-20px',
2037
- padding: '30px 20px'
2038
- }}>
2039
- <p style={{ margin: '10px 0', fontSize: '1rem' }}>LocalLend Frontend - Built with React + TypeScript + Vite</p>
2040
- <p style={{ margin: '10px 0', fontSize: '0.9rem' }}>Backend API: http://localhost:8080</p>
2041
- <p style={{ margin: '10px 0', fontSize: '0.9rem' }}>Frontend: http://localhost:5173</p>
2042
- </footer>
2043
- </div>
2044
- </div>
2045
- );
2046
- };
2047
-
2048
- export default App;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/assets/react.svg DELETED
frontend/src/components/Navbar.tsx DELETED
@@ -1,217 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import {
3
- AppBar,
4
- Toolbar,
5
- Typography,
6
- Button,
7
- IconButton,
8
- Menu,
9
- MenuItem,
10
- TextField,
11
- InputAdornment,
12
- Box,
13
- Avatar,
14
- Badge,
15
- Select,
16
- FormControl,
17
- InputLabel
18
- } from '@mui/material';
19
- import {
20
- Search as SearchIcon,
21
- Notifications as NotificationsIcon,
22
- ExitToApp as LogoutIcon
23
- } from '@mui/icons-material';
24
- import { useNavigate, useLocation, Link } from 'react-router-dom';
25
- import { useAuth } from '../context/AuthContext';
26
- import { categoryService } from '../services/categoryService';
27
- import type { Category } from '../types';
28
-
29
- export const Navbar: React.FC = () => {
30
- const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
31
- const [searchQuery, setSearchQuery] = useState('');
32
- const [selectedCategory, setSelectedCategory] = useState('');
33
- const [categories, setCategories] = useState<Category[]>([]);
34
-
35
- const { user, isAuthenticated, logout } = useAuth();
36
- const navigate = useNavigate();
37
- const _location = useLocation();
38
-
39
- useEffect(() => {
40
- const fetchCategories = async () => {
41
- try {
42
- const response = await categoryService.getCategories();
43
- console.log('Categories API response:', response);
44
-
45
- // The API returns { data: Category[], success: true, count: number, message: string }
46
- if (response && response.data) {
47
- console.log('Categories from response.data:', response.data);
48
- console.log('Number of categories:', response.data.length);
49
- setCategories(response.data);
50
- } else {
51
- console.warn('No categories data in response:', response);
52
- setCategories([]);
53
- }
54
- } catch (error) {
55
- console.error('Failed to fetch categories:', error);
56
- }
57
- };
58
-
59
- fetchCategories();
60
- }, []);
61
-
62
- const handleUserMenuClick = (event: React.MouseEvent<HTMLElement>) => {
63
- setAnchorEl(event.currentTarget);
64
- };
65
-
66
- const handleUserMenuClose = () => {
67
- setAnchorEl(null);
68
- };
69
-
70
- const handleLogout = () => {
71
- logout();
72
- handleUserMenuClose();
73
- navigate('/');
74
- };
75
-
76
- const handleSearch = (e: React.FormEvent) => {
77
- e.preventDefault();
78
- if (searchQuery.trim()) {
79
- const searchParams = new URLSearchParams();
80
- searchParams.set('query', searchQuery);
81
- if (selectedCategory) {
82
- searchParams.set('categoryId', selectedCategory);
83
- }
84
- navigate(`/?${searchParams.toString()}`);
85
- }
86
- };
87
-
88
- const isOpen = Boolean(anchorEl);
89
-
90
- return (
91
- <AppBar position="static">
92
- <Toolbar>
93
- <Typography
94
- variant="h6"
95
- component={Link}
96
- to="/"
97
- sx={{
98
- flexGrow: 0,
99
- mr: 4,
100
- textDecoration: 'none',
101
- color: 'inherit',
102
- fontWeight: 'bold'
103
- }}
104
- >
105
- LocalLend
106
- </Typography>
107
-
108
- <Box component="form" onSubmit={handleSearch} sx={{ display: 'flex', alignItems: 'center', flexGrow: 1, maxWidth: 600 }}>
109
- <TextField
110
- size="small"
111
- placeholder="Search items..."
112
- value={searchQuery}
113
- onChange={(e) => setSearchQuery(e.target.value)}
114
- InputProps={{
115
- startAdornment: (
116
- <InputAdornment position="start">
117
- <SearchIcon />
118
- </InputAdornment>
119
- ),
120
- }}
121
- sx={{ flexGrow: 1, mr: 1, bgcolor: 'background.paper', borderRadius: 1 }}
122
- />
123
-
124
- <FormControl size="small" sx={{ minWidth: 120, mr: 1 }}>
125
- <InputLabel>Category</InputLabel>
126
- <Select
127
- value={selectedCategory}
128
- onChange={(e) => setSelectedCategory(e.target.value)}
129
- label="Category"
130
- sx={{ bgcolor: 'background.paper' }}
131
- >
132
- <MenuItem value="">All Categories</MenuItem>
133
- {categories.map((category) => (
134
- <MenuItem key={category.id} value={category.id}>
135
- {category.name}
136
- </MenuItem>
137
- ))}
138
- </Select>
139
- </FormControl>
140
-
141
- <Button
142
- type="submit"
143
- variant="contained"
144
- color="secondary"
145
- startIcon={<SearchIcon />}
146
- >
147
- Search
148
- </Button>
149
- </Box>
150
-
151
- <Box sx={{ display: 'flex', alignItems: 'center', ml: 2 }}>
152
- {isAuthenticated ? (
153
- <>
154
- <IconButton color="inherit" sx={{ mr: 1 }}>
155
- <Badge badgeContent={0} color="error">
156
- <NotificationsIcon />
157
- </Badge>
158
- </IconButton>
159
-
160
- <IconButton
161
- color="inherit"
162
- onClick={handleUserMenuClick}
163
- sx={{ p: 0 }}
164
- >
165
- <Avatar alt={user?.name} src={user?.profileImageUrl}>
166
- {user?.name?.[0]?.toUpperCase()}
167
- </Avatar>
168
- </IconButton>
169
-
170
- <Menu
171
- anchorEl={anchorEl}
172
- open={isOpen}
173
- onClose={handleUserMenuClose}
174
- transformOrigin={{ horizontal: 'right', vertical: 'top' }}
175
- anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
176
- >
177
- <MenuItem onClick={handleUserMenuClose}>
178
- <Link to="/dashboard" style={{ textDecoration: 'none', color: 'inherit' }}>
179
- Dashboard
180
- </Link>
181
- </MenuItem>
182
- <MenuItem onClick={handleUserMenuClose}>
183
- <Link to="/profile" style={{ textDecoration: 'none', color: 'inherit' }}>
184
- Profile
185
- </Link>
186
- </MenuItem>
187
- <MenuItem onClick={handleLogout}>
188
- <LogoutIcon sx={{ mr: 1 }} />
189
- Logout
190
- </MenuItem>
191
- </Menu>
192
- </>
193
- ) : (
194
- <>
195
- <Button
196
- color="inherit"
197
- component={Link}
198
- to="/login"
199
- sx={{ mr: 1 }}
200
- >
201
- Login
202
- </Button>
203
- <Button
204
- variant="outlined"
205
- color="inherit"
206
- component={Link}
207
- to="/register"
208
- >
209
- Register
210
- </Button>
211
- </>
212
- )}
213
- </Box>
214
- </Toolbar>
215
- </AppBar>
216
- );
217
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/context/AuthContext.tsx DELETED
@@ -1,103 +0,0 @@
1
- import React, { createContext, useContext, useState, useEffect } from 'react';
2
- import type { ReactNode } from 'react';
3
- import { authService } from '../services/authService';
4
- import type { User, LoginRequest, RegisterRequest } from '../types';
5
-
6
- interface AuthContextType {
7
- user: User | null;
8
- isAuthenticated: boolean;
9
- isLoading: boolean;
10
- login: (credentials: LoginRequest) => Promise<void>;
11
- register: (userData: RegisterRequest) => Promise<void>;
12
- logout: () => void;
13
- }
14
-
15
- const AuthContext = createContext<AuthContextType | undefined>(undefined);
16
-
17
- export const useAuth = (): AuthContextType => {
18
- const context = useContext(AuthContext);
19
- if (!context) {
20
- throw new Error('useAuth must be used within an AuthProvider');
21
- }
22
- return context;
23
- };
24
-
25
- interface AuthProviderProps {
26
- children: ReactNode;
27
- }
28
-
29
- export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
30
- const [user, setUser] = useState<User | null>(null);
31
- const [isLoading, setIsLoading] = useState(true);
32
-
33
- // Check for existing authentication on component mount
34
- useEffect(() => {
35
- const checkAuth = () => {
36
- try {
37
- const currentUser = authService.getCurrentUser();
38
- const isAuth = authService.isAuthenticated();
39
-
40
- if (isAuth && currentUser) {
41
- setUser(currentUser);
42
- }
43
- } catch (error) {
44
- console.error('Error checking authentication:', error);
45
- // Clear invalid token/user data
46
- authService.logout();
47
- } finally {
48
- setIsLoading(false);
49
- }
50
- };
51
-
52
- checkAuth();
53
- }, []);
54
-
55
- const login = async (credentials: LoginRequest): Promise<void> => {
56
- setIsLoading(true);
57
- try {
58
- const response = await authService.login(credentials);
59
- setUser(response.user);
60
- } catch (error) {
61
- throw error;
62
- } finally {
63
- setIsLoading(false);
64
- }
65
- };
66
-
67
- const register = async (userData: RegisterRequest): Promise<void> => {
68
- setIsLoading(true);
69
- try {
70
- await authService.register(userData);
71
- // After successful registration, auto-login the user
72
- const loginResponse = await authService.login({
73
- usernameOrEmail: userData.username,
74
- password: userData.password
75
- });
76
- setUser(loginResponse.user);
77
- } catch (error) {
78
- throw error;
79
- } finally {
80
- setIsLoading(false);
81
- }
82
- };
83
-
84
- const logout = (): void => {
85
- authService.logout();
86
- setUser(null);
87
- };
88
-
89
- const value: AuthContextType = {
90
- user,
91
- isAuthenticated: !!user,
92
- isLoading,
93
- login,
94
- register,
95
- logout
96
- };
97
-
98
- return (
99
- <AuthContext.Provider value={value}>
100
- {children}
101
- </AuthContext.Provider>
102
- );
103
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/index.css DELETED
@@ -1,74 +0,0 @@
1
- :root {
2
- font-family: 'Roboto', system-ui, Avenir, Helvetica, Arial, sans-serif;
3
- line-height: 1.5;
4
- font-weight: 400;
5
-
6
- color-scheme: light dark;
7
- color: #213547;
8
- background-color: #ffffff;
9
-
10
- font-synthesis: none;
11
- text-rendering: optimizeLegibility;
12
- -webkit-font-smoothing: antialiased;
13
- -moz-osx-font-smoothing: grayscale;
14
- }
15
-
16
- a {
17
- font-weight: 500;
18
- color: #646cff;
19
- text-decoration: inherit;
20
- }
21
- a:hover {
22
- color: #535bf2;
23
- }
24
-
25
- body {
26
- margin: 0;
27
- min-width: 320px;
28
- min-height: 100vh;
29
- background-color: #ffffff;
30
- color: #213547;
31
- }
32
-
33
- h1 {
34
- font-size: 3.2em;
35
- line-height: 1.1;
36
- }
37
-
38
- button {
39
- border-radius: 8px;
40
- border: 1px solid transparent;
41
- padding: 0.6em 1.2em;
42
- font-size: 1em;
43
- font-weight: 500;
44
- font-family: inherit;
45
- background-color: #f9f9f9;
46
- color: #213547;
47
- cursor: pointer;
48
- transition: border-color 0.25s;
49
- }
50
- button:hover {
51
- border-color: #646cff;
52
- }
53
- button:focus,
54
- button:focus-visible {
55
- outline: 4px auto -webkit-focus-ring-color;
56
- }
57
-
58
- @media (prefers-color-scheme: dark) {
59
- :root {
60
- color: rgba(255, 255, 255, 0.87);
61
- background-color: #242424;
62
- }
63
- body {
64
- background-color: #242424;
65
- color: rgba(255, 255, 255, 0.87);
66
- }
67
- a:hover {
68
- color: #747bff;
69
- }
70
- button {
71
- background-color: #1a1a1a;
72
- color: rgba(255, 255, 255, 0.87);
73
- }
74
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/main.tsx DELETED
@@ -1,13 +0,0 @@
1
- import { StrictMode } from 'react'
2
- import { createRoot } from 'react-dom/client'
3
- import './index.css'
4
- import App from './App.tsx'
5
- import { AuthProvider } from './context/AuthContext'
6
-
7
- createRoot(document.getElementById('root')!).render(
8
- <StrictMode>
9
- <AuthProvider>
10
- <App />
11
- </AuthProvider>
12
- </StrictMode>,
13
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/AddItemPage.tsx DELETED
@@ -1,651 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useAuth } from '../context/AuthContext';
3
-
4
- interface AddItemPageProps {
5
- onBack: () => void;
6
- }
7
-
8
- export const AddItemPage: React.FC<AddItemPageProps> = ({ onBack }) => {
9
- const { user } = useAuth();
10
- const [isLoading, setIsLoading] = useState(false);
11
- const [categories, setCategories] = useState<any[]>([]);
12
- const [formData, setFormData] = useState({
13
- title: '',
14
- description: '',
15
- category: '',
16
- condition: 'GOOD',
17
- deposit: ''
18
- });
19
-
20
- // Image upload states
21
- const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
22
- const [imagePreviews, setImagePreviews] = useState<string[]>([]);
23
- const [uploadedImageUrls, setUploadedImageUrls] = useState<string[]>([]);
24
- const [isUploadingImages, setIsUploadingImages] = useState(false);
25
-
26
- // Load categories from backend on component mount
27
- useEffect(() => {
28
- const fetchCategories = async () => {
29
- try {
30
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/categories`);
31
- if (response.ok) {
32
- const categoryResponse = await response.json();
33
- console.log('Categories loaded:', categoryResponse);
34
- // Extract the data array from the response
35
- const categoryData = categoryResponse.data || categoryResponse;
36
- setCategories(categoryData);
37
- } else {
38
- console.error('Failed to load categories');
39
- // Fallback categories if API fails
40
- setCategories([
41
- { id: '1', name: 'Electronics' },
42
- { id: '2', name: 'Tools' },
43
- { id: '3', name: 'Sports' },
44
- { id: '4', name: 'Books' },
45
- { id: '5', name: 'Furniture' },
46
- { id: '6', name: 'Vehicles' },
47
- { id: '7', name: 'Appliances' },
48
- { id: '8', name: 'Other' }
49
- ]);
50
- }
51
- } catch (error) {
52
- console.error('Error fetching categories:', error);
53
- // Fallback categories if API fails
54
- setCategories([
55
- { id: '1', name: 'Electronics' },
56
- { id: '2', name: 'Tools' },
57
- { id: '3', name: 'Sports' },
58
- { id: '4', name: 'Books' },
59
- { id: '5', name: 'Furniture' },
60
- { id: '6', name: 'Vehicles' },
61
- { id: '7', name: 'Appliances' },
62
- { id: '8', name: 'Other' }
63
- ]);
64
- }
65
- };
66
-
67
- fetchCategories();
68
- }, []);
69
-
70
- const conditions = [
71
- { value: 'NEW', label: 'New' },
72
- { value: 'EXCELLENT', label: 'Excellent - Like new' },
73
- { value: 'GOOD', label: 'Good - Minor wear' },
74
- { value: 'FAIR', label: 'Fair - Noticeable wear' },
75
- { value: 'POOR', label: 'Poor - Significant wear' }
76
- ];
77
-
78
- const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
79
- const { name, value } = e.target;
80
- setFormData(prev => ({
81
- ...prev,
82
- [name]: value
83
- }));
84
- };
85
-
86
- // Handle image file selection
87
- const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
88
- const files = e.target.files;
89
- if (!files) return;
90
-
91
- const fileArray = Array.from(files);
92
-
93
- // Validate file count
94
- if (fileArray.length + selectedFiles.length > 5) {
95
- alert('Maximum 5 images allowed per item');
96
- return;
97
- }
98
-
99
- // Validate file types and sizes
100
- const validFiles: File[] = [];
101
- for (const file of fileArray) {
102
- if (!file.type.startsWith('image/')) {
103
- alert(`${file.name} is not an image file`);
104
- continue;
105
- }
106
- if (file.size > 10 * 1024 * 1024) {
107
- alert(`${file.name} exceeds 10MB size limit`);
108
- continue;
109
- }
110
- validFiles.push(file);
111
- }
112
-
113
- if (validFiles.length === 0) return;
114
-
115
- // Update selected files
116
- setSelectedFiles(prev => [...prev, ...validFiles]);
117
-
118
- // Generate previews
119
- validFiles.forEach(file => {
120
- const reader = new FileReader();
121
- reader.onloadend = () => {
122
- setImagePreviews(prev => [...prev, reader.result as string]);
123
- };
124
- reader.readAsDataURL(file);
125
- });
126
- };
127
-
128
- // Remove an image from selection
129
- const handleRemoveImage = (index: number) => {
130
- setSelectedFiles(prev => prev.filter((_, i) => i !== index));
131
- setImagePreviews(prev => prev.filter((_, i) => i !== index));
132
- setUploadedImageUrls(prev => prev.filter((_, i) => i !== index));
133
- };
134
-
135
- // Upload images to backend
136
- const uploadImages = async (): Promise<string[]> => {
137
- if (selectedFiles.length === 0) return [];
138
-
139
- setIsUploadingImages(true);
140
- const uploadedUrls: string[] = [];
141
-
142
- try {
143
- // Upload images one by one (or use upload-multiple endpoint)
144
- for (const file of selectedFiles) {
145
- const formData = new FormData();
146
- formData.append('file', file);
147
- formData.append('folder', 'locallend/items');
148
-
149
- const response = await fetch(
150
- `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/images/upload`,
151
- {
152
- method: 'POST',
153
- headers: {
154
- 'Authorization': `Bearer ${localStorage.getItem('token')}`
155
- },
156
- body: formData
157
- }
158
- );
159
-
160
- if (response.ok) {
161
- const result = await response.json();
162
- uploadedUrls.push(result.url);
163
- } else {
164
- console.error('Failed to upload image:', file.name);
165
- throw new Error(`Failed to upload ${file.name}`);
166
- }
167
- }
168
-
169
- setUploadedImageUrls(uploadedUrls);
170
- return uploadedUrls;
171
- } catch (error: any) {
172
- console.error('Image upload error:', error);
173
- throw new Error('Failed to upload images: ' + error.message);
174
- } finally {
175
- setIsUploadingImages(false);
176
- }
177
- };
178
-
179
- const handleSubmit = async (e: React.FormEvent) => {
180
- e.preventDefault();
181
-
182
- if (!user) {
183
- alert('You must be logged in to add items');
184
- return;
185
- }
186
-
187
- // Client-side validation
188
- if (formData.title.length < 3 || formData.title.length > 100) {
189
- alert('Item title must be between 3 and 100 characters long');
190
- return;
191
- }
192
-
193
- if (formData.description.length < 10 || formData.description.length > 500) {
194
- alert('Item description must be between 10 and 500 characters long');
195
- return;
196
- }
197
-
198
- if (!formData.category) {
199
- alert('Please select a category');
200
- return;
201
- }
202
-
203
- if (formData.deposit === '' || isNaN(Number(formData.deposit)) || Number(formData.deposit) < 0) {
204
- alert('Deposit must be a non-negative number');
205
- return;
206
- }
207
-
208
- setIsLoading(true);
209
-
210
- try {
211
- // Upload images first (if any selected)
212
- let imageUrls: string[] = [];
213
- if (selectedFiles.length > 0) {
214
- try {
215
- imageUrls = await uploadImages();
216
- console.log('Images uploaded successfully:', imageUrls);
217
- } catch (uploadError: any) {
218
- alert('Failed to upload images. Please try again.');
219
- setIsLoading(false);
220
- return;
221
- }
222
- }
223
-
224
- // Prepare item data matching the backend CreateItemRequest interface
225
- const itemData = {
226
- name: formData.title, // Backend expects 'name', not 'title'
227
- description: formData.description,
228
- categoryId: formData.category, // Backend expects categoryId
229
- condition: formData.condition,
230
- deposit: Number(formData.deposit) || 0,
231
- images: imageUrls // Include uploaded image URLs
232
- };
233
-
234
- console.log('Submitting item data:', itemData);
235
-
236
- // Use the itemService to create the item (it handles auth headers properly)
237
- const newItem = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items`, {
238
- method: 'POST',
239
- headers: {
240
- 'Content-Type': 'application/json',
241
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
242
- 'X-User-Id': user.id.toString()
243
- },
244
- body: JSON.stringify(itemData)
245
- });
246
-
247
- if (newItem.ok) {
248
- const createdItem = await newItem.json();
249
- console.log('Item created successfully:', createdItem);
250
- alert(`Item "${createdItem.name || formData.title}" added successfully!`);
251
-
252
- // Reset form
253
- setFormData({
254
- title: '',
255
- description: '',
256
- category: '',
257
- condition: 'GOOD',
258
- deposit: ''
259
- });
260
- setSelectedFiles([]);
261
- setImagePreviews([]);
262
- setUploadedImageUrls([]);
263
-
264
- onBack(); // Go back to dashboard/home
265
- } else {
266
- const errorResponse = await newItem.text();
267
- console.error('Server error response:', errorResponse);
268
-
269
- let errorMessage = 'Failed to add item';
270
- try {
271
- const errorData = JSON.parse(errorResponse);
272
- errorMessage = errorData.message || errorData.error || errorMessage;
273
- } catch (e) {
274
- errorMessage = errorResponse || errorMessage;
275
- }
276
-
277
- throw new Error(`Server error (${newItem.status}): ${errorMessage}`);
278
- }
279
- } catch (error: any) {
280
- console.error('Error adding item:', error);
281
-
282
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
283
- alert('Network error: Cannot connect to backend server. Please ensure the backend is running on http://localhost:8080');
284
- } else {
285
- alert(`Error adding item: ${error.message}`);
286
- }
287
- } finally {
288
- setIsLoading(false);
289
- }
290
- };
291
-
292
- return (
293
- <div style={{
294
- fontFamily: 'Roboto, Arial, sans-serif',
295
- margin: '0',
296
- padding: '0',
297
- backgroundColor: '#f5f5f5',
298
- color: '#213547',
299
- minHeight: '100vh'
300
- }}>
301
- {/* Navigation Bar */}
302
- <nav style={{
303
- backgroundColor: '#1976d2',
304
- color: 'white',
305
- padding: '12px 0',
306
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
307
- marginBottom: '20px'
308
- }}>
309
- <div style={{
310
- maxWidth: '1200px',
311
- margin: '0 auto',
312
- padding: '0 20px',
313
- display: 'flex',
314
- alignItems: 'center',
315
- justifyContent: 'space-between'
316
- }}>
317
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
318
- <button
319
- onClick={onBack}
320
- style={{
321
- color: 'white',
322
- textDecoration: 'none',
323
- background: 'none',
324
- border: 'none',
325
- cursor: 'pointer',
326
- fontSize: '1.5rem',
327
- fontWeight: 'bold'
328
- }}
329
- >
330
- LocalLend
331
- </button>
332
- </div>
333
-
334
- <span style={{
335
- color: 'white',
336
- fontSize: '14px',
337
- padding: '8px 12px',
338
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
339
- borderRadius: '4px'
340
- }}>
341
- Welcome, {user?.name}!
342
- </span>
343
- </div>
344
- </nav>
345
-
346
- {/* Add Item Form */}
347
- <div style={{ padding: '0 20px', maxWidth: '800px', margin: '0 auto' }}>
348
- <h1 style={{ marginBottom: '10px', color: '#1976d2' }}>Add New Item</h1>
349
- <p style={{ color: '#666', marginBottom: '30px' }}>
350
- List an item you'd like to lend to others in your community.
351
- </p>
352
-
353
- <div style={{
354
- backgroundColor: 'white',
355
- padding: '30px',
356
- borderRadius: '8px',
357
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
358
- }}>
359
- <form onSubmit={handleSubmit}>
360
- {/* Title */}
361
- <div style={{ marginBottom: '20px' }}>
362
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
363
- Item Title * (3-100 characters)
364
- </label>
365
- <input
366
- type="text"
367
- name="title"
368
- value={formData.title}
369
- onChange={handleChange}
370
- required
371
- minLength={3}
372
- maxLength={100}
373
- placeholder="e.g., Canon DSLR Camera, Mountain Bike, Power Drill"
374
- style={{
375
- width: '100%',
376
- padding: '12px',
377
- border: '1px solid #ddd',
378
- borderRadius: '4px',
379
- fontSize: '16px',
380
- boxSizing: 'border-box'
381
- }}
382
- />
383
- {formData.title.length > 0 && formData.title.length < 3 && (
384
- <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
385
- Title must be at least 3 characters long
386
- </div>
387
- )}
388
- {formData.title.length > 100 && (
389
- <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
390
- Title must be no more than 100 characters long
391
- </div>
392
- )}
393
- </div>
394
-
395
- {/* Description */}
396
- <div style={{ marginBottom: '20px' }}>
397
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
398
- Description * (10-500 characters)
399
- </label>
400
- <textarea
401
- name="description"
402
- value={formData.description}
403
- onChange={handleChange}
404
- required
405
- minLength={10}
406
- maxLength={500}
407
- rows={4}
408
- placeholder="Describe your item in detail - condition, features, what's included..."
409
- style={{
410
- width: '100%',
411
- padding: '12px',
412
- border: '1px solid #ddd',
413
- borderRadius: '4px',
414
- fontSize: '16px',
415
- boxSizing: 'border-box',
416
- resize: 'vertical'
417
- }}
418
- />
419
- <div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
420
- {formData.description.length}/500 characters
421
- </div>
422
- {formData.description.length > 0 && formData.description.length < 10 && (
423
- <div style={{ color: 'red', fontSize: '12px', marginTop: '4px' }}>
424
- Description must be at least 10 characters long
425
- </div>
426
- )}
427
- </div>
428
-
429
- {/* Category and Condition Row */}
430
- <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', marginBottom: '20px' }}>
431
- <div>
432
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
433
- Category *
434
- </label>
435
- <select
436
- name="category"
437
- value={formData.category}
438
- onChange={handleChange}
439
- required
440
- style={{
441
- width: '100%',
442
- padding: '12px',
443
- border: '1px solid #ddd',
444
- borderRadius: '4px',
445
- fontSize: '16px',
446
- boxSizing: 'border-box'
447
- }}
448
- >
449
- <option value="">Select a category</option>
450
- {categories.map(cat => (
451
- <option key={cat.id} value={cat.id}>{cat.name}</option>
452
- ))}
453
- </select>
454
- </div>
455
-
456
- <div>
457
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
458
- Condition *
459
- </label>
460
- <select
461
- name="condition"
462
- value={formData.condition}
463
- onChange={handleChange}
464
- required
465
- style={{
466
- width: '100%',
467
- padding: '12px',
468
- border: '1px solid #ddd',
469
- borderRadius: '4px',
470
- fontSize: '16px',
471
- boxSizing: 'border-box'
472
- }}
473
- >
474
- {conditions.map(condition => (
475
- <option key={condition.value} value={condition.value}>
476
- {condition.label}
477
- </option>
478
- ))}
479
- </select>
480
- </div>
481
- </div>
482
-
483
- {/* Deposit */}
484
- <div style={{ marginBottom: '20px' }}>
485
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
486
- Deposit (₹) *
487
- </label>
488
- <input
489
- type="number"
490
- name="deposit"
491
- value={formData.deposit}
492
- onChange={handleChange}
493
- required
494
- min="0"
495
- step="0.01"
496
- placeholder="25.00"
497
- style={{
498
- width: '200px',
499
- padding: '12px',
500
- border: '1px solid #ddd',
501
- borderRadius: '4px',
502
- fontSize: '16px',
503
- boxSizing: 'border-box'
504
- }}
505
- />
506
- </div>
507
-
508
- {/* Image Upload Section */}
509
- <div style={{ marginBottom: '20px' }}>
510
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
511
- Item Images (Optional, max 5)
512
- </label>
513
- <p style={{ fontSize: '12px', color: '#666', marginBottom: '10px' }}>
514
- Upload clear photos of your item. Supported formats: JPG, PNG, GIF, WebP (max 10MB each)
515
- </p>
516
-
517
- <input
518
- type="file"
519
- accept="image/*"
520
- multiple
521
- onChange={handleImageSelect}
522
- disabled={isUploadingImages || selectedFiles.length >= 5}
523
- style={{
524
- display: 'block',
525
- marginBottom: '15px',
526
- padding: '10px',
527
- border: '2px dashed #ddd',
528
- borderRadius: '4px',
529
- width: '100%',
530
- boxSizing: 'border-box',
531
- cursor: selectedFiles.length >= 5 ? 'not-allowed' : 'pointer'
532
- }}
533
- />
534
-
535
- {/* Image Previews */}
536
- {imagePreviews.length > 0 && (
537
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(120px, 1fr))', gap: '10px' }}>
538
- {imagePreviews.map((preview, index) => (
539
- <div key={index} style={{ position: 'relative', borderRadius: '8px', overflow: 'hidden', border: '2px solid #ddd' }}>
540
- <img
541
- src={preview}
542
- alt={`Preview ${index + 1}`}
543
- style={{
544
- width: '100%',
545
- height: '120px',
546
- objectFit: 'cover',
547
- display: 'block'
548
- }}
549
- />
550
- <button
551
- type="button"
552
- onClick={() => handleRemoveImage(index)}
553
- disabled={isUploadingImages}
554
- style={{
555
- position: 'absolute',
556
- top: '5px',
557
- right: '5px',
558
- backgroundColor: 'rgba(244, 67, 54, 0.9)',
559
- color: 'white',
560
- border: 'none',
561
- borderRadius: '50%',
562
- width: '24px',
563
- height: '24px',
564
- cursor: 'pointer',
565
- fontSize: '16px',
566
- lineHeight: '24px',
567
- padding: '0',
568
- display: 'flex',
569
- alignItems: 'center',
570
- justifyContent: 'center'
571
- }}
572
- title="Remove image"
573
- >
574
- ×
575
- </button>
576
- {uploadedImageUrls[index] && (
577
- <div style={{
578
- position: 'absolute',
579
- bottom: '5px',
580
- left: '5px',
581
- backgroundColor: 'rgba(76, 175, 80, 0.9)',
582
- color: 'white',
583
- padding: '2px 6px',
584
- borderRadius: '3px',
585
- fontSize: '10px',
586
- fontWeight: 'bold'
587
- }}>
588
- ✓ Uploaded
589
- </div>
590
- )}
591
- </div>
592
- ))}
593
- </div>
594
- )}
595
-
596
- {isUploadingImages && (
597
- <div style={{
598
- marginTop: '10px',
599
- padding: '10px',
600
- backgroundColor: '#e3f2fd',
601
- borderRadius: '4px',
602
- color: '#1976d2',
603
- fontSize: '14px'
604
- }}>
605
- 📤 Uploading images...
606
- </div>
607
- )}
608
- </div>
609
-
610
- {/* Submit Buttons */}
611
- <div style={{ display: 'flex', gap: '15px' }}>
612
- <button
613
- type="submit"
614
- disabled={isLoading}
615
- style={{
616
- padding: '12px 30px',
617
- backgroundColor: isLoading ? '#ccc' : '#4caf50',
618
- color: 'white',
619
- border: 'none',
620
- borderRadius: '4px',
621
- cursor: isLoading ? 'not-allowed' : 'pointer',
622
- fontSize: '16px',
623
- fontWeight: 'bold'
624
- }}
625
- >
626
- {isLoading ? 'Adding Item...' : 'Add Item'}
627
- </button>
628
-
629
- <button
630
- type="button"
631
- onClick={onBack}
632
- disabled={isLoading}
633
- style={{
634
- padding: '12px 30px',
635
- backgroundColor: '#757575',
636
- color: 'white',
637
- border: 'none',
638
- borderRadius: '4px',
639
- cursor: isLoading ? 'not-allowed' : 'pointer',
640
- fontSize: '16px'
641
- }}
642
- >
643
- Cancel
644
- </button>
645
- </div>
646
- </form>
647
- </div>
648
- </div>
649
- </div>
650
- );
651
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/BookItemPage.tsx DELETED
@@ -1,348 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { useAuth } from '../context/AuthContext';
3
- // import { bookingService } from '../services/bookingService';
4
- // import type { CreateBookingRequest } from '../types';
5
-
6
- interface BookItemPageProps {
7
- onBack: () => void;
8
- item: any;
9
- }
10
-
11
- export const BookItemPage: React.FC<BookItemPageProps> = ({ onBack, item }) => {
12
- const { user } = useAuth();
13
- const [isLoading, setIsLoading] = useState(false);
14
- const [formData, setFormData] = useState({
15
- startDate: '',
16
- endDate: '',
17
- bookingNotes: '',
18
- depositAmount: 0,
19
- acceptTerms: false
20
- });
21
-
22
- const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
23
- const { name, value, type } = e.target;
24
- setFormData(prev => ({
25
- ...prev,
26
- [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
27
- }));
28
- };
29
-
30
- const handleSubmit = async (e: React.FormEvent) => {
31
- e.preventDefault();
32
-
33
- if (!formData.acceptTerms) {
34
- alert('Please accept the terms and conditions');
35
- return;
36
- }
37
-
38
- // More robust date validation
39
- const now = new Date();
40
- const startDate = new Date(formData.startDate);
41
- const endDate = new Date(formData.endDate);
42
-
43
- // Start date must be at least tomorrow
44
- const tomorrow = new Date(now);
45
- tomorrow.setDate(tomorrow.getDate() + 1);
46
- tomorrow.setHours(0, 0, 0, 0);
47
-
48
- if (startDate < tomorrow) {
49
- alert('Start date must be at least tomorrow');
50
- return;
51
- }
52
-
53
- if (endDate <= startDate) {
54
- alert('End date must be after start date');
55
- return;
56
- }
57
-
58
- // Additional validation checks
59
- if (!formData.acceptTerms) {
60
- alert('You must accept the terms and conditions');
61
- return;
62
- }
63
-
64
- if (!item?.id) {
65
- alert('Invalid item selected');
66
- return;
67
- }
68
-
69
- setIsLoading(true);
70
-
71
- try {
72
- console.log('🚀 Creating booking for item:', item.id, 'user:', user?.id);
73
-
74
- // Create date objects with proper time
75
- const startDateTime = new Date(formData.startDate + 'T10:00:00');
76
- const endDateTime = new Date(formData.endDate + 'T18:00:00');
77
-
78
- // Calculate duration in days
79
- const durationDays = Math.max(1, Math.ceil((endDateTime.getTime() - startDateTime.getTime()) / (1000 * 60 * 60 * 24)));
80
-
81
- // Format according to integration guide requirements
82
- // The guide shows format: "2025-11-10T10:00:00" (without Z and milliseconds)
83
- const formatDateTime = (date: Date) => {
84
- const year = date.getFullYear();
85
- const month = String(date.getMonth() + 1).padStart(2, '0');
86
- const day = String(date.getDate()).padStart(2, '0');
87
- const hours = String(date.getHours()).padStart(2, '0');
88
- const minutes = String(date.getMinutes()).padStart(2, '0');
89
- const seconds = String(date.getSeconds()).padStart(2, '0');
90
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
91
- };
92
-
93
- const bookingData = {
94
- itemId: item.id,
95
- startDate: formatDateTime(startDateTime), // "2025-11-10T10:00:00"
96
- endDate: formatDateTime(endDateTime), // "2025-11-13T18:00:00"
97
- bookingNotes: (formData.bookingNotes || '').slice(0, 500),
98
- depositAmount: Number(formData.depositAmount) || 0,
99
- requestedDurationDays: durationDays,
100
- acceptTerms: formData.acceptTerms
101
- };
102
-
103
- console.log('📝 Booking data to send:', bookingData);
104
- console.log('🆔 User ID for X-User-Id header:', user?.id);
105
-
106
- if (!user?.id) {
107
- alert('❌ User not found. Please log in again.');
108
- return;
109
- }
110
-
111
- // Test backend connectivity first
112
- try {
113
- const healthCheck = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/categories`, {
114
- method: 'GET'
115
- });
116
- console.log('🏥 Backend health check:', healthCheck.status);
117
- if (!healthCheck.ok) {
118
- alert(`❌ Backend connectivity issue: ${healthCheck.status}. Please check if backend is running on port 8080.`);
119
- return;
120
- }
121
- } catch (connectError) {
122
- console.error('🏥 Backend connection failed:', connectError);
123
- alert('❌ Cannot connect to backend. Please ensure backend is running on port 8080.');
124
- return;
125
- }
126
-
127
- try {
128
- // First try direct fetch to see exact error
129
- const token = localStorage.getItem('token');
130
- console.log('🔐 Using token:', token ? token.substring(0, 20) + '...' : 'NO TOKEN');
131
-
132
- const directResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings`, {
133
- method: 'POST',
134
- headers: {
135
- 'Content-Type': 'application/json',
136
- 'Authorization': `Bearer ${token}`,
137
- 'X-User-Id': user.id
138
- },
139
- body: JSON.stringify(bookingData)
140
- });
141
-
142
- console.log('📡 Direct response status:', directResponse.status);
143
- console.log('📡 Direct response headers:', Object.fromEntries(directResponse.headers.entries()));
144
-
145
- if (!directResponse.ok) {
146
- const errorText = await directResponse.text();
147
- console.error('📡 Direct response error body:', errorText);
148
-
149
- let errorJson = null;
150
- try {
151
- errorJson = JSON.parse(errorText);
152
- } catch (e) {
153
- // Error text is not JSON
154
- }
155
-
156
- throw new Error(`HTTP ${directResponse.status}: ${errorJson?.message || errorText || 'Unknown error'}`);
157
- }
158
-
159
- const bookingResponse = await directResponse.json();
160
- console.log('✅ Direct booking created successfully:', bookingResponse);
161
-
162
- // Also test the service layer for comparison
163
- // const bookingResponse = await bookingService.createBooking(bookingData, user.id);
164
- console.log('✅ Booking created successfully:', bookingResponse);
165
- alert(`✅ Booking request sent for "${item.name}"! You can track it in your dashboard.`);
166
- onBack();
167
- } catch (serviceError: any) {
168
- console.error('❌ Booking service failed:', serviceError);
169
- console.error('Full error object:', JSON.stringify(serviceError, null, 2));
170
-
171
- // Try to extract detailed error information
172
- let detailedError = 'Unknown error occurred';
173
- if (serviceError?.response) {
174
- const { status, data } = serviceError.response;
175
- console.error(`HTTP ${status}:`, data);
176
-
177
- if (status === 400) {
178
- detailedError = `Bad Request (400): ${data?.message || data?.details || JSON.stringify(data)}`;
179
- } else if (status === 401) {
180
- detailedError = 'Unauthorized (401): Please log in again';
181
- } else if (status === 403) {
182
- detailedError = 'Forbidden (403): Not authorized to create booking';
183
- } else if (status === 404) {
184
- detailedError = 'Not Found (404): Booking endpoint not available';
185
- } else {
186
- detailedError = `HTTP ${status}: ${data?.message || 'Server error'}`;
187
- }
188
- } else if (serviceError?.message) {
189
- detailedError = serviceError.message;
190
- }
191
-
192
- alert(`❌ Failed to create booking:\n${detailedError}\n\nCheck browser console for detailed logs.`);
193
- }
194
- } catch (error) {
195
- console.error('💥 Error creating booking:', error);
196
- alert(`❌ Network error creating booking: ${error}`);
197
- } finally {
198
- setIsLoading(false);
199
- }
200
- };
201
-
202
- if (!item) {
203
- return (
204
- <div style={{ padding: '20px', textAlign: 'center' }}>
205
- <h2>Item not found</h2>
206
- <button onClick={onBack} style={{ padding: '10px 20px', backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
207
- Back
208
- </button>
209
- </div>
210
- );
211
- }
212
-
213
- return (
214
- <div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
215
- <div style={{ marginBottom: '20px' }}>
216
- <button onClick={onBack} style={{ padding: '8px 16px', backgroundColor: '#666', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
217
- ← Back
218
- </button>
219
- </div>
220
-
221
- <div style={{ backgroundColor: 'white', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)', marginBottom: '20px' }}>
222
- <h2 style={{ color: '#1976d2', marginBottom: '10px' }}>Request to Book Item</h2>
223
- <div style={{ display: 'flex', alignItems: 'center', marginBottom: '20px' }}>
224
- {item.images?.[0] && (
225
- <img src={item.images[0]} alt={item.name} style={{ width: '60px', height: '60px', objectFit: 'cover', borderRadius: '4px', marginRight: '15px' }} />
226
- )}
227
- <div>
228
- <h3 style={{ margin: '0 0 5px 0', color: '#333' }}>{item.name}</h3>
229
- <p style={{ margin: '0', color: '#666', fontSize: '14px' }}>{item.description}</p>
230
- <p style={{ margin: '5px 0 0 0', color: '#1976d2', fontSize: '14px', fontWeight: 'bold' }}>Owner: {item.ownerName}</p>
231
- </div>
232
- </div>
233
- </div>
234
-
235
- <form onSubmit={handleSubmit} style={{ backgroundColor: 'white', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}>
236
- <div style={{ marginBottom: '20px' }}>
237
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
238
- Start Date *
239
- </label>
240
- <input
241
- type="date"
242
- name="startDate"
243
- value={formData.startDate}
244
- onChange={handleChange}
245
- required
246
- min={new Date().toISOString().split('T')[0]}
247
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px' }}
248
- />
249
- </div>
250
-
251
- <div style={{ marginBottom: '20px' }}>
252
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
253
- End Date *
254
- </label>
255
- <input
256
- type="date"
257
- name="endDate"
258
- value={formData.endDate}
259
- onChange={handleChange}
260
- required
261
- min={formData.startDate || new Date().toISOString().split('T')[0]}
262
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px' }}
263
- />
264
- </div>
265
-
266
- <div style={{ marginBottom: '20px' }}>
267
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
268
- Deposit Amount in ₹ (Optional)
269
- </label>
270
- <input
271
- type="number"
272
- name="depositAmount"
273
- value={formData.depositAmount}
274
- onChange={handleChange}
275
- min="0"
276
- step="0.01"
277
- placeholder="0.00"
278
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px' }}
279
- />
280
- </div>
281
-
282
- <div style={{ marginBottom: '20px' }}>
283
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#333' }}>
284
- Notes to Owner (Optional)
285
- </label>
286
- <textarea
287
- name="bookingNotes"
288
- value={formData.bookingNotes}
289
- onChange={handleChange}
290
- rows={4}
291
- placeholder="Any special requests or information for the owner..."
292
- style={{ width: '100%', padding: '10px', border: '1px solid #ddd', borderRadius: '4px', fontSize: '16px', resize: 'vertical' }}
293
- />
294
- </div>
295
-
296
- <div style={{ marginBottom: '20px' }}>
297
- <label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
298
- <input
299
- type="checkbox"
300
- name="acceptTerms"
301
- checked={formData.acceptTerms}
302
- onChange={handleChange}
303
- required
304
- style={{ marginRight: '8px' }}
305
- />
306
- <span style={{ color: '#333' }}>I accept the terms and conditions for booking this item *</span>
307
- </label>
308
- </div>
309
-
310
- <div style={{ display: 'flex', gap: '10px' }}>
311
- <button
312
- type="button"
313
- onClick={onBack}
314
- style={{
315
- flex: 1,
316
- padding: '12px',
317
- backgroundColor: '#666',
318
- color: 'white',
319
- border: 'none',
320
- borderRadius: '4px',
321
- cursor: 'pointer',
322
- fontSize: '16px'
323
- }}
324
- >
325
- Cancel
326
- </button>
327
- <button
328
- type="submit"
329
- disabled={isLoading || !formData.acceptTerms}
330
- style={{
331
- flex: 2,
332
- padding: '12px',
333
- backgroundColor: isLoading || !formData.acceptTerms ? '#ccc' : '#1976d2',
334
- color: 'white',
335
- border: 'none',
336
- borderRadius: '4px',
337
- cursor: isLoading || !formData.acceptTerms ? 'not-allowed' : 'pointer',
338
- fontSize: '16px',
339
- fontWeight: 'bold'
340
- }}
341
- >
342
- {isLoading ? 'Creating Booking...' : 'Send Booking Request'}
343
- </button>
344
- </div>
345
- </form>
346
- </div>
347
- );
348
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/BrowsePage.tsx DELETED
@@ -1,238 +0,0 @@
1
- // Browse Items Page Component
2
- import React, { useState } from 'react';
3
-
4
- export interface BrowsePageProps {
5
- onBack?: () => void;
6
- }
7
-
8
- export const BrowsePage: React.FC<BrowsePageProps> = ({ onBack }) => {
9
- const [searchQuery, setSearchQuery] = useState('');
10
- const [selectedCategory, setSelectedCategory] = useState('');
11
-
12
- // Mock data for items
13
- const mockItems = [
14
- {
15
- id: 1,
16
- title: 'Electric Drill',
17
- category: 'Tools',
18
- description: 'Professional grade electric drill, perfect for home projects',
19
- owner: 'John Smith',
20
- pricePerDay: 15,
21
- location: 'Downtown',
22
- image: '🔧'
23
- },
24
- {
25
- id: 2,
26
- title: 'Canon Camera',
27
- category: 'Electronics',
28
- description: 'DSLR camera with multiple lenses for photography enthusiasts',
29
- owner: 'Sarah Johnson',
30
- pricePerDay: 25,
31
- location: 'Uptown',
32
- image: '📷'
33
- },
34
- {
35
- id: 3,
36
- title: 'Mountain Bike',
37
- category: 'Sports',
38
- description: 'High-quality mountain bike for trail adventures',
39
- owner: 'Mike Wilson',
40
- pricePerDay: 20,
41
- location: 'Suburbs',
42
- image: '🚵‍♂️'
43
- },
44
- {
45
- id: 4,
46
- title: 'Programming Books Set',
47
- category: 'Books',
48
- description: 'Complete set of modern programming language books',
49
- owner: 'Lisa Chen',
50
- pricePerDay: 5,
51
- location: 'University Area',
52
- image: '📚'
53
- }
54
- ];
55
-
56
- const filteredItems = mockItems.filter(item => {
57
- const matchesSearch = item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
58
- item.description.toLowerCase().includes(searchQuery.toLowerCase());
59
- const matchesCategory = selectedCategory === '' || item.category === selectedCategory;
60
- return matchesSearch && matchesCategory;
61
- });
62
-
63
- return (
64
- <div style={{
65
- minHeight: '100vh',
66
- backgroundColor: '#f5f5f5',
67
- padding: '20px'
68
- }}>
69
- <div style={{
70
- maxWidth: '1200px',
71
- margin: '0 auto'
72
- }}>
73
- {/* Header */}
74
- <div style={{
75
- backgroundColor: 'white',
76
- padding: '30px',
77
- borderRadius: '8px',
78
- marginBottom: '30px',
79
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
80
- }}>
81
- <h1 style={{ color: '#1976d2', marginBottom: '20px' }}>
82
- Browse Available Items
83
- </h1>
84
-
85
- {/* Search and Filter */}
86
- <div style={{
87
- display: 'flex',
88
- gap: '15px',
89
- marginBottom: '20px',
90
- flexWrap: 'wrap'
91
- }}>
92
- <input
93
- type="search"
94
- placeholder="Search items..."
95
- value={searchQuery}
96
- onChange={(e) => setSearchQuery(e.target.value)}
97
- style={{
98
- flex: 1,
99
- minWidth: '250px',
100
- padding: '12px',
101
- border: '1px solid #ddd',
102
- borderRadius: '4px',
103
- fontSize: '16px'
104
- }}
105
- />
106
- <select
107
- value={selectedCategory}
108
- onChange={(e) => setSelectedCategory(e.target.value)}
109
- style={{
110
- padding: '12px',
111
- border: '1px solid #ddd',
112
- borderRadius: '4px',
113
- fontSize: '16px',
114
- minWidth: '150px'
115
- }}
116
- >
117
- <option value="">All Categories</option>
118
- <option value="Tools">Tools</option>
119
- <option value="Electronics">Electronics</option>
120
- <option value="Sports">Sports</option>
121
- <option value="Books">Books</option>
122
- </select>
123
- </div>
124
-
125
- {onBack && (
126
- <button
127
- onClick={onBack}
128
- style={{
129
- padding: '8px 16px',
130
- backgroundColor: '#666',
131
- color: 'white',
132
- border: 'none',
133
- borderRadius: '4px',
134
- cursor: 'pointer'
135
- }}
136
- >
137
- ← Back to Home
138
- </button>
139
- )}
140
- </div>
141
-
142
- {/* Items Grid */}
143
- <div style={{
144
- display: 'grid',
145
- gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
146
- gap: '20px'
147
- }}>
148
- {filteredItems.map(item => (
149
- <div
150
- key={item.id}
151
- style={{
152
- backgroundColor: 'white',
153
- borderRadius: '8px',
154
- padding: '20px',
155
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
156
- transition: 'transform 0.2s',
157
- cursor: 'pointer'
158
- }}
159
- onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-2px)'}
160
- onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'}
161
- >
162
- <div style={{ fontSize: '3rem', textAlign: 'center', marginBottom: '15px' }}>
163
- {item.image}
164
- </div>
165
-
166
- <h3 style={{ color: '#1976d2', marginBottom: '10px' }}>
167
- {item.title}
168
- </h3>
169
-
170
- <div style={{
171
- display: 'inline-block',
172
- backgroundColor: '#e3f2fd',
173
- color: '#1976d2',
174
- padding: '4px 8px',
175
- borderRadius: '4px',
176
- fontSize: '12px',
177
- marginBottom: '10px'
178
- }}>
179
- {item.category}
180
- </div>
181
-
182
- <p style={{ color: '#666', marginBottom: '15px', lineHeight: '1.5' }}>
183
- {item.description}
184
- </p>
185
-
186
- <div style={{ marginBottom: '15px' }}>
187
- <div style={{ fontSize: '14px', color: '#666' }}>
188
- Owner: {item.owner}
189
- </div>
190
- <div style={{ fontSize: '14px', color: '#666' }}>
191
- Location: {item.location}
192
- </div>
193
- </div>
194
-
195
- <div style={{
196
- display: 'flex',
197
- justifyContent: 'space-between',
198
- alignItems: 'center'
199
- }}>
200
- <div style={{ fontWeight: 'bold', color: '#4caf50', fontSize: '18px' }}>
201
- ₹${item.pricePerDay}/day
202
- </div>
203
- <button
204
- style={{
205
- padding: '8px 16px',
206
- backgroundColor: '#1976d2',
207
- color: 'white',
208
- border: 'none',
209
- borderRadius: '4px',
210
- cursor: 'pointer',
211
- fontSize: '14px'
212
- }}
213
- onClick={() => alert(`Request to borrow "${item.title}" - Feature coming soon!`)}
214
- >
215
- Request to Borrow
216
- </button>
217
- </div>
218
- </div>
219
- ))}
220
- </div>
221
-
222
- {filteredItems.length === 0 && (
223
- <div style={{
224
- backgroundColor: 'white',
225
- padding: '40px',
226
- borderRadius: '8px',
227
- textAlign: 'center',
228
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
229
- }}>
230
- <p style={{ color: '#666', fontSize: '18px' }}>
231
- No items found matching your search criteria.
232
- </p>
233
- </div>
234
- )}
235
- </div>
236
- </div>
237
- );
238
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/DashboardPage.tsx DELETED
@@ -1,1685 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useAuth } from '../context/AuthContext';
3
- import { itemService } from '../services/itemService';
4
-
5
- interface DashboardPageProps {
6
- onBack: () => void;
7
- onAddItem?: () => void;
8
- }
9
-
10
- export const DashboardPage: React.FC<DashboardPageProps> = ({ onBack, onAddItem }) => {
11
- const { user } = useAuth();
12
- const [myItems, setMyItems] = useState<any[]>([]);
13
- const [myBookings, setMyBookings] = useState<any[]>([]);
14
- const [receivedBookings, setReceivedBookings] = useState<any[]>([]);
15
- const [isLoading, setIsLoading] = useState(true);
16
- const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
17
-
18
- // Function to delete an item
19
- const handleDeleteItem = async (itemId: string, itemName: string) => {
20
- if (!user?.id) {
21
- alert('❌ User not found. Please log in again.');
22
- return;
23
- }
24
-
25
- const confirmDelete = window.confirm(
26
- `Are you sure you want to delete "${itemName}"?\n\nThis action cannot be undone. The item will be permanently removed from your listings.`
27
- );
28
-
29
- if (!confirmDelete) {
30
- return;
31
- }
32
-
33
- setDeletingItemId(itemId);
34
- try {
35
- console.log('🗑️ Deleting item:', itemId, 'with userId:', user.id);
36
- console.log('🗑️ DELETE request URL:', `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items/${itemId}`);
37
-
38
- const deleteResult = await itemService.deleteItem(itemId, user.id);
39
- console.log('🗑️ Delete API response:', deleteResult);
40
-
41
- // Remove the item from the local state immediately
42
- setMyItems(prevItems => prevItems.filter(item => item.id !== itemId));
43
-
44
- alert(`✅ "${itemName}" has been successfully deleted.`);
45
- console.log('✅ Item deleted successfully');
46
- } catch (error: any) {
47
- console.error('❌ Delete item failed:', error);
48
-
49
- let errorMessage = 'Failed to delete item. Please try again.';
50
- if (error?.response?.status === 400) {
51
- errorMessage = 'Cannot delete item: It may have active bookings or be in use.';
52
- } else if (error?.response?.status === 403) {
53
- errorMessage = 'You are not authorized to delete this item.';
54
- } else if (error?.response?.status === 404) {
55
- errorMessage = 'Item not found. It may have already been deleted.';
56
- } else if (error?.response?.data?.message) {
57
- errorMessage = error.response.data.message;
58
- }
59
-
60
- alert(`❌ ${errorMessage}`);
61
- } finally {
62
- setDeletingItemId(null);
63
- }
64
- };
65
-
66
- // Function to toggle item status
67
- const handleToggleStatus = async (itemId: string, currentStatus: string) => {
68
- if (!user?.id) {
69
- alert('❌ User not found. Please log in again.');
70
- return;
71
- }
72
-
73
- // Validate current status and determine new status
74
- if (!currentStatus || !['AVAILABLE', 'UNAVAILABLE', 'BORROWED'].includes(currentStatus)) {
75
- alert('❌ Invalid current item status. Please refresh the page and try again.');
76
- return;
77
- }
78
-
79
- // Don't allow changing status if item is borrowed
80
- if (currentStatus === 'BORROWED') {
81
- alert('❌ Cannot change status of borrowed items. Wait for the item to be returned.');
82
- return;
83
- }
84
-
85
- const newStatus = currentStatus === 'AVAILABLE' ? 'UNAVAILABLE' : 'AVAILABLE';
86
- console.log('📝 Status change:', `${currentStatus} → ${newStatus}`);
87
-
88
- try {
89
- console.log('🔄 Updating item status:', {
90
- itemId,
91
- currentStatus,
92
- newStatus,
93
- userId: user.id
94
- });
95
-
96
- // First test backend connectivity and API routing
97
- try {
98
- const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
99
- console.log('🔍 Testing backend at:', baseUrl);
100
-
101
- // Test 1: Basic connectivity
102
- const healthCheck = await fetch(`${baseUrl}/api/items`, {
103
- method: 'GET'
104
- });
105
- console.log('🏥 Backend health check for /api/items:', healthCheck.status);
106
-
107
- // Test 2: Check if we're getting static resource errors
108
- if (healthCheck.status === 500) {
109
- const errorText = await healthCheck.text();
110
- if (errorText.includes('No static resource')) {
111
- alert(`❌ Backend API Routing Issue Detected!
112
-
113
- The backend is treating API calls as static file requests.
114
-
115
- This means:
116
- 1. The backend API server is not running properly
117
- 2. You may be connecting to a frontend/static server instead of the API server
118
- 3. The backend routes are not configured correctly
119
-
120
- Current URL: ${baseUrl}
121
- Expected: Spring Boot API server with /api/* endpoints
122
-
123
- Please:
124
- 1. Check if your backend API server is running on port 8080
125
- 2. Verify you're not connecting to a frontend server
126
- 3. Check backend logs for proper API startup`);
127
- return;
128
- }
129
- }
130
-
131
- // Test 3: Try a different endpoint to confirm API availability
132
- if (!healthCheck.ok && healthCheck.status !== 401 && healthCheck.status !== 403) {
133
- // Try categories endpoint (public)
134
- const categoriesTest = await fetch(`${baseUrl}/api/categories`);
135
- console.log('🏥 Categories endpoint test:', categoriesTest.status);
136
-
137
- if (categoriesTest.status === 500) {
138
- const catErrorText = await categoriesTest.text();
139
- if (catErrorText.includes('No static resource')) {
140
- alert('❌ Backend API server is not running. Static resource errors detected on multiple endpoints.');
141
- return;
142
- }
143
- }
144
- }
145
-
146
- } catch (connectError) {
147
- console.error('🏥 Backend connection failed:', connectError);
148
- alert(`❌ Cannot connect to backend at ${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}.
149
-
150
- Please ensure:
151
- 1. Backend is running
152
- 2. Correct port (8080)
153
- 3. No firewall blocking
154
- 4. CORS is configured
155
-
156
- Error: ${(connectError as any)?.message || connectError}`);
157
- return;
158
- }
159
-
160
- // Test direct API call first to see exact error
161
- const token = localStorage.getItem('token');
162
- console.log('🔐 Token exists:', !!token);
163
- console.log('🔐 Token preview:', token ? token.substring(0, 20) + '...' : 'NO TOKEN');
164
-
165
- // Use toggle endpoint only (simpler and works)
166
- let directResponse;
167
- let endpointUsed = `/api/items/${itemId}/toggle-availability`;
168
-
169
- try {
170
- directResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}${endpointUsed}`, {
171
- method: 'PATCH',
172
- headers: {
173
- 'Content-Type': 'application/json',
174
- 'Authorization': `Bearer ${token}`,
175
- 'X-User-Id': user.id
176
- }
177
- // No body needed - toggle endpoint just flips current status
178
- });
179
- } catch (error) {
180
- throw new Error(`Failed to call toggle endpoint: ${error}`);
181
- }
182
-
183
- console.log('📡 Used endpoint:', endpointUsed);
184
-
185
- console.log('📡 Direct response status:', directResponse.status);
186
- console.log('📡 Direct response headers:', Object.fromEntries(directResponse.headers.entries()));
187
-
188
- if (!directResponse.ok) {
189
- const errorText = await directResponse.text();
190
- console.error('📡 Direct response error body:', errorText);
191
-
192
- let errorJson = null;
193
- try {
194
- errorJson = JSON.parse(errorText);
195
- } catch (e) {
196
- // Error text is not JSON
197
- }
198
-
199
- let errorMessage = 'Failed to update item status';
200
-
201
- // Special handling for static resource error (backend routing issue)
202
- if (directResponse.status === 500 && errorText.includes('No static resource')) {
203
- errorMessage = `❌ Backend API Error: The backend is treating API calls as static resources.
204
-
205
- This indicates:
206
- 1. Backend API server is not properly running
207
- 2. Wrong backend URL or port
208
- 3. API endpoints not properly configured
209
- 4. Request being routed to static file server instead of API
210
-
211
- Current backend URL: ${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}
212
-
213
- Please check:
214
- - Is the backend API server running on port 8080?
215
- - Are you connecting to the right backend service?
216
- - Is the backend properly configured for API routes?`;
217
-
218
- } else if (directResponse.status === 400) {
219
- errorMessage = 'Bad Request: ' + (errorJson?.message || errorText || 'Invalid status value or request format');
220
- } else if (directResponse.status === 401) {
221
- errorMessage = 'Unauthorized: Please log in again';
222
- } else if (directResponse.status === 403) {
223
- errorMessage = 'Forbidden: You are not the owner of this item';
224
- } else if (directResponse.status === 404) {
225
- errorMessage = 'Not Found: Item not found or endpoint not available';
226
- } else {
227
- errorMessage = `HTTP ${directResponse.status}: ${errorJson?.message || errorText || 'Server error'}`;
228
- }
229
-
230
- throw new Error(errorMessage);
231
- }
232
-
233
- const updatedItem = await directResponse.json();
234
- console.log('✅ Direct API success, updated item:', updatedItem);
235
-
236
- // Update the item in the local state
237
- setMyItems(prevItems =>
238
- prevItems.map(item =>
239
- item.id === itemId ? { ...item, status: updatedItem.status || newStatus } : item
240
- )
241
- );
242
-
243
- console.log('✅ Item status updated successfully to:', updatedItem.status || newStatus);
244
- alert(`✅ Item status changed to ${updatedItem.status || newStatus}`);
245
-
246
- } catch (error: any) {
247
- console.error('❌ Update status failed:', error);
248
- console.error('Full error object:', JSON.stringify(error, null, 2));
249
-
250
- let userFriendlyMessage = 'Failed to update item status. Please try again.';
251
- if (error.message) {
252
- userFriendlyMessage = error.message;
253
- }
254
-
255
- alert(`❌ ${userFriendlyMessage}\n\nCheck browser console for detailed logs.`);
256
- }
257
- };
258
-
259
- // Function to cancel a booking
260
- const handleCancelBooking = async (bookingId: string, itemName: string) => {
261
- if (!user?.id) {
262
- alert('❌ User not found. Please log in again.');
263
- return;
264
- }
265
-
266
- const confirmCancel = window.confirm(
267
- `Are you sure you want to cancel your booking for "${itemName}"?\n\nThis action cannot be undone.`
268
- );
269
-
270
- if (!confirmCancel) return;
271
-
272
- try {
273
- const token = localStorage.getItem('token');
274
-
275
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/${bookingId}/cancel`, {
276
- method: 'PATCH',
277
- headers: {
278
- 'Content-Type': 'application/json',
279
- 'Authorization': `Bearer ${token}`,
280
- 'X-User-Id': user.id
281
- },
282
- body: JSON.stringify({
283
- reason: 'Cancelled by borrower'
284
- })
285
- });
286
-
287
- if (!response.ok) {
288
- const errorText = await response.text();
289
- console.error('Cancel booking error:', errorText);
290
-
291
- let errorMessage = 'Failed to cancel booking';
292
- if (response.status === 403) {
293
- errorMessage = 'You are not authorized to cancel this booking';
294
- } else if (response.status === 404) {
295
- errorMessage = 'Booking not found';
296
- } else if (response.status === 400) {
297
- errorMessage = 'Cannot cancel booking (may already be active or completed)';
298
- }
299
-
300
- throw new Error(errorMessage);
301
- }
302
-
303
- // Refresh bookings data
304
- await response.json();
305
- setMyBookings(prevBookings =>
306
- prevBookings.map(booking =>
307
- booking.id === bookingId ? { ...booking, status: 'CANCELLED' } : booking
308
- )
309
- );
310
-
311
- alert(`✅ Booking for "${itemName}" has been cancelled successfully`);
312
-
313
- } catch (error: any) {
314
- console.error('❌ Cancel booking failed:', error);
315
- alert(`❌ ${error.message || 'Failed to cancel booking. Please try again.'}`);
316
- }
317
- };
318
-
319
- // Function to view booking details
320
- const handleViewBookingDetails = (booking: any) => {
321
- const formatDate = (dateString: string) => {
322
- if (!dateString) return 'Not specified';
323
- try {
324
- return new Date(dateString).toLocaleDateString('en-US', {
325
- weekday: 'long',
326
- year: 'numeric',
327
- month: 'long',
328
- day: 'numeric'
329
- });
330
- } catch {
331
- return dateString;
332
- }
333
- };
334
-
335
- const details = `
336
- 📦 BOOKING DETAILS
337
-
338
- 🏷️ Item: ${booking.itemName || booking.itemTitle || 'Unknown Item'}
339
- 👤 Owner: ${booking.ownerName || 'Unknown Owner'}
340
- 📋 Status: ${booking.status || 'Unknown'}
341
-
342
- 📅 RENTAL PERIOD
343
- Start: ${formatDate(booking.startDate)}
344
- End: ${formatDate(booking.endDate)}
345
-
346
- 💰 FINANCIAL
347
- Deposit: ₹${booking.deposit || booking.itemDeposit || '0'}
348
-
349
- 📝 TIMELINE
350
- Requested: ${formatDate(booking.createdAt)}
351
- Booking ID: ${booking.id}
352
-
353
- 💬 NOTES
354
- ${booking.notes ? `Your Notes: "${booking.notes}"` : 'No notes from you'}
355
- ${booking.ownerNotes ? `Owner Notes: "${booking.ownerNotes}"` : 'No notes from owner'}
356
-
357
- ${booking.borrowerEmail ? `📧 Owner Contact: ${booking.ownerEmail || 'Not available'}` : ''}
358
- `.trim();
359
-
360
- alert(details);
361
- };
362
-
363
- // Function to start a booking
364
- const handleStartBooking = async (bookingId: string, itemName: string) => {
365
- if (!user?.id) {
366
- alert('❌ User not found. Please log in again.');
367
- return;
368
- }
369
-
370
- const confirmStart = window.confirm(
371
- `Start your booking for "${itemName}"?\n\nThis will mark the item as actively borrowed.`
372
- );
373
-
374
- if (!confirmStart) return;
375
-
376
- try {
377
- const token = localStorage.getItem('token');
378
-
379
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/${bookingId}/start`, {
380
- method: 'PATCH',
381
- headers: {
382
- 'Content-Type': 'application/json',
383
- 'Authorization': `Bearer ${token}`,
384
- 'X-User-Id': user.id
385
- }
386
- });
387
-
388
- if (!response.ok) {
389
- const errorText = await response.text();
390
- console.error('Start booking error:', errorText);
391
-
392
- let errorMessage = 'Failed to start booking';
393
- if (response.status === 403) {
394
- errorMessage = 'You are not authorized to start this booking';
395
- } else if (response.status === 404) {
396
- errorMessage = 'Booking not found';
397
- } else if (response.status === 400) {
398
- errorMessage = 'Cannot start booking (may not be confirmed yet)';
399
- }
400
-
401
- throw new Error(errorMessage);
402
- }
403
-
404
- // Update booking status
405
- await response.json();
406
- setMyBookings(prevBookings =>
407
- prevBookings.map(booking =>
408
- booking.id === bookingId ? { ...booking, status: 'ACTIVE' } : booking
409
- )
410
- );
411
-
412
- alert(`✅ Booking for "${itemName}" is now active. Enjoy your rental!`);
413
-
414
- } catch (error: any) {
415
- console.error('❌ Start booking failed:', error);
416
- alert(`❌ ${error.message || 'Failed to start booking. Please try again.'}`);
417
- }
418
- };
419
-
420
- // Function to complete a booking
421
- const handleCompleteBooking = async (bookingId: string, itemName: string) => {
422
- if (!user?.id) {
423
- alert('❌ User not found. Please log in again.');
424
- return;
425
- }
426
-
427
- const confirmComplete = window.confirm(
428
- `Mark your booking for "${itemName}" as completed?\n\nThis indicates you have returned the item to the owner.`
429
- );
430
-
431
- if (!confirmComplete) return;
432
-
433
- try {
434
- const token = localStorage.getItem('token');
435
-
436
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/${bookingId}/complete`, {
437
- method: 'PATCH',
438
- headers: {
439
- 'Content-Type': 'application/json',
440
- 'Authorization': `Bearer ${token}`,
441
- 'X-User-Id': user.id
442
- }
443
- });
444
-
445
- if (!response.ok) {
446
- const errorText = await response.text();
447
- console.error('Complete booking error:', errorText);
448
-
449
- let errorMessage = 'Failed to complete booking';
450
- if (response.status === 403) {
451
- errorMessage = 'You are not authorized to complete this booking';
452
- } else if (response.status === 404) {
453
- errorMessage = 'Booking not found';
454
- } else if (response.status === 400) {
455
- errorMessage = 'Cannot complete booking (may not be active)';
456
- }
457
-
458
- throw new Error(errorMessage);
459
- }
460
-
461
- // Update booking status
462
- await response.json();
463
- setMyBookings(prevBookings =>
464
- prevBookings.map(booking =>
465
- booking.id === bookingId ? { ...booking, status: 'COMPLETED' } : booking
466
- )
467
- );
468
-
469
- alert(`✅ Booking for "${itemName}" has been marked as completed. Thank you for using LocalLend!`);
470
-
471
- } catch (error: any) {
472
- console.error('❌ Complete booking failed:', error);
473
- alert(`❌ ${error.message || 'Failed to complete booking. Please try again.'}`);
474
- }
475
- };
476
-
477
- // Function to approve a booking request
478
- const handleApproveBookingRequest = async (bookingId: string, itemName: string, borrowerName: string) => {
479
- if (!user?.id) {
480
- alert('❌ User not found. Please log in again.');
481
- return;
482
- }
483
-
484
- const ownerNotes = prompt(
485
- `Approve booking request for "${itemName}" by ${borrowerName}?\n\nAdd any notes for the borrower (optional):`,
486
- "Please take good care of the item. Contact me if you have any questions."
487
- );
488
-
489
- if (ownerNotes === null) return; // User cancelled
490
-
491
- try {
492
- const token = localStorage.getItem('token');
493
-
494
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/${bookingId}/approve`, {
495
- method: 'PATCH',
496
- headers: {
497
- 'Content-Type': 'application/json',
498
- 'Authorization': `Bearer ${token}`,
499
- 'X-User-Id': user.id
500
- },
501
- body: JSON.stringify({
502
- ownerNotes: ownerNotes.trim() || undefined
503
- })
504
- });
505
-
506
- if (!response.ok) {
507
- const errorText = await response.text();
508
- console.error('Approve booking error:', errorText);
509
-
510
- let errorMessage = 'Failed to approve booking request';
511
- if (response.status === 403) {
512
- errorMessage = 'You are not authorized to approve this booking';
513
- } else if (response.status === 404) {
514
- errorMessage = 'Booking request not found';
515
- } else if (response.status === 400) {
516
- errorMessage = 'Cannot approve this booking (may already be processed)';
517
- }
518
-
519
- throw new Error(errorMessage);
520
- }
521
-
522
- // Update booking status in received bookings
523
- await response.json();
524
- setReceivedBookings(prevBookings =>
525
- prevBookings.map(booking =>
526
- booking.id === bookingId ? { ...booking, status: 'CONFIRMED', ownerNotes: ownerNotes.trim() } : booking
527
- )
528
- );
529
-
530
- alert(`✅ Booking request for "${itemName}" by ${borrowerName} has been approved!`);
531
-
532
- } catch (error: any) {
533
- console.error('❌ Approve booking failed:', error);
534
- alert(`❌ ${error.message || 'Failed to approve booking request. Please try again.'}`);
535
- }
536
- };
537
-
538
- // Function to decline a booking request
539
- const handleDeclineBookingRequest = async (bookingId: string, itemName: string, borrowerName: string) => {
540
- if (!user?.id) {
541
- alert('❌ User not found. Please log in again.');
542
- return;
543
- }
544
-
545
- const reason = prompt(
546
- `Decline booking request for "${itemName}" by ${borrowerName}?\n\nReason for declining (optional):`,
547
- "Sorry, the item is not available for the requested dates."
548
- );
549
-
550
- if (reason === null) return; // User cancelled
551
-
552
- const confirmDecline = window.confirm(
553
- `Are you sure you want to decline the booking request for "${itemName}" by ${borrowerName}?\n\nThis action cannot be undone.`
554
- );
555
-
556
- if (!confirmDecline) return;
557
-
558
- try {
559
- const token = localStorage.getItem('token');
560
-
561
- const response = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/${bookingId}/reject`, {
562
- method: 'PATCH',
563
- headers: {
564
- 'Content-Type': 'application/json',
565
- 'Authorization': `Bearer ${token}`,
566
- 'X-User-Id': user.id
567
- },
568
- body: JSON.stringify({
569
- reason: reason?.trim() || undefined
570
- })
571
- });
572
-
573
- if (!response.ok) {
574
- const errorText = await response.text();
575
- console.error('Decline booking error:', errorText);
576
-
577
- let errorMessage = 'Failed to decline booking request';
578
- if (response.status === 403) {
579
- errorMessage = 'You are not authorized to decline this booking';
580
- } else if (response.status === 404) {
581
- errorMessage = 'Booking request not found';
582
- } else if (response.status === 400) {
583
- errorMessage = 'Cannot decline this booking (may already be processed)';
584
- }
585
-
586
- throw new Error(errorMessage);
587
- }
588
-
589
- // Update booking status in received bookings
590
- await response.json();
591
- setReceivedBookings(prevBookings =>
592
- prevBookings.map(booking =>
593
- booking.id === bookingId ? { ...booking, status: 'REJECTED', rejectionReason: reason?.trim() } : booking
594
- )
595
- );
596
-
597
- alert(`✅ Booking request for "${itemName}" by ${borrowerName} has been declined.`);
598
-
599
- } catch (error: any) {
600
- console.error('❌ Decline booking failed:', error);
601
- alert(`❌ ${error.message || 'Failed to decline booking request. Please try again.'}`);
602
- }
603
- };
604
-
605
- // Function to view full booking request details
606
- const handleViewBookingRequestDetails = (booking: any) => {
607
- const formatDate = (dateString: string) => {
608
- if (!dateString) return 'Not specified';
609
- try {
610
- return new Date(dateString).toLocaleDateString('en-US', {
611
- weekday: 'long',
612
- year: 'numeric',
613
- month: 'long',
614
- day: 'numeric'
615
- });
616
- } catch {
617
- return dateString;
618
- }
619
- };
620
-
621
- const calculateDuration = () => {
622
- if (!booking.startDate || !booking.endDate) return 'Unknown duration';
623
- try {
624
- const start = new Date(booking.startDate);
625
- const end = new Date(booking.endDate);
626
- const diffTime = Math.abs(end.getTime() - start.getTime());
627
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
628
- return `${diffDays} day${diffDays !== 1 ? 's' : ''}`;
629
- } catch {
630
- return 'Unknown duration';
631
- }
632
- };
633
-
634
- const details = `
635
- 📦 BOOKING REQUEST DETAILS
636
-
637
- 🏷️ Item: ${booking.itemName || booking.itemTitle || 'Unknown Item'}
638
- 👤 Borrower: ${booking.borrowerName || 'Unknown Borrower'}
639
- 📧 Contact: ${booking.borrowerEmail || 'Not provided'}
640
- 📋 Status: ${booking.status || 'Unknown'}
641
-
642
- 📅 RENTAL PERIOD
643
- Start: ${formatDate(booking.startDate)}
644
- End: ${formatDate(booking.endDate)}
645
- Duration: ${calculateDuration()}
646
-
647
- 💰 FINANCIAL DETAILS
648
- Deposit Required: ₹${booking.deposit || booking.itemDeposit || '0'}
649
- ${booking.itemValue ? `Item Value: $${booking.itemValue}` : ''}
650
-
651
- 📝 TIMELINE
652
- Request Date: ${formatDate(booking.createdAt)}
653
- Booking ID: ${booking.id}
654
-
655
- ⭐ BORROWER INFORMATION
656
- ${booking.borrowerRating ? `Rating: ${booking.borrowerRating.toFixed(1)}/5 stars` : 'Rating: Not available'}
657
- ${booking.borrowerCompletedBookings ? `Completed Bookings: ${booking.borrowerCompletedBookings}` : ''}
658
-
659
- 💬 BORROWER'S MESSAGE
660
- ${booking.notes || booking.borrowerNotes || 'No message provided'}
661
-
662
- ${booking.ownerNotes ? `📋 YOUR NOTES\n"${booking.ownerNotes}"` : ''}
663
- ${booking.rejectionReason ? `❌ REJECTION REASON\n"${booking.rejectionReason}"` : ''}
664
- `.trim();
665
-
666
- alert(details);
667
- };
668
-
669
- // Function to contact borrower
670
- const handleContactBorrower = (booking: any) => {
671
- if (!booking.borrowerEmail) {
672
- alert('❌ No contact information available for this borrower.');
673
- return;
674
- }
675
-
676
- const subject = encodeURIComponent(`LocalLend: Regarding your booking for ${booking.itemName || booking.itemTitle || 'item'}`);
677
- const body = encodeURIComponent(`Hi ${booking.borrowerName || 'there'},
678
-
679
- I'm contacting you regarding your booking request for "${booking.itemName || booking.itemTitle || 'item'}".
680
-
681
- Booking Details:
682
- - Start Date: ${booking.startDate}
683
- - End Date: ${booking.endDate}
684
- - Status: ${booking.status}
685
-
686
- Best regards,
687
- ${user?.name || 'Item Owner'}
688
-
689
- ---
690
- This message was sent through LocalLend platform.`);
691
-
692
- window.open(`mailto:${booking.borrowerEmail}?subject=${subject}&body=${body}`);
693
- };
694
-
695
- // Function to send message to borrower
696
- const handleSendMessage = (booking: any) => {
697
- if (!booking.borrowerEmail) {
698
- alert('❌ No contact information available for this borrower.');
699
- return;
700
- }
701
-
702
- const message = prompt(
703
- `Send a message to ${booking.borrowerName || 'borrower'} about "${booking.itemName || booking.itemTitle}":\n\n(This will open your email client)`,
704
- `Hi ${booking.borrowerName || 'there'},\n\nRegarding your booking for "${booking.itemName || booking.itemTitle}"...\n\nBest regards,\n${user?.name || 'Item Owner'}`
705
- );
706
-
707
- if (message === null) return; // User cancelled
708
-
709
- const subject = encodeURIComponent(`LocalLend: ${booking.itemName || booking.itemTitle || 'Booking'}`);
710
- const body = encodeURIComponent(message);
711
-
712
- window.open(`mailto:${booking.borrowerEmail}?subject=${subject}&body=${body}`);
713
- };
714
-
715
- useEffect(() => {
716
- const loadDashboardData = async () => {
717
- try {
718
- setIsLoading(true);
719
- console.log('=== DASHBOARD DATA LOADING START ===');
720
- console.log('Current user:', user);
721
- console.log('Current user ID:', user?.id, 'Type:', typeof user?.id);
722
- console.log('JWT Token exists:', !!localStorage.getItem('token'));
723
-
724
- // Load items I'm lending (owned by me)
725
- const itemsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items?ownerId=${user?.id}`, {
726
- headers: {
727
- 'Content-Type': 'application/json',
728
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
729
- 'X-User-Id': user?.id?.toString() || ''
730
- }
731
- });
732
- if (itemsResponse.ok) {
733
- const itemsData = await itemsResponse.json();
734
- console.log('Items API response:', itemsData);
735
- // Backend returns paginated data with 'content' array, not 'data' array
736
- const allItems = itemsData.content || itemsData.data || [];
737
- console.log('All items from API:', allItems);
738
- console.log('Query URL was:', `${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/items?ownerId=${user?.id}`);
739
-
740
- // Filter items to only show items owned by current user (frontend filtering as backup)
741
- const myOwnedItems = allItems.filter((item: any) => {
742
- console.log(`Comparing item.ownerId (${item.ownerId}) === user.id (${user?.id}):`, item.ownerId === user?.id);
743
- return item.ownerId === user?.id || item.owner_id === user?.id;
744
- });
745
- console.log('Filtered myItems to:', myOwnedItems);
746
- setMyItems(myOwnedItems);
747
- } else {
748
- console.log('Items API failed:', itemsResponse.status);
749
- }
750
-
751
- // Try to load my bookings (items I've borrowed) - gracefully handle server errors
752
- try {
753
- console.log('Fetching my bookings for user:', user?.id);
754
-
755
- // Use only confirmed working endpoint from integration guide
756
- let bookingsResponse;
757
- let endpointUsed = '/api/bookings/my-bookings';
758
-
759
- try {
760
- bookingsResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/my-bookings`, {
761
- headers: {
762
- 'Content-Type': 'application/json',
763
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
764
- 'X-User-Id': user?.id?.toString() || ''
765
- }
766
- });
767
- } catch (fetchError) {
768
- console.log('Error trying booking endpoints:', fetchError);
769
- setMyBookings([]);
770
- return;
771
- }
772
-
773
- console.log('Bookings response status:', bookingsResponse.status, 'from endpoint:', endpointUsed);
774
-
775
- if (bookingsResponse.ok) {
776
- const bookingsData = await bookingsResponse.json();
777
- console.log('✅ BOOKINGS SUCCESS - API response:', bookingsData);
778
- console.log('Bookings response type:', typeof bookingsData, 'is array:', Array.isArray(bookingsData));
779
- // Handle both paginated (content) and direct array responses
780
- const bookings = bookingsData.content || bookingsData.data || bookingsData || [];
781
- console.log('Final bookings array:', bookings, 'length:', bookings.length);
782
-
783
- // Note: Removed debugging call to /api/bookings (405 Method Not Allowed)
784
- // The /api/bookings endpoint only supports POST for creating bookings, not GET
785
- console.log('� Successfully loaded user bookings:', bookings.length, 'items');
786
-
787
- setMyBookings(bookings);
788
- } else {
789
- const errorText = await bookingsResponse.text();
790
- console.log('All booking endpoints failed. Last error:', bookingsResponse.status, 'response:', errorText);
791
- setMyBookings([]);
792
- }
793
- } catch (error) {
794
- console.log('Bookings endpoint failed:', error);
795
- setMyBookings([]);
796
- }
797
-
798
- // Try to load bookings received (people wanting to borrow my items) - gracefully handle server errors
799
- try {
800
- console.log('Fetching received bookings for owner:', user?.id);
801
-
802
- // Use only confirmed working endpoint from integration guide
803
- let receivedResponse;
804
- let ownerEndpointUsed = '/api/bookings/my-owned';
805
-
806
- try {
807
- receivedResponse = await fetch(`${import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'}/api/bookings/my-owned`, {
808
- headers: {
809
- 'Content-Type': 'application/json',
810
- 'Authorization': `Bearer ${localStorage.getItem('token')}`,
811
- 'X-User-Id': user?.id?.toString() || ''
812
- }
813
- });
814
- } catch (fetchError) {
815
- console.log('Error trying owner booking endpoints:', fetchError);
816
- setReceivedBookings([]);
817
- return;
818
- }
819
-
820
- console.log('Received bookings response status:', receivedResponse.status, 'from endpoint:', ownerEndpointUsed);
821
-
822
- if (receivedResponse.ok) {
823
- const receivedData = await receivedResponse.json();
824
- console.log('Received bookings API response:', receivedData);
825
- console.log('Received bookings response type:', typeof receivedData, 'is array:', Array.isArray(receivedData));
826
- // Handle both paginated (content) and direct array responses
827
- const bookings = receivedData.content || receivedData.data || receivedData || [];
828
- console.log('Setting receivedBookings to:', bookings, 'length:', bookings.length);
829
- setReceivedBookings(bookings);
830
- } else {
831
- const errorText = await receivedResponse.text();
832
- console.log('All owner booking endpoints failed. Last error:', receivedResponse.status, 'response:', errorText);
833
- setReceivedBookings([]);
834
- }
835
- } catch (error) {
836
- console.log('Received bookings endpoint failed:', error);
837
- setReceivedBookings([]);
838
- }
839
- } catch (error) {
840
- console.error('Error loading dashboard data:', error);
841
- // Set empty arrays if there's a general error
842
- setMyItems([]);
843
- setMyBookings([]);
844
- setReceivedBookings([]);
845
- } finally {
846
- setIsLoading(false);
847
- }
848
- };
849
-
850
- if (user) {
851
- loadDashboardData();
852
- }
853
- }, [user]);
854
-
855
- // Debug: Log current state values
856
- console.log('Dashboard render - myItems:', myItems, 'myBookings:', myBookings, 'receivedBookings:', receivedBookings);
857
-
858
- return (
859
- <div style={{
860
- fontFamily: 'Roboto, Arial, sans-serif',
861
- margin: '0',
862
- padding: '0',
863
- backgroundColor: '#f5f5f5',
864
- color: '#213547',
865
- minHeight: '100vh'
866
- }}>
867
- {/* Navigation Bar */}
868
- <nav style={{
869
- backgroundColor: '#1976d2',
870
- color: 'white',
871
- padding: '12px 0',
872
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
873
- marginBottom: '20px'
874
- }}>
875
- <div style={{
876
- maxWidth: '1200px',
877
- margin: '0 auto',
878
- padding: '0 20px',
879
- display: 'flex',
880
- alignItems: 'center',
881
- justifyContent: 'space-between'
882
- }}>
883
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
884
- <button
885
- onClick={onBack}
886
- style={{
887
- color: 'white',
888
- textDecoration: 'none',
889
- background: 'none',
890
- border: 'none',
891
- cursor: 'pointer',
892
- fontSize: '1.5rem',
893
- fontWeight: 'bold'
894
- }}
895
- >
896
- LocalLend
897
- </button>
898
- </div>
899
-
900
- <span style={{
901
- color: 'white',
902
- fontSize: '14px',
903
- padding: '8px 12px',
904
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
905
- borderRadius: '4px'
906
- }}>
907
- Welcome, {user?.name}!
908
- </span>
909
- </div>
910
- </nav>
911
-
912
- {/* Dashboard Content */}
913
- <div style={{ padding: '0 20px', maxWidth: '1200px', margin: '0 auto' }}>
914
- <h1 style={{ marginBottom: '30px', color: '#1976d2' }}>Dashboard</h1>
915
-
916
- {isLoading ? (
917
- <div style={{ textAlign: 'center', padding: '40px' }}>
918
- <p>Loading your dashboard...</p>
919
- </div>
920
- ) : (
921
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', gap: '20px' }}>
922
-
923
- {/* My Items Section */}
924
- <div style={{
925
- backgroundColor: 'white',
926
- padding: '20px',
927
- borderRadius: '8px',
928
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
929
- }}>
930
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
931
- <h2 style={{ color: '#1976d2', margin: 0 }}>My Items ({myItems.length})</h2>
932
- {onAddItem && (
933
- <button
934
- onClick={onAddItem}
935
- style={{
936
- padding: '8px 16px',
937
- backgroundColor: '#1976d2',
938
- color: 'white',
939
- border: 'none',
940
- borderRadius: '4px',
941
- cursor: 'pointer',
942
- fontSize: '14px',
943
- fontWeight: 'bold'
944
- }}
945
- >
946
- + Add Item
947
- </button>
948
- )}
949
- </div>
950
- <p style={{ color: '#666', marginBottom: '15px', fontSize: '14px' }}>Items you're lending to others</p>
951
-
952
- {myItems.length === 0 ? (
953
- <div style={{ textAlign: 'center', padding: '40px 20px' }}>
954
- <p style={{ color: '#888', fontStyle: 'italic', marginBottom: '15px' }}>No items listed yet. Start lending!</p>
955
- {onAddItem && (
956
- <button
957
- onClick={onAddItem}
958
- style={{
959
- padding: '12px 24px',
960
- backgroundColor: '#1976d2',
961
- color: 'white',
962
- border: 'none',
963
- borderRadius: '6px',
964
- cursor: 'pointer',
965
- fontSize: '16px',
966
- fontWeight: 'bold'
967
- }}
968
- >
969
- List Your First Item
970
- </button>
971
- )}
972
- </div>
973
- ) : (
974
- <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
975
- {myItems.map((item) => (
976
- <div key={item.id} style={{
977
- border: '1px solid #e0e0e0',
978
- borderRadius: '8px',
979
- padding: '16px',
980
- marginBottom: '12px',
981
- backgroundColor: '#fafafa',
982
- position: 'relative'
983
- }}>
984
- <div style={{ display: 'flex', gap: '12px' }}>
985
- {/* Item Image */}
986
- {item.images && item.images[0] && (
987
- <img
988
- src={item.images[0]}
989
- alt={item.name}
990
- style={{
991
- width: '80px',
992
- height: '80px',
993
- objectFit: 'cover',
994
- borderRadius: '6px',
995
- flexShrink: 0
996
- }}
997
- />
998
- )}
999
-
1000
- {/* Item Details */}
1001
- <div style={{ flex: 1, minWidth: 0 }}>
1002
- <h4 style={{ margin: '0 0 6px 0', color: '#333', fontSize: '16px', fontWeight: 'bold' }}>
1003
- {item.name || item.title}
1004
- </h4>
1005
-
1006
- <p style={{
1007
- margin: '0 0 8px 0',
1008
- color: '#666',
1009
- fontSize: '14px',
1010
- lineHeight: '1.4',
1011
- overflow: 'hidden',
1012
- textOverflow: 'ellipsis',
1013
- display: '-webkit-box',
1014
- WebkitLineClamp: 2,
1015
- WebkitBoxOrient: 'vertical'
1016
- }}>
1017
- {item.description}
1018
- </p>
1019
-
1020
- {/* Item Metadata */}
1021
- <div style={{ marginBottom: '10px' }}>
1022
- {item.categoryName && (
1023
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>
1024
- 📂 Category: <strong>{item.categoryName}</strong>
1025
- </div>
1026
- )}
1027
-
1028
- {item.deposit > 0 && (
1029
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>
1030
- 💰 Deposit: <strong>₹${item.deposit}</strong>
1031
- </div>
1032
- )}
1033
-
1034
- {item.averageRating > 0 && (
1035
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>
1036
- ⭐ Rating: <strong>{item.averageRating.toFixed(1)}/5</strong>
1037
- </div>
1038
- )}
1039
-
1040
- {item.createdAt && (
1041
- <div style={{ fontSize: '12px', color: '#888' }}>
1042
- 📅 Listed: <strong>{new Date(item.createdAt).toLocaleDateString()}</strong>
1043
- </div>
1044
- )}
1045
- </div>
1046
-
1047
- {/* Status and Condition Badges */}
1048
- <div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
1049
- <span style={{
1050
- padding: '4px 10px',
1051
- borderRadius: '12px',
1052
- fontSize: '12px',
1053
- fontWeight: 'bold',
1054
- backgroundColor: item.status === 'AVAILABLE' ? '#4caf50' : item.status === 'BORROWED' ? '#ff5722' : '#ff9800',
1055
- color: 'white'
1056
- }}>
1057
- {item.status || 'UNKNOWN'}
1058
- </span>
1059
- <span style={{
1060
- padding: '4px 10px',
1061
- borderRadius: '12px',
1062
- fontSize: '12px',
1063
- fontWeight: 'bold',
1064
- backgroundColor: '#2196f3',
1065
- color: 'white'
1066
- }}>
1067
- {item.condition || 'N/A'}
1068
- </span>
1069
- {item.canBeBorrowed === false && (
1070
- <span style={{
1071
- padding: '4px 10px',
1072
- borderRadius: '12px',
1073
- fontSize: '12px',
1074
- fontWeight: 'bold',
1075
- backgroundColor: '#9e9e9e',
1076
- color: 'white'
1077
- }}>
1078
- Not Borrowable
1079
- </span>
1080
- )}
1081
- </div>
1082
-
1083
- {/* Action Buttons */}
1084
- <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
1085
- <button
1086
- onClick={() => handleToggleStatus(item.id, item.status)}
1087
- style={{
1088
- padding: '6px 12px',
1089
- fontSize: '12px',
1090
- border: '1px solid #1976d2',
1091
- backgroundColor: 'transparent',
1092
- color: '#1976d2',
1093
- borderRadius: '4px',
1094
- cursor: 'pointer',
1095
- fontWeight: 'bold'
1096
- }}
1097
- title={`Change status to ${item.status === 'AVAILABLE' ? 'UNAVAILABLE' : 'AVAILABLE'}`}
1098
- >
1099
- {item.status === 'AVAILABLE' ? '⏸️ Make Unavailable' : '▶️ Make Available'}
1100
- </button>
1101
-
1102
- <button
1103
- onClick={() => handleDeleteItem(item.id, item.name)}
1104
- disabled={deletingItemId === item.id}
1105
- style={{
1106
- padding: '6px 12px',
1107
- fontSize: '12px',
1108
- border: '1px solid #f44336',
1109
- backgroundColor: deletingItemId === item.id ? '#ffcdd2' : 'transparent',
1110
- color: deletingItemId === item.id ? '#666' : '#f44336',
1111
- borderRadius: '4px',
1112
- cursor: deletingItemId === item.id ? 'not-allowed' : 'pointer',
1113
- fontWeight: 'bold'
1114
- }}
1115
- title="Delete this item permanently"
1116
- >
1117
- {deletingItemId === item.id ? '🗑️ Deleting...' : '🗑️ Delete'}
1118
- </button>
1119
- </div>
1120
- </div>
1121
- </div>
1122
- </div>
1123
- ))}
1124
- </div>
1125
- )}
1126
- </div>
1127
-
1128
- {/* My Bookings Section */}
1129
- <div style={{
1130
- backgroundColor: 'white',
1131
- padding: '20px',
1132
- borderRadius: '8px',
1133
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
1134
- }}>
1135
- <h2 style={{ color: '#1976d2', marginBottom: '15px' }}>My Bookings ({myBookings.length})</h2>
1136
- <p style={{ color: '#666', marginBottom: '15px', fontSize: '14px' }}>Items you've borrowed or are borrowing from others</p>
1137
-
1138
- {myBookings.length === 0 ? (
1139
- <div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
1140
- <p style={{ fontSize: '48px', margin: '0 0 10px 0' }}>📋</p>
1141
- <p style={{ fontStyle: 'italic', fontSize: '16px' }}>No bookings yet</p>
1142
- <p style={{ fontSize: '14px', margin: '10px 0 0 0' }}>Browse items to make your first booking!</p>
1143
- </div>
1144
- ) : (
1145
- <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
1146
- {myBookings.map((booking) => {
1147
- const getStatusColor = (status: string) => {
1148
- switch(status?.toUpperCase()) {
1149
- case 'PENDING': return '#ff9800';
1150
- case 'CONFIRMED': case 'APPROVED': return '#4caf50';
1151
- case 'ACTIVE': return '#2196f3';
1152
- case 'COMPLETED': return '#9c27b0';
1153
- case 'CANCELLED': case 'REJECTED': return '#f44336';
1154
- default: return '#757575';
1155
- }
1156
- };
1157
-
1158
- const getStatusIcon = (status: string) => {
1159
- switch(status?.toUpperCase()) {
1160
- case 'PENDING': return '⏳';
1161
- case 'CONFIRMED': case 'APPROVED': return '✅';
1162
- case 'ACTIVE': return '📦';
1163
- case 'COMPLETED': return '✨';
1164
- case 'CANCELLED': case 'REJECTED': return '❌';
1165
- default: return '📋';
1166
- }
1167
- };
1168
-
1169
- return (
1170
- <div key={booking.id} style={{
1171
- border: '1px solid #e0e0e0',
1172
- borderRadius: '8px',
1173
- padding: '16px',
1174
- marginBottom: '12px',
1175
- backgroundColor: '#fafafa',
1176
- position: 'relative'
1177
- }}>
1178
- {/* Header with Item and Status */}
1179
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '12px' }}>
1180
- <div style={{ flex: 1 }}>
1181
- <h4 style={{ margin: '0 0 6px 0', color: '#333', fontSize: '16px', fontWeight: 'bold' }}>
1182
- {booking.itemName || booking.itemTitle || 'Unknown Item'}
1183
- </h4>
1184
- <div style={{ fontSize: '13px', color: '#666', marginBottom: '4px' }}>
1185
- 👤 Owner: <strong>{booking.ownerName || 'Unknown Owner'}</strong>
1186
- </div>
1187
- </div>
1188
- <span style={{
1189
- padding: '6px 12px',
1190
- borderRadius: '16px',
1191
- backgroundColor: getStatusColor(booking.status),
1192
- color: 'white',
1193
- fontSize: '12px',
1194
- fontWeight: 'bold',
1195
- display: 'flex',
1196
- alignItems: 'center',
1197
- gap: '4px'
1198
- }}>
1199
- {getStatusIcon(booking.status)} {booking.status || 'UNKNOWN'}
1200
- </span>
1201
- </div>
1202
-
1203
- {/* Booking Details */}
1204
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '12px', marginBottom: '12px' }}>
1205
- {/* Dates */}
1206
- <div>
1207
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>📅 Booking Period</div>
1208
- <div style={{ fontSize: '14px', color: '#333', fontWeight: '500' }}>
1209
- {booking.startDate && booking.endDate ? (
1210
- <>
1211
- <div>From: {new Date(booking.startDate).toLocaleDateString()}</div>
1212
- <div>To: {new Date(booking.endDate).toLocaleDateString()}</div>
1213
- </>
1214
- ) : (
1215
- <div>{booking.startDate} - {booking.endDate}</div>
1216
- )}
1217
- </div>
1218
- </div>
1219
-
1220
- {/* Deposit Info */}
1221
- {(booking.deposit || booking.itemDeposit) && (
1222
- <div>
1223
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>💰 Deposit</div>
1224
- <div style={{ fontSize: '14px', color: '#333', fontWeight: '500' }}>
1225
- ₹${booking.deposit || booking.itemDeposit || 0}
1226
- </div>
1227
- </div>
1228
- )}
1229
-
1230
- {/* Booking Date */}
1231
- {booking.createdAt && (
1232
- <div>
1233
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>📝 Requested</div>
1234
- <div style={{ fontSize: '14px', color: '#333', fontWeight: '500' }}>
1235
- {new Date(booking.createdAt).toLocaleDateString()}
1236
- </div>
1237
- </div>
1238
- )}
1239
- </div>
1240
-
1241
- {/* Notes */}
1242
- {(booking.notes || booking.borrowerNotes || booking.ownerNotes) && (
1243
- <div style={{ marginBottom: '12px' }}>
1244
- {booking.notes && (
1245
- <div style={{ marginBottom: '6px' }}>
1246
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '2px' }}>💬 Your Notes</div>
1247
- <div style={{
1248
- fontSize: '13px',
1249
- color: '#555',
1250
- backgroundColor: '#f0f7ff',
1251
- padding: '8px',
1252
- borderRadius: '6px',
1253
- border: '1px solid #e3f2fd'
1254
- }}>
1255
- "{booking.notes}"
1256
- </div>
1257
- </div>
1258
- )}
1259
- {booking.ownerNotes && (
1260
- <div>
1261
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '2px' }}>📋 Owner Notes</div>
1262
- <div style={{
1263
- fontSize: '13px',
1264
- color: '#555',
1265
- backgroundColor: '#fff3e0',
1266
- padding: '8px',
1267
- borderRadius: '6px',
1268
- border: '1px solid #ffe0b2'
1269
- }}>
1270
- "{booking.ownerNotes}"
1271
- </div>
1272
- </div>
1273
- )}
1274
- </div>
1275
- )}
1276
-
1277
- {/* Action Buttons Based on Status */}
1278
- <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
1279
- {booking.status === 'CONFIRMED' && (
1280
- <button
1281
- onClick={() => handleStartBooking(booking.id, booking.itemName || booking.itemTitle || 'Unknown Item')}
1282
- style={{
1283
- padding: '8px 16px',
1284
- backgroundColor: '#2196f3',
1285
- color: 'white',
1286
- border: 'none',
1287
- borderRadius: '6px',
1288
- cursor: 'pointer',
1289
- fontSize: '13px',
1290
- fontWeight: '500'
1291
- }}>
1292
- 🚀 Start Booking
1293
- </button>
1294
- )}
1295
- {booking.status === 'ACTIVE' && (
1296
- <button
1297
- onClick={() => handleCompleteBooking(booking.id, booking.itemName || booking.itemTitle || 'Unknown Item')}
1298
- style={{
1299
- padding: '8px 16px',
1300
- backgroundColor: '#9c27b0',
1301
- color: 'white',
1302
- border: 'none',
1303
- borderRadius: '6px',
1304
- cursor: 'pointer',
1305
- fontSize: '13px',
1306
- fontWeight: '500'
1307
- }}>
1308
- ✅ Complete Booking
1309
- </button>
1310
- )}
1311
- {(booking.status === 'PENDING' || booking.status === 'CONFIRMED') && (
1312
- <button
1313
- onClick={() => handleCancelBooking(booking.id, booking.itemName || booking.itemTitle || 'Unknown Item')}
1314
- style={{
1315
- padding: '8px 16px',
1316
- backgroundColor: '#f44336',
1317
- color: 'white',
1318
- border: 'none',
1319
- borderRadius: '6px',
1320
- cursor: 'pointer',
1321
- fontSize: '13px',
1322
- fontWeight: '500'
1323
- }}>
1324
- ❌ Cancel
1325
- </button>
1326
- )}
1327
- <button
1328
- onClick={() => handleViewBookingDetails(booking)}
1329
- style={{
1330
- padding: '8px 16px',
1331
- backgroundColor: 'white',
1332
- color: '#1976d2',
1333
- border: '1px solid #1976d2',
1334
- borderRadius: '6px',
1335
- cursor: 'pointer',
1336
- fontSize: '13px',
1337
- fontWeight: '500'
1338
- }}>
1339
- 📄 View Details
1340
- </button>
1341
- </div>
1342
- </div>
1343
- );
1344
- })}
1345
- </div>
1346
- )}
1347
- </div>
1348
-
1349
- {/* Received Booking Requests Section */}
1350
- <div style={{
1351
- backgroundColor: 'white',
1352
- padding: '20px',
1353
- borderRadius: '8px',
1354
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
1355
- gridColumn: 'span 2'
1356
- }}>
1357
- <h2 style={{ color: '#1976d2', marginBottom: '15px' }}>Booking Requests ({receivedBookings.length})</h2>
1358
- <p style={{ color: '#666', marginBottom: '15px', fontSize: '14px' }}>People wanting to borrow your items - Manage approvals and track rentals</p>
1359
-
1360
- {receivedBookings.length === 0 ? (
1361
- <div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
1362
- <p style={{ fontSize: '48px', margin: '0 0 10px 0' }}>📬</p>
1363
- <p style={{ fontStyle: 'italic', fontSize: '16px' }}>No booking requests yet</p>
1364
- <p style={{ fontSize: '14px', margin: '10px 0 0 0' }}>Your items will appear here when people want to borrow them!</p>
1365
- </div>
1366
- ) : (
1367
- <div style={{ maxHeight: '500px', overflowY: 'auto' }}>
1368
- {receivedBookings.map((booking) => {
1369
- const getStatusColor = (status: any) => {
1370
- switch(status?.toUpperCase()) {
1371
- case 'PENDING': return '#ff9800';
1372
- case 'CONFIRMED': case 'APPROVED': return '#4caf50';
1373
- case 'ACTIVE': return '#2196f3';
1374
- case 'COMPLETED': return '#9c27b0';
1375
- case 'CANCELLED': case 'REJECTED': return '#f44336';
1376
- default: return '#757575';
1377
- }
1378
- };
1379
-
1380
- const getStatusIcon = (status: any) => {
1381
- switch(status?.toUpperCase()) {
1382
- case 'PENDING': return '⏳';
1383
- case 'CONFIRMED': case 'APPROVED': return '✅';
1384
- case 'ACTIVE': return '📦';
1385
- case 'COMPLETED': return '✨';
1386
- case 'CANCELLED': case 'REJECTED': return '❌';
1387
- default: return '📋';
1388
- }
1389
- };
1390
-
1391
- const isPending = booking.status?.toUpperCase() === 'PENDING';
1392
- const isActive = booking.status?.toUpperCase() === 'ACTIVE';
1393
-
1394
- return (
1395
- <div key={booking.id} style={{
1396
- border: isPending ? '2px solid #ff9800' : '1px solid #e0e0e0',
1397
- borderRadius: '12px',
1398
- padding: '20px',
1399
- marginBottom: '16px',
1400
- backgroundColor: isPending ? '#fff8e1' : '#fafafa',
1401
- position: 'relative'
1402
- }}>
1403
- {/* Urgent Indicator for Pending */}
1404
- {isPending && (
1405
- <div style={{
1406
- position: 'absolute',
1407
- top: '12px',
1408
- right: '12px',
1409
- backgroundColor: '#ff9800',
1410
- color: 'white',
1411
- padding: '4px 8px',
1412
- borderRadius: '12px',
1413
- fontSize: '11px',
1414
- fontWeight: 'bold'
1415
- }}>
1416
- 🚨 NEEDS APPROVAL
1417
- </div>
1418
- )}
1419
-
1420
- {/* Header with Item and Borrower */}
1421
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
1422
- <div style={{ flex: 1, paddingRight: isPending ? '120px' : '0' }}>
1423
- <h4 style={{ margin: '0 0 8px 0', color: '#333', fontSize: '18px', fontWeight: 'bold' }}>
1424
- 📦 {booking.itemName || booking.itemTitle || 'Unknown Item'}
1425
- </h4>
1426
- <div style={{ fontSize: '14px', color: '#666', marginBottom: '6px' }}>
1427
- 👤 Requested by: <strong style={{ color: '#333' }}>{booking.borrowerName || 'Unknown Borrower'}</strong>
1428
- </div>
1429
- {booking.borrowerEmail && (
1430
- <div style={{ fontSize: '13px', color: '#666', marginBottom: '6px' }}>
1431
- ✉️ Contact: <span style={{ color: '#1976d2' }}>{booking.borrowerEmail}</span>
1432
- </div>
1433
- )}
1434
- </div>
1435
- <span style={{
1436
- padding: '8px 14px',
1437
- borderRadius: '20px',
1438
- backgroundColor: getStatusColor(booking.status),
1439
- color: 'white',
1440
- fontSize: '13px',
1441
- fontWeight: 'bold',
1442
- display: 'flex',
1443
- alignItems: 'center',
1444
- gap: '6px'
1445
- }}>
1446
- {getStatusIcon(booking.status)} {booking.status || 'UNKNOWN'}
1447
- </span>
1448
- </div>
1449
-
1450
- {/* Booking Details Grid */}
1451
- <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: '16px', marginBottom: '16px' }}>
1452
- {/* Rental Period */}
1453
- <div>
1454
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '6px', fontWeight: '600' }}>📅 RENTAL PERIOD</div>
1455
- <div style={{ fontSize: '14px', color: '#333' }}>
1456
- {booking.startDate && booking.endDate ? (
1457
- <>
1458
- <div style={{ fontWeight: '500' }}>Start: {new Date(booking.startDate).toLocaleDateString()}</div>
1459
- <div style={{ fontWeight: '500' }}>End: {new Date(booking.endDate).toLocaleDateString()}</div>
1460
- <div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
1461
- ({Math.ceil((new Date(booking.endDate).getTime() - new Date(booking.startDate).getTime()) / (1000 * 3600 * 24))} days)
1462
- </div>
1463
- </>
1464
- ) : (
1465
- <div>{booking.startDate} - {booking.endDate}</div>
1466
- )}
1467
- </div>
1468
- </div>
1469
-
1470
- {/* Deposit & Financial */}
1471
- <div>
1472
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '6px', fontWeight: '600' }}>💰 FINANCIAL</div>
1473
- <div style={{ fontSize: '14px', color: '#333' }}>
1474
- {(booking.deposit || booking.itemDeposit) ? (
1475
- <div style={{ fontWeight: '500', color: '#4caf50' }}>Deposit: ₹${booking.deposit || booking.itemDeposit}</div>
1476
- ) : (
1477
- <div style={{ color: '#666' }}>No deposit required</div>
1478
- )}
1479
- </div>
1480
- </div>
1481
-
1482
- {/* Request Date */}
1483
- <div>
1484
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '6px', fontWeight: '600' }}>📝 REQUESTED</div>
1485
- <div style={{ fontSize: '14px', color: '#333', fontWeight: '500' }}>
1486
- {booking.createdAt ? new Date(booking.createdAt).toLocaleDateString() : 'Unknown'}
1487
- </div>
1488
- </div>
1489
-
1490
- {/* Borrower Rating */}
1491
- {booking.borrowerRating && (
1492
- <div>
1493
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '6px', fontWeight: '600' }}>⭐ BORROWER RATING</div>
1494
- <div style={{ fontSize: '14px', color: '#333', fontWeight: '500' }}>
1495
- {booking.borrowerRating.toFixed(1)}/5 ⭐
1496
- </div>
1497
- </div>
1498
- )}
1499
- </div>
1500
-
1501
- {/* Notes Section */}
1502
- {(booking.notes || booking.borrowerNotes) && (
1503
- <div style={{ marginBottom: '16px' }}>
1504
- <div style={{ fontSize: '12px', color: '#888', marginBottom: '6px', fontWeight: '600' }}>💬 BORROWER'S MESSAGE</div>
1505
- <div style={{
1506
- fontSize: '14px',
1507
- color: '#555',
1508
- backgroundColor: 'white',
1509
- padding: '12px',
1510
- borderRadius: '8px',
1511
- border: '1px solid #e0e0e0',
1512
- fontStyle: 'italic'
1513
- }}>
1514
- "{booking.notes || booking.borrowerNotes || 'No message provided.'}"
1515
- </div>
1516
- </div>
1517
- )}
1518
-
1519
- {/* Action Buttons */}
1520
- <div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', justifyContent: 'flex-end' }}>
1521
- {isPending && (
1522
- <>
1523
- <button
1524
- onClick={() => handleApproveBookingRequest(
1525
- booking.id,
1526
- booking.itemName || booking.itemTitle || 'Unknown Item',
1527
- booking.borrowerName || 'Unknown Borrower'
1528
- )}
1529
- style={{
1530
- padding: '10px 20px',
1531
- backgroundColor: '#4caf50',
1532
- color: 'white',
1533
- border: 'none',
1534
- borderRadius: '8px',
1535
- cursor: 'pointer',
1536
- fontSize: '14px',
1537
- fontWeight: '600',
1538
- display: 'flex',
1539
- alignItems: 'center',
1540
- gap: '6px'
1541
- }}>
1542
- ✅ Approve Request
1543
- </button>
1544
- <button
1545
- onClick={() => handleDeclineBookingRequest(
1546
- booking.id,
1547
- booking.itemName || booking.itemTitle || 'Unknown Item',
1548
- booking.borrowerName || 'Unknown Borrower'
1549
- )}
1550
- style={{
1551
- padding: '10px 20px',
1552
- backgroundColor: '#f44336',
1553
- color: 'white',
1554
- border: 'none',
1555
- borderRadius: '8px',
1556
- cursor: 'pointer',
1557
- fontSize: '14px',
1558
- fontWeight: '600',
1559
- display: 'flex',
1560
- alignItems: 'center',
1561
- gap: '6px'
1562
- }}>
1563
- ❌ Decline Request
1564
- </button>
1565
- </>
1566
- )}
1567
-
1568
- {isActive && (
1569
- <button
1570
- onClick={() => handleContactBorrower(booking)}
1571
- style={{
1572
- padding: '10px 20px',
1573
- backgroundColor: '#2196f3',
1574
- color: 'white',
1575
- border: 'none',
1576
- borderRadius: '8px',
1577
- cursor: 'pointer',
1578
- fontSize: '14px',
1579
- fontWeight: '600',
1580
- display: 'flex',
1581
- alignItems: 'center',
1582
- gap: '6px'
1583
- }}>
1584
- 📞 Contact Borrower
1585
- </button>
1586
- )}
1587
-
1588
- <button
1589
- onClick={() => handleViewBookingRequestDetails(booking)}
1590
- style={{
1591
- padding: '10px 20px',
1592
- backgroundColor: 'white',
1593
- color: '#1976d2',
1594
- border: '2px solid #1976d2',
1595
- borderRadius: '8px',
1596
- cursor: 'pointer',
1597
- fontSize: '14px',
1598
- fontWeight: '600',
1599
- display: 'flex',
1600
- alignItems: 'center',
1601
- gap: '6px'
1602
- }}>
1603
- 📄 View Full Details
1604
- </button>
1605
-
1606
- {booking.borrowerEmail && (
1607
- <button
1608
- onClick={() => handleSendMessage(booking)}
1609
- style={{
1610
- padding: '10px 20px',
1611
- backgroundColor: '#ff9800',
1612
- color: 'white',
1613
- border: 'none',
1614
- borderRadius: '8px',
1615
- cursor: 'pointer',
1616
- fontSize: '14px',
1617
- fontWeight: '600',
1618
- display: 'flex',
1619
- alignItems: 'center',
1620
- gap: '6px'
1621
- }}>
1622
- 💬 Send Message
1623
- </button>
1624
- )}
1625
- </div>
1626
- </div>
1627
- );
1628
- })}
1629
- </div>
1630
- )}
1631
- </div>
1632
-
1633
- </div>
1634
- )}
1635
-
1636
- {/* Quick Actions */}
1637
- <div style={{
1638
- marginTop: '30px',
1639
- display: 'flex',
1640
- gap: '15px',
1641
- flexWrap: 'wrap'
1642
- }}>
1643
- <button
1644
- onClick={onAddItem}
1645
- style={{
1646
- padding: '12px 24px',
1647
- backgroundColor: '#4caf50',
1648
- color: 'white',
1649
- border: 'none',
1650
- borderRadius: '4px',
1651
- cursor: 'pointer',
1652
- fontSize: '16px',
1653
- fontWeight: 'bold'
1654
- }}>
1655
- + Add New Item
1656
- </button>
1657
-
1658
- <button style={{
1659
- padding: '12px 24px',
1660
- backgroundColor: '#2196f3',
1661
- color: 'white',
1662
- border: 'none',
1663
- borderRadius: '4px',
1664
- cursor: 'pointer',
1665
- fontSize: '16px'
1666
- }}>
1667
- Browse Items
1668
- </button>
1669
-
1670
- <button onClick={onBack} style={{
1671
- padding: '12px 24px',
1672
- backgroundColor: '#757575',
1673
- color: 'white',
1674
- border: 'none',
1675
- borderRadius: '4px',
1676
- cursor: 'pointer',
1677
- fontSize: '16px'
1678
- }}>
1679
- Back to Home
1680
- </button>
1681
- </div>
1682
- </div>
1683
- </div>
1684
- );
1685
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/HomePage.tsx DELETED
@@ -1,297 +0,0 @@
1
- // Home/Dashboard Page Component
2
- // This displays items available for borrowing with search and filter functionality
3
-
4
- import React, { useState, useEffect } from 'react';
5
- import {
6
- Container,
7
- Grid,
8
- Card,
9
- CardMedia,
10
- CardContent,
11
- Typography,
12
- Box,
13
- FormControl,
14
- InputLabel,
15
- Select,
16
- MenuItem,
17
- Pagination,
18
- CircularProgress,
19
- Alert,
20
- Chip,
21
- Rating
22
- } from '@mui/material';
23
- import { useNavigate, useSearchParams } from 'react-router-dom';
24
- import { itemService } from '../services/itemService';
25
- import { categoryService } from '../services/categoryService';
26
- import { handleApiError } from '../services/api';
27
- import { formatCurrency } from '../utils/helpers';
28
- import { ITEM_CONDITIONS, DEFAULT_PAGE_SIZE } from '../utils/constants';
29
- import type { Item, Category, ItemCondition } from '../types';
30
-
31
- export const HomePage: React.FC = () => {
32
- const [items, setItems] = useState<Item[]>([]);
33
- const [categories, setCategories] = useState<Category[]>([]);
34
- const [loading, setLoading] = useState(true);
35
- const [error, setError] = useState('');
36
- const [totalPages, setTotalPages] = useState(1);
37
- const [totalItems, setTotalItems] = useState(0);
38
-
39
- const [searchParams, setSearchParams] = useSearchParams();
40
- const navigate = useNavigate();
41
-
42
- // Get current filters from URL
43
- const currentPage = parseInt(searchParams.get('page') || '1');
44
- const categoryId = searchParams.get('categoryId') || '';
45
- const condition = searchParams.get('condition') || '';
46
- const sort = searchParams.get('sort') || 'createdAt,desc';
47
- const query = searchParams.get('query') || '';
48
-
49
- useEffect(() => {
50
- fetchCategories();
51
- }, []);
52
-
53
- useEffect(() => {
54
- fetchItems();
55
- }, [searchParams]);
56
-
57
- const fetchCategories = async () => {
58
- try {
59
- const response = await categoryService.getCategories();
60
- setCategories(response.data || []);
61
- } catch (error) {
62
- console.error('Failed to fetch categories:', error);
63
- }
64
- };
65
-
66
- const fetchItems = async () => {
67
- setLoading(true);
68
- setError('');
69
-
70
- try {
71
- const params = {
72
- page: currentPage - 1, // Backend uses 0-based pagination
73
- size: DEFAULT_PAGE_SIZE,
74
- sort,
75
- ...(categoryId && { categoryId }),
76
- ...(condition && { condition: condition as ItemCondition }),
77
- ...(query && { query })
78
- };
79
-
80
- let response;
81
- if (query) {
82
- response = await itemService.searchItems(query, categoryId, condition as ItemCondition);
83
- } else {
84
- response = await itemService.getItems(params);
85
- }
86
-
87
- setItems(response.content || response.data || []);
88
- setTotalPages(response.totalPages || 1);
89
- setTotalItems(response.totalElements || response.count || 0);
90
- } catch (err) {
91
- setError(handleApiError(err));
92
- } finally {
93
- setLoading(false);
94
- }
95
- };
96
-
97
- const handleFilterChange = (filterName: string, value: string) => {
98
- const newParams = new URLSearchParams(searchParams);
99
-
100
- if (value) {
101
- newParams.set(filterName, value);
102
- } else {
103
- newParams.delete(filterName);
104
- }
105
-
106
- // Reset to page 1 when filters change
107
- newParams.set('page', '1');
108
-
109
- setSearchParams(newParams);
110
- };
111
-
112
- const handlePageChange = (_event: React.ChangeEvent<unknown>, page: number) => {
113
- const newParams = new URLSearchParams(searchParams);
114
- newParams.set('page', page.toString());
115
- setSearchParams(newParams);
116
- };
117
-
118
- const handleItemClick = (itemId: string) => {
119
- navigate(`/items/${itemId}`);
120
- };
121
-
122
- if (loading && items.length === 0) {
123
- return (
124
- <Container>
125
- <Box display="flex" justifyContent="center" mt={4}>
126
- <CircularProgress />
127
- </Box>
128
- </Container>
129
- );
130
- }
131
-
132
- return (
133
- <Container maxWidth="lg" sx={{ py: 4 }}>
134
- {/* Hero Section */}
135
- <Box textAlign="center" mb={6}>
136
- <Typography variant="h3" component="h1" gutterBottom>
137
- Find Items to Borrow in Your Community
138
- </Typography>
139
- <Typography variant="h6" color="text.secondary">
140
- LocalLend connects neighbors to share items and build community trust.
141
- </Typography>
142
- </Box>
143
-
144
- {/* Filters */}
145
- <Box mb={4}>
146
- <Grid container spacing={2} alignItems="center">
147
- <Grid item xs={12} sm={3}>
148
- <FormControl fullWidth>
149
- <InputLabel>Category</InputLabel>
150
- <Select
151
- value={categoryId}
152
- onChange={(e) => handleFilterChange('categoryId', e.target.value)}
153
- label="Category"
154
- >
155
- <MenuItem value="">All Categories</MenuItem>
156
- {categories.map((category) => (
157
- <MenuItem key={category.id} value={category.id}>
158
- {category.name}
159
- </MenuItem>
160
- ))}
161
- </Select>
162
- </FormControl>
163
- </Grid>
164
-
165
- <Grid item xs={12} sm={3}>
166
- <FormControl fullWidth>
167
- <InputLabel>Condition</InputLabel>
168
- <Select
169
- value={condition}
170
- onChange={(e) => handleFilterChange('condition', e.target.value)}
171
- label="Condition"
172
- >
173
- <MenuItem value="">All Conditions</MenuItem>
174
- {Object.values(ITEM_CONDITIONS).map((cond) => (
175
- <MenuItem key={cond} value={cond}>
176
- {cond}
177
- </MenuItem>
178
- ))}
179
- </Select>
180
- </FormControl>
181
- </Grid>
182
-
183
- <Grid item xs={12} sm={3}>
184
- <FormControl fullWidth>
185
- <InputLabel>Sort By</InputLabel>
186
- <Select
187
- value={sort}
188
- onChange={(e) => handleFilterChange('sort', e.target.value)}
189
- label="Sort By"
190
- >
191
- <MenuItem value="createdAt,desc">Newest First</MenuItem>
192
- <MenuItem value="name,asc">Name A-Z</MenuItem>
193
- <MenuItem value="averageRating,desc">Highest Rated</MenuItem>
194
- </Select>
195
- </FormControl>
196
- </Grid>
197
-
198
- <Grid item xs={12} sm={3}>
199
- <Typography variant="body2" color="text.secondary">
200
- {totalItems} items found
201
- </Typography>
202
- </Grid>
203
- </Grid>
204
- </Box>
205
-
206
- {error && (
207
- <Alert severity="error" sx={{ mb: 3 }}>
208
- {error}
209
- </Alert>
210
- )}
211
-
212
- {/* Items Grid */}
213
- <Grid container spacing={3}>
214
- {items.map((item) => (
215
- <Grid item xs={12} sm={6} md={4} lg={3} key={item.id}>
216
- <Card
217
- sx={{
218
- height: '100%',
219
- cursor: 'pointer',
220
- '&:hover': { transform: 'translateY(-4px)' },
221
- transition: 'transform 0.2s'
222
- }}
223
- onClick={() => handleItemClick(item.id)}
224
- >
225
- <CardMedia
226
- component="img"
227
- height="200"
228
- image={item.images?.[0] || '/placeholder-image.jpg'}
229
- alt={item.name}
230
- />
231
- <CardContent>
232
- <Typography gutterBottom variant="h6" component="h3" noWrap>
233
- {item.name}
234
- </Typography>
235
- <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
236
- by {item.ownerName}
237
- </Typography>
238
- <Typography variant="body2" sx={{ mb: 2, height: 40, overflow: 'hidden' }}>
239
- {item.description}
240
- </Typography>
241
-
242
- <Box display="flex" flexDirection="column" gap={1}>
243
- <Box display="flex" justifyContent="space-between" alignItems="center">
244
- <Chip
245
- label={item.condition}
246
- size="small"
247
- color={item.condition === 'NEW' ? 'success' : 'default'}
248
- />
249
- <Box display="flex" alignItems="center" gap={0.5}>
250
- <Rating value={item.averageRating} readOnly size="small" precision={0.1} />
251
- <Typography variant="caption">
252
- ({item.averageRating?.toFixed(1)})
253
- </Typography>
254
- </Box>
255
- </Box>
256
-
257
- <Typography variant="h6" color="primary">
258
- {item.deposit > 0 ? `${formatCurrency(item.deposit)} deposit` : 'No deposit'}
259
- </Typography>
260
- </Box>
261
- </CardContent>
262
- </Card>
263
- </Grid>
264
- ))}
265
- </Grid>
266
-
267
- {items.length === 0 && !loading && (
268
- <Box textAlign="center" py={8}>
269
- <Typography variant="h6" color="text.secondary">
270
- No items found matching your criteria.
271
- </Typography>
272
- <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
273
- Try adjusting your filters or search terms.
274
- </Typography>
275
- </Box>
276
- )}
277
-
278
- {/* Pagination */}
279
- {totalPages > 1 && (
280
- <Box display="flex" justifyContent="center" mt={4}>
281
- <Pagination
282
- count={totalPages}
283
- page={currentPage}
284
- onChange={handlePageChange}
285
- color="primary"
286
- />
287
- </Box>
288
- )}
289
-
290
- {loading && items.length > 0 && (
291
- <Box display="flex" justifyContent="center" mt={2}>
292
- <CircularProgress size={24} />
293
- </Box>
294
- )}
295
- </Container>
296
- );
297
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/LoginPage.tsx DELETED
@@ -1,254 +0,0 @@
1
- // Login Page Component
2
- import React, { useState } from 'react';
3
- import { useAuth } from '../context/AuthContext';
4
- import type { LoginRequest } from '../types';
5
-
6
- export interface LoginPageProps {
7
- onBack?: () => void;
8
- }
9
-
10
- export const LoginPage: React.FC<LoginPageProps> = ({ onBack }) => {
11
- const { login } = useAuth();
12
- const [email, setEmail] = useState('');
13
- const [password, setPassword] = useState('');
14
- const [isLoading, setIsLoading] = useState(false);
15
-
16
- const handleSubmit = async (e: React.FormEvent) => {
17
- e.preventDefault();
18
-
19
- setIsLoading(true);
20
- try {
21
- const loginData: LoginRequest = {
22
- usernameOrEmail: email, // Backend expects usernameOrEmail, so we pass email
23
- password: password
24
- };
25
-
26
- console.log('Login attempt:', loginData);
27
- await login(loginData);
28
-
29
- alert(`Login successful! Welcome back!`);
30
- // Navigation is handled centrally in App via auth state; no manual redirect needed here
31
- } catch (error: any) {
32
- console.error('Login failed:', error);
33
-
34
- // Check for network/CORS errors
35
- if (error.message === 'Failed to fetch') {
36
- alert('Network error: Cannot connect to backend at http://localhost:8080. Please check:\n1. Backend is running on port 8080\n2. CORS is properly configured\n3. No firewall blocking the connection');
37
- } else if (error.message === 'Unauthorized') {
38
- alert('Invalid username/email or password');
39
- } else {
40
- // Extract error message from various response formats
41
- const errorMessage = error?.response?.data?.message ||
42
- error?.response?.data?.error ||
43
- error?.data?.message ||
44
- error?.message ||
45
- 'Login failed. Please check your credentials.';
46
- alert(`Login failed: ${errorMessage}`);
47
- }
48
- } finally {
49
- setIsLoading(false);
50
- }
51
- };
52
-
53
- return (
54
- <div style={{
55
- minHeight: '100vh',
56
- display: 'flex',
57
- alignItems: 'center',
58
- justifyContent: 'center',
59
- backgroundColor: '#f5f5f5'
60
- }}>
61
- <div style={{
62
- backgroundColor: 'white',
63
- padding: '40px',
64
- borderRadius: '8px',
65
- boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
66
- width: '100%',
67
- maxWidth: '400px'
68
- }}>
69
- <h1 style={{ textAlign: 'center', marginBottom: '30px', color: '#1976d2' }}>
70
- Login to LocalLend
71
- </h1>
72
-
73
- <form onSubmit={handleSubmit}>
74
- <div style={{ marginBottom: '20px' }}>
75
- <label style={{
76
- display: 'block',
77
- marginBottom: '5px',
78
- fontWeight: 'bold',
79
- color: '#000',
80
- fontSize: '14px',
81
- fontFamily: 'Arial, sans-serif'
82
- }}>
83
- Email
84
- </label>
85
- <input
86
- type="email"
87
- value={email}
88
- onChange={(e) => setEmail(e.target.value)}
89
- required
90
- style={{
91
- width: '100%',
92
- padding: '12px',
93
- border: '1px solid #ddd',
94
- borderRadius: '4px',
95
- fontSize: '16px',
96
- boxSizing: 'border-box'
97
- }}
98
- />
99
- </div>
100
-
101
- <div style={{ marginBottom: '30px' }}>
102
- <label style={{
103
- display: 'block',
104
- marginBottom: '5px',
105
- fontWeight: 'bold',
106
- color: '#000',
107
- fontSize: '14px',
108
- fontFamily: 'Arial, sans-serif'
109
- }}>
110
- Password
111
- </label>
112
- <input
113
- type="password"
114
- value={password}
115
- onChange={(e) => setPassword(e.target.value)}
116
- required
117
- style={{
118
- width: '100%',
119
- padding: '12px',
120
- border: '1px solid #ddd',
121
- borderRadius: '4px',
122
- fontSize: '16px',
123
- boxSizing: 'border-box'
124
- }}
125
- />
126
- </div>
127
-
128
- <button
129
- type="submit"
130
- disabled={isLoading}
131
- style={{
132
- width: '100%',
133
- padding: '12px',
134
- backgroundColor: isLoading ? '#ccc' : '#1976d2',
135
- color: 'white',
136
- border: 'none',
137
- borderRadius: '4px',
138
- fontSize: '16px',
139
- cursor: isLoading ? 'not-allowed' : 'pointer',
140
- marginBottom: '20px'
141
- }}
142
- >
143
- {isLoading ? 'Logging In...' : 'Login'}
144
- </button>
145
- </form>
146
-
147
- <div style={{ textAlign: 'center' }}>
148
- <p style={{ margin: '10px 0' }}>
149
- Don't have an account?{' '}
150
- <button
151
- style={{ color: '#1976d2', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline' }}
152
- onClick={() => console.log('Navigate to register')}
153
- >
154
- Register here
155
- </button>
156
- </p>
157
- {onBack && (
158
- <button
159
- onClick={onBack}
160
- style={{ color: '#666', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline' }}
161
- >
162
- ← Back to Home
163
- </button>
164
- )}
165
- </div>
166
- </div>
167
- </div>
168
- );
169
- };
170
-
171
- // When React is properly installed, this will become:
172
- /*
173
- import React, { useState } from 'react';
174
- import { useNavigate, Link } from 'react-router-dom';
175
- import { useAuth } from '../context/AuthContext';
176
- import { handleApiError } from '../services/api';
177
-
178
- export const LoginPage: React.FC = () => {
179
- const [formData, setFormData] = useState({
180
- usernameOrEmail: '',
181
- password: ''
182
- });
183
- const [isLoading, setIsLoading] = useState(false);
184
- const [error, setError] = useState('');
185
-
186
- const { login } = useAuth();
187
- const navigate = useNavigate();
188
-
189
- const handleSubmit = async (e: React.FormEvent) => {
190
- e.preventDefault();
191
- setIsLoading(true);
192
- setError('');
193
-
194
- try {
195
- await login(formData);
196
- navigate('/dashboard');
197
- } catch (err) {
198
- setError(handleApiError(err));
199
- } finally {
200
- setIsLoading(false);
201
- }
202
- };
203
-
204
- return (
205
- <Container maxWidth="sm">
206
- <Box sx={{ mt: 8, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
207
- <Typography component="h1" variant="h4">
208
- Login to LocalLend
209
- </Typography>
210
-
211
- {error && (
212
- <Alert severity="error" sx={{ mt: 2, width: '100%' }}>
213
- {error}
214
- </Alert>
215
- )}
216
-
217
- <Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
218
- <TextField
219
- margin="normal"
220
- required
221
- fullWidth
222
- label="Username or Email"
223
- value={formData.usernameOrEmail}
224
- onChange={(e) => setFormData(prev => ({ ...prev, usernameOrEmail: e.target.value }))}
225
- />
226
- <TextField
227
- margin="normal"
228
- required
229
- fullWidth
230
- label="Password"
231
- type="password"
232
- value={formData.password}
233
- onChange={(e) => setFormData(prev => ({ ...prev, password: e.target.value }))}
234
- />
235
- <Button
236
- type="submit"
237
- fullWidth
238
- variant="contained"
239
- disabled={isLoading}
240
- sx={{ mt: 3, mb: 2 }}
241
- >
242
- {isLoading ? 'Logging in...' : 'Login'}
243
- </Button>
244
- <Box textAlign="center">
245
- <Link to="/register">
246
- Don't have an account? Register here
247
- </Link>
248
- </Box>
249
- </Box>
250
- </Box>
251
- </Container>
252
- );
253
- };
254
- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/MyBookingsPage.tsx DELETED
@@ -1,15 +0,0 @@
1
- import React from 'react';
2
-
3
- interface MyBookingsPageProps {
4
- onBack: () => void;
5
- }
6
-
7
- export const MyBookingsPage: React.FC<MyBookingsPageProps> = ({ onBack }) => (
8
- <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
9
- <h1>My Bookings</h1>
10
- <p>Here you can see all the items you've booked from other users.</p>
11
- <button onClick={onBack} style={{ padding: '10px 20px', backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
12
- Back to Home
13
- </button>
14
- </div>
15
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/MyItemsPage.tsx DELETED
@@ -1,15 +0,0 @@
1
- import React from 'react';
2
-
3
- interface MyItemsPageProps {
4
- onBack: () => void;
5
- }
6
-
7
- export const MyItemsPage: React.FC<MyItemsPageProps> = ({ onBack }) => (
8
- <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
9
- <h1>My Items</h1>
10
- <p>Here you can manage the items you're lending out to others.</p>
11
- <button onClick={onBack} style={{ padding: '10px 20px', backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>
12
- Back to Home
13
- </button>
14
- </div>
15
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/RegisterPage.tsx DELETED
@@ -1,419 +0,0 @@
1
- // Register Page Component
2
- import React, { useState } from 'react';
3
- import { useAuth } from '../context/AuthContext';
4
- import type { RegisterRequest } from '../types';
5
-
6
- export interface RegisterPageProps {
7
- onBack?: () => void;
8
- }
9
-
10
- export const RegisterPage: React.FC<RegisterPageProps> = ({ onBack }) => {
11
- const { register } = useAuth();
12
- const [formData, setFormData] = useState({
13
- username: '',
14
- name: '',
15
- email: '',
16
- phoneNumber: '',
17
- password: '',
18
- confirmPassword: ''
19
- });
20
- const [isLoading, setIsLoading] = useState(false);
21
-
22
- // Test backend connectivity
23
- const testBackendConnection = async () => {
24
- const backendUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
25
- console.log('🔍 Testing connection to:', backendUrl);
26
-
27
- try {
28
- // Test multiple endpoints to find what works
29
- const testEndpoints = [
30
- '/api/categories',
31
- '/api/auth/login', // This should exist according to the guide
32
- '/actuator/health', // Common Spring Boot health endpoint
33
- '/', // Basic root endpoint
34
- ];
35
-
36
- let successFound = false;
37
- let results = [];
38
-
39
- for (const endpoint of testEndpoints) {
40
- try {
41
- console.log(`Testing ${backendUrl}${endpoint}`);
42
- const response = await fetch(`${backendUrl}${endpoint}`, {
43
- method: 'GET',
44
- mode: 'cors',
45
- headers: {
46
- 'Content-Type': 'application/json',
47
- },
48
- });
49
-
50
- console.log(`Response for ${endpoint}:`, response.status, response.statusText);
51
- const responseText = await response.text();
52
- console.log(`Response body:`, responseText);
53
-
54
- results.push(`${endpoint}: ${response.status} ${response.statusText} - ${responseText.substring(0, 100)}`);
55
-
56
- if (response.ok) {
57
- successFound = true;
58
- alert(`✅ Backend connection successful!\nURL: ${backendUrl}${endpoint}\nStatus: ${response.status}\nResponse: ${responseText.substring(0, 200)}...\n\nBackend is reachable!`);
59
- break;
60
- }
61
- } catch (err: any) {
62
- console.error(`Error testing ${endpoint}:`, err);
63
- results.push(`${endpoint}: ${err.message}`);
64
- }
65
- }
66
-
67
- if (!successFound) {
68
- alert(`❌ Cannot connect to backend:\nURL: ${backendUrl}\n\nTest Results:\n${results.join('\n')}\n\nPossible issues:\n1. Backend not running on port 8080\n2. Backend running on different port\n3. CORS not configured\n4. Backend bound to localhost only`);
69
- }
70
-
71
- } catch (error: any) {
72
- console.error('Connection test failed:', error);
73
- alert(`❌ Network error:\nURL: ${backendUrl}\nError: ${error.message}\n\nCheck:\n1. Is backend running?\n2. What port is it on?\n3. Is Docker networking working?`);
74
- }
75
- };
76
-
77
- // Test alternative ports
78
- const testAlternativePort = async (port: number) => {
79
- const testUrl = `http://localhost:${port}`;
80
- console.log(`🔍 Testing alternative port: ${testUrl}`);
81
-
82
- try {
83
- const response = await fetch(`${testUrl}/api/categories`, {
84
- method: 'GET',
85
- mode: 'cors',
86
- });
87
-
88
- if (response.ok) {
89
- alert(`✅ Found backend on port ${port}!\nURL: ${testUrl}\nUpdate your .env file:\nVITE_API_BASE_URL=http://localhost:${port}`);
90
- } else {
91
- alert(`⚠️ Port ${port} responded with: ${response.status} ${response.statusText}`);
92
- }
93
- } catch (error: any) {
94
- console.log(`Port ${port}: ${error.message}`);
95
- alert(`❌ Port ${port}: ${error.message}`);
96
- }
97
- };
98
-
99
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
100
- setFormData({
101
- ...formData,
102
- [e.target.name]: e.target.value
103
- });
104
- };
105
-
106
- const handleSubmit = async (e: React.FormEvent) => {
107
- e.preventDefault();
108
-
109
- // Client-side validation to match backend requirements
110
- const validationErrors: string[] = [];
111
-
112
- if (formData.username.length < 3 || formData.username.length > 50) {
113
- validationErrors.push('Username must be between 3 and 50 characters');
114
- }
115
-
116
- if (formData.name.length < 2 || formData.name.length > 100) {
117
- validationErrors.push('Full name must be between 2 and 100 characters');
118
- }
119
-
120
- if (formData.password.length < 8) {
121
- validationErrors.push('Password must be at least 8 characters long');
122
- }
123
-
124
- if (formData.password !== formData.confirmPassword) {
125
- validationErrors.push('Passwords do not match');
126
- }
127
-
128
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
129
- if (!emailRegex.test(formData.email)) {
130
- validationErrors.push('Please enter a valid email address');
131
- }
132
-
133
- if (validationErrors.length > 0) {
134
- alert('Validation errors:\n' + validationErrors.join('\n'));
135
- return;
136
- }
137
-
138
- setIsLoading(true);
139
- try {
140
- // Prepare data for backend (excluding confirmPassword)
141
- const registerData: RegisterRequest = {
142
- username: formData.username,
143
- name: formData.name,
144
- email: formData.email,
145
- password: formData.password,
146
- phoneNumber: formData.phoneNumber || undefined
147
- };
148
-
149
- console.log('Registration attempt:', registerData);
150
- console.log('Attempting to connect to backend at:', import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080');
151
-
152
- await register(registerData);
153
-
154
- alert(`Registration successful! Welcome ${formData.name}!`);
155
-
156
- // Redirect back to home after successful registration
157
- if (onBack) {
158
- onBack();
159
- }
160
- } catch (error: any) {
161
- console.error('Registration failed:', error);
162
-
163
- // Check for network/CORS errors
164
- if (error.message === 'Failed to fetch') {
165
- alert('Network error: Cannot connect to backend at ' + (import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080') + '. Please check:\n1. Backend is running on port 8080\n2. CORS is properly configured\n3. Backend container is accessible from browser');
166
- } else if (error.message === 'Unauthorized') {
167
- alert('Authentication error: Please check your credentials');
168
- } else {
169
- // Extract error message from various response formats
170
- const errorMessage = error?.response?.data?.message ||
171
- error?.response?.data?.error ||
172
- error?.data?.message ||
173
- error?.message ||
174
- 'Registration failed. Please try again.';
175
- alert(`Registration failed: ${errorMessage}`);
176
- }
177
- } finally {
178
- setIsLoading(false);
179
- }
180
- };
181
-
182
- return (
183
- <div style={{
184
- minHeight: '100vh',
185
- display: 'flex',
186
- alignItems: 'center',
187
- justifyContent: 'center',
188
- backgroundColor: '#f5f5f5',
189
- padding: '20px'
190
- }}>
191
- <div style={{
192
- backgroundColor: 'white',
193
- padding: '40px',
194
- borderRadius: '8px',
195
- boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
196
- width: '100%',
197
- maxWidth: '500px'
198
- }}>
199
- <h1 style={{ textAlign: 'center', marginBottom: '30px', color: '#1976d2' }}>
200
- Join LocalLend
201
- </h1>
202
-
203
- {/* Backend Connection Test Buttons */}
204
- <div style={{ marginBottom: '20px' }}>
205
- <button
206
- type="button"
207
- onClick={testBackendConnection}
208
- style={{
209
- width: '100%',
210
- padding: '8px',
211
- backgroundColor: '#f0f0f0',
212
- color: '#666',
213
- border: '1px solid #ccc',
214
- borderRadius: '4px',
215
- fontSize: '14px',
216
- cursor: 'pointer',
217
- marginBottom: '8px'
218
- }}
219
- >
220
- 🔍 Test Backend Connection (Port 8080)
221
- </button>
222
-
223
- <div style={{ display: 'flex', gap: '5px' }}>
224
- {[8081, 8082, 9090, 3000].map(port => (
225
- <button
226
- key={port}
227
- type="button"
228
- onClick={() => testAlternativePort(port)}
229
- style={{
230
- flex: 1,
231
- padding: '6px',
232
- backgroundColor: '#e8f4fd',
233
- color: '#1976d2',
234
- border: '1px solid #1976d2',
235
- borderRadius: '3px',
236
- fontSize: '12px',
237
- cursor: 'pointer'
238
- }}
239
- >
240
- :{port}
241
- </button>
242
- ))}
243
- </div>
244
- </div>
245
-
246
- <form onSubmit={handleSubmit}>
247
- <div style={{ marginBottom: '20px' }}>
248
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#000' }}>
249
- Username
250
- </label>
251
- <input
252
- type="text"
253
- name="username"
254
- value={formData.username}
255
- onChange={handleChange}
256
- required
257
- minLength={3}
258
- maxLength={50}
259
- style={{
260
- width: '100%',
261
- padding: '12px',
262
- border: '1px solid #ddd',
263
- borderRadius: '4px',
264
- fontSize: '16px',
265
- boxSizing: 'border-box'
266
- }}
267
- />
268
- </div>
269
-
270
- <div style={{ marginBottom: '20px' }}>
271
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#000' }}>
272
- Full Name
273
- </label>
274
- <input
275
- type="text"
276
- name="name"
277
- value={formData.name}
278
- onChange={handleChange}
279
- required
280
- minLength={2}
281
- maxLength={100}
282
- style={{
283
- width: '100%',
284
- padding: '12px',
285
- border: '1px solid #ddd',
286
- borderRadius: '4px',
287
- fontSize: '16px',
288
- boxSizing: 'border-box'
289
- }}
290
- />
291
- </div>
292
-
293
- <div style={{ marginBottom: '20px' }}>
294
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#000' }}>
295
- Email
296
- </label>
297
- <input
298
- type="email"
299
- name="email"
300
- value={formData.email}
301
- onChange={handleChange}
302
- required
303
- style={{
304
- width: '100%',
305
- padding: '12px',
306
- border: '1px solid #ddd',
307
- borderRadius: '4px',
308
- fontSize: '16px',
309
- boxSizing: 'border-box'
310
- }}
311
- />
312
- </div>
313
-
314
- <div style={{ marginBottom: '20px' }}>
315
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#000' }}>
316
- Phone Number (Optional)
317
- </label>
318
- <input
319
- type="tel"
320
- name="phoneNumber"
321
- value={formData.phoneNumber}
322
- onChange={handleChange}
323
- style={{
324
- width: '100%',
325
- padding: '12px',
326
- border: '1px solid #ddd',
327
- borderRadius: '4px',
328
- fontSize: '16px',
329
- boxSizing: 'border-box'
330
- }}
331
- />
332
- </div>
333
-
334
- <div style={{ marginBottom: '20px' }}>
335
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#000' }}>
336
- Password (Minimum 8 characters)
337
- </label>
338
- <input
339
- type="password"
340
- name="password"
341
- value={formData.password}
342
- onChange={handleChange}
343
- required
344
- minLength={8}
345
- style={{
346
- width: '100%',
347
- padding: '12px',
348
- border: '1px solid #ddd',
349
- borderRadius: '4px',
350
- fontSize: '16px',
351
- boxSizing: 'border-box'
352
- }}
353
- />
354
- </div>
355
-
356
- <div style={{ marginBottom: '30px' }}>
357
- <label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#000' }}>
358
- Confirm Password
359
- </label>
360
- <input
361
- type="password"
362
- name="confirmPassword"
363
- value={formData.confirmPassword}
364
- onChange={handleChange}
365
- required
366
- minLength={8}
367
- style={{
368
- width: '100%',
369
- padding: '12px',
370
- border: '1px solid #ddd',
371
- borderRadius: '4px',
372
- fontSize: '16px',
373
- boxSizing: 'border-box'
374
- }}
375
- />
376
- </div>
377
-
378
- <button
379
- type="submit"
380
- disabled={isLoading}
381
- style={{
382
- width: '100%',
383
- padding: '12px',
384
- backgroundColor: isLoading ? '#ccc' : '#1976d2',
385
- color: 'white',
386
- border: 'none',
387
- borderRadius: '4px',
388
- fontSize: '16px',
389
- fontWeight: 'bold',
390
- cursor: isLoading ? 'not-allowed' : 'pointer',
391
- marginBottom: '10px'
392
- }}
393
- >
394
- {isLoading ? 'Signing Up...' : 'Sign Up'}
395
- </button>
396
-
397
- {onBack && (
398
- <button
399
- type="button"
400
- onClick={onBack}
401
- style={{
402
- width: '100%',
403
- padding: '12px',
404
- backgroundColor: '#f5f5f5',
405
- color: '#333',
406
- border: '1px solid #ddd',
407
- borderRadius: '4px',
408
- fontSize: '16px',
409
- cursor: 'pointer'
410
- }}
411
- >
412
- Back to Home
413
- </button>
414
- )}
415
- </form>
416
- </div>
417
- </div>
418
- );
419
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/pages/SearchResultsPage.tsx DELETED
@@ -1,445 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { useAuth } from '../context/AuthContext';
3
-
4
- interface SearchResultsPageProps {
5
- onBack: () => void;
6
- searchQuery: string;
7
- onSearchQueryChange: (query: string) => void;
8
- onBookItem: (item: any) => void;
9
- }
10
-
11
- export const SearchResultsPage: React.FC<SearchResultsPageProps> = ({ onBack, searchQuery, onSearchQueryChange, onBookItem }) => {
12
- console.log('🔍🔍🔍 SearchResultsPage rendered with onBookItem:', onBookItem);
13
- console.log('🔍🔍🔍 SearchResultsPage component loaded successfully');
14
-
15
- useEffect(() => {
16
- console.log('🔍🔍🔍 SearchResultsPage useEffect - this should show when page loads');
17
- }, []);
18
-
19
- const { user } = useAuth();
20
- const [searchResults, setSearchResults] = useState<any[]>([]);
21
- const [isLoading, setIsLoading] = useState(true);
22
- const [selectedCategory, setSelectedCategory] = useState('');
23
-
24
- // Helpers for richer display
25
- const formatCurrency = (amount: any) => {
26
- const n = Number(amount ?? 0);
27
- if (!isFinite(n)) return '₹0.00';
28
- return n.toLocaleString('en-IN', { style: 'currency', currency: 'INR' });
29
- };
30
-
31
- const formatDate = (dateLike: any) => {
32
- if (!dateLike) return '—';
33
- try {
34
- const d = new Date(dateLike);
35
- return d.toLocaleDateString();
36
- } catch {
37
- return '—';
38
- }
39
- };
40
-
41
- const getStatusStyle = (status?: string) => {
42
- const s = (status || 'AVAILABLE').toUpperCase();
43
- if (s === 'AVAILABLE') return { bg: '#4caf50', label: 'Available' };
44
- if (s === 'BORROWED') return { bg: '#ff9800', label: 'Borrowed' };
45
- return { bg: '#9e9e9e', label: 'Unavailable' };
46
- };
47
-
48
- const renderStars = (rating?: number) => {
49
- const r = Math.round(Number(rating || 0));
50
- const stars = Array.from({ length: 5 }, (_, i) => (i < r ? '★' : '☆')).join('');
51
- return <span aria-label={`Rating ${r} of 5`} title={`Rating ${r}/5`} style={{ color: '#fbc02d' }}>{stars}</span>;
52
- };
53
-
54
- useEffect(() => {
55
- const performSearch = async () => {
56
- try {
57
- setIsLoading(true);
58
- const term = (searchQuery || '').trim();
59
- const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
60
- const headers: Record<string, string> = {
61
- 'Content-Type': 'application/json',
62
- };
63
- const token = localStorage.getItem('token');
64
- if (token) headers['Authorization'] = `Bearer ${token}`;
65
- if (user?.id) headers['X-User-Id'] = user.id.toString();
66
-
67
- let results: any[] = [];
68
-
69
- // 1) If user typed a term, try the dedicated search endpoint
70
- if (term) {
71
- const qp = new URLSearchParams();
72
- qp.append('q', term); // Backend expects 'q' not 'query'
73
- // Only pass categoryId if it looks like a real id (not display name)
74
- if (selectedCategory && selectedCategory.length > 12) {
75
- qp.append('categoryId', selectedCategory);
76
- }
77
- const url = `${baseUrl}/api/items/search?${qp.toString()}`;
78
- console.log('[Search] Fetching:', url);
79
- const res = await fetch(url, { headers });
80
- if (res.ok) {
81
- const data = await res.json();
82
- results = Array.isArray(data) ? data : (data.content || data.data || []);
83
- console.log('[Search] /api/items/search returned:', results.length, 'items');
84
- } else {
85
- console.warn('[Search] /api/items/search failed with status:', res.status);
86
- }
87
- }
88
-
89
- // 2) Fallback: fetch general items and optionally client-filter
90
- if (!term || results.length === 0) {
91
- const qp = new URLSearchParams();
92
- qp.append('page', '0');
93
- qp.append('size', '50');
94
- if (selectedCategory && selectedCategory.length > 12) {
95
- qp.append('categoryId', selectedCategory);
96
- }
97
- const url = `${baseUrl}/api/items?${qp.toString()}`;
98
- console.log('[Search] Fallback fetching:', url);
99
- const res = await fetch(url, { headers });
100
- if (res.ok) {
101
- const data = await res.json();
102
- const all = data.content || data.data || [];
103
- console.log('[Search] /api/items returned:', all.length, 'items');
104
- results = term
105
- ? all.filter((it: any) => {
106
- const name = (it.name || it.title || '').toString().toLowerCase();
107
- const desc = (it.description || '').toString().toLowerCase();
108
- const categoryName = (it.categoryName || it.category || '').toString().toLowerCase();
109
- const q = term.toLowerCase();
110
- return name.includes(q) || desc.includes(q) || categoryName.includes(q);
111
- })
112
- : all;
113
- console.log('[Search] After client filter:', results.length, 'items');
114
- } else {
115
- console.warn('[Search] /api/items fallback failed with status:', res.status);
116
- }
117
- }
118
-
119
- // Exclude current user's items
120
- const filtered = results.filter((it: any) => {
121
- const ownerId = it.ownerId || it.owner?.id || it.owner_id;
122
- return !user?.id || ownerId !== user.id;
123
- });
124
- console.log('[Search] After excluding own items:', filtered.length, 'items');
125
- setSearchResults(filtered);
126
- } catch (error) {
127
- console.error('Search error:', error);
128
- setSearchResults([]);
129
- } finally {
130
- setIsLoading(false);
131
- }
132
- };
133
-
134
- performSearch();
135
- }, [searchQuery, selectedCategory, user?.id]);
136
-
137
- const handleNewSearch = (e: React.FormEvent) => {
138
- e.preventDefault();
139
- // Search will be triggered by useEffect when searchQuery changes
140
- };
141
-
142
- return (
143
- <div style={{
144
- fontFamily: 'Roboto, Arial, sans-serif',
145
- margin: '0',
146
- padding: '0',
147
- backgroundColor: '#f5f5f5',
148
- color: '#213547',
149
- minHeight: '100vh'
150
- }}>
151
- {/* Navigation Bar */}
152
- <nav style={{
153
- backgroundColor: '#1976d2',
154
- color: 'white',
155
- padding: '12px 0',
156
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
157
- marginBottom: '20px'
158
- }}>
159
- <div style={{
160
- maxWidth: '1200px',
161
- margin: '0 auto',
162
- padding: '0 20px',
163
- display: 'flex',
164
- alignItems: 'center',
165
- justifyContent: 'space-between',
166
- gap: '20px'
167
- }}>
168
- <div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>
169
- <button
170
- onClick={onBack}
171
- style={{
172
- color: 'white',
173
- textDecoration: 'none',
174
- background: 'none',
175
- border: 'none',
176
- cursor: 'pointer',
177
- fontSize: '1.5rem',
178
- fontWeight: 'bold'
179
- }}
180
- >
181
- LocalLend
182
- </button>
183
- </div>
184
-
185
- {/* Search Bar */}
186
- <form onSubmit={handleNewSearch} style={{
187
- display: 'flex',
188
- alignItems: 'center',
189
- gap: '8px',
190
- flex: 1,
191
- maxWidth: '500px'
192
- }}>
193
- <input
194
- type="search"
195
- placeholder="Search items..."
196
- value={searchQuery}
197
- onChange={(e) => onSearchQueryChange(e.target.value)}
198
- style={{
199
- flex: 1,
200
- padding: '8px 12px',
201
- border: 'none',
202
- borderRadius: '4px',
203
- fontSize: '14px'
204
- }}
205
- />
206
- <select
207
- value={selectedCategory}
208
- onChange={(e) => setSelectedCategory(e.target.value)}
209
- style={{
210
- padding: '8px',
211
- border: 'none',
212
- borderRadius: '4px',
213
- fontSize: '14px'
214
- }}
215
- >
216
- <option value="">All Categories</option>
217
- <option value="electronics">Electronics</option>
218
- <option value="tools">Tools</option>
219
- <option value="sports">Sports</option>
220
- <option value="books">Books</option>
221
- </select>
222
- <button type="submit" style={{
223
- padding: '8px 16px',
224
- backgroundColor: '#1565c0',
225
- color: 'white',
226
- border: 'none',
227
- borderRadius: '4px',
228
- cursor: 'pointer',
229
- fontSize: '14px'
230
- }}>
231
- Search
232
- </button>
233
- </form>
234
-
235
- {user && (
236
- <span style={{
237
- color: 'white',
238
- fontSize: '14px',
239
- padding: '8px 12px',
240
- backgroundColor: 'rgba(255, 255, 255, 0.1)',
241
- borderRadius: '4px'
242
- }}>
243
- Welcome, {user.name}!
244
- </span>
245
- )}
246
- </div>
247
- </nav>
248
-
249
- {/* Search Results Content */}
250
- <div style={{ padding: '0 20px', maxWidth: '1200px', margin: '0 auto' }}>
251
- <div style={{ marginBottom: '20px' }}>
252
- <h1 style={{ color: '#1976d2', marginBottom: '10px' }}>
253
- Search Results {searchQuery && `for "${searchQuery}"`}
254
- </h1>
255
- {selectedCategory && (
256
- <p style={{ color: '#666', margin: '0' }}>
257
- Category: {selectedCategory}
258
- </p>
259
- )}
260
- </div>
261
-
262
- {isLoading ? (
263
- <div style={{ textAlign: 'center', padding: '40px' }}>
264
- <p>Searching for items...</p>
265
- </div>
266
- ) : (
267
- <>
268
- <div style={{ marginBottom: '20px', color: '#666' }}>
269
- Found {searchResults.length} item{searchResults.length !== 1 ? 's' : ''}
270
- </div>
271
-
272
- {searchResults.length === 0 ? (
273
- <div style={{
274
- backgroundColor: 'white',
275
- padding: '40px',
276
- borderRadius: '8px',
277
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
278
- textAlign: 'center'
279
- }}>
280
- <h3 style={{ color: '#666', marginBottom: '10px' }}>No items found</h3>
281
- <p style={{ color: '#888' }}>
282
- Try adjusting your search terms or browse all available items.
283
- </p>
284
- <button
285
- onClick={() => onSearchQueryChange('')}
286
- style={{
287
- padding: '10px 20px',
288
- backgroundColor: '#1976d2',
289
- color: 'white',
290
- border: 'none',
291
- borderRadius: '4px',
292
- cursor: 'pointer',
293
- marginTop: '15px'
294
- }}
295
- >
296
- Clear Search
297
- </button>
298
- </div>
299
- ) : (
300
- <div style={{
301
- display: 'grid',
302
- gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
303
- gap: '20px'
304
- }}>
305
- {searchResults.map((item) => {
306
- const isOwn = !!user && (item.ownerId === user.id || item.owner?.id === user.id);
307
- const name = item.name || item.title || 'Untitled';
308
- const description = item.description || '';
309
- const ownerName = item.ownerName || item.owner?.name || 'Unknown';
310
- const categoryName = item.categoryName || item.category || 'General';
311
- const imageSrc = item.imageUrl || item.images?.[0] || '';
312
- const deposit = item.deposit ?? 0;
313
- const avgRating = item.averageRating ?? item.rating ?? 0;
314
- const statusInfo = getStatusStyle(item.status);
315
- const createdAt = formatDate(item.createdAt);
316
- const canBorrow = item.canBeBorrowed !== false;
317
-
318
- return (
319
- <div key={item.id} style={{
320
- backgroundColor: 'white',
321
- padding: '20px',
322
- borderRadius: '8px',
323
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
324
- display: 'flex',
325
- flexDirection: 'column',
326
- height: 'fit-content'
327
- }}>
328
- {/* Item Image */}
329
- <div style={{
330
- width: '100%',
331
- height: '200px',
332
- backgroundColor: '#e0e0e0',
333
- borderRadius: '4px',
334
- marginBottom: '15px',
335
- display: 'flex',
336
- alignItems: 'center',
337
- justifyContent: 'center',
338
- color: '#666'
339
- }}>
340
- {imageSrc ? (
341
- <img src={imageSrc} alt={name} style={{ width: '100%', height: '100%', objectFit: 'cover', borderRadius: '4px' }} />
342
- ) : (
343
- <span>No Image</span>
344
- )}
345
- </div>
346
-
347
- {/* Title and status */}
348
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
349
- <h3 style={{ margin: '0 0 8px 0', color: '#333' }}>{name}</h3>
350
- <span style={{ padding: '2px 8px', borderRadius: 12, backgroundColor: statusInfo.bg, color: 'white', fontSize: 12 }}>
351
- {statusInfo.label}
352
- </span>
353
- </div>
354
- <p style={{ margin: '0 0 10px 0', color: '#666', fontSize: '14px', flexGrow: 1 }}>{description}</p>
355
-
356
- {/* Owner */}
357
- <div style={{ marginBottom: '10px', fontSize: '14px' }}>
358
- <span style={{ color: '#666' }}>Owner: </span>
359
- <span style={{ color: '#333', fontWeight: 'bold' }}>{ownerName}</span>
360
- </div>
361
-
362
- {/* Chips: deposit, condition, category, borrowable */}
363
- <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '15px', fontSize: '12px' }}>
364
- <span style={{ padding: '2px 8px', borderRadius: '12px', backgroundColor: '#4caf50', color: 'white' }}>
365
- Deposit {formatCurrency(deposit)}
366
- </span>
367
- {item.condition && (
368
- <span style={{ padding: '2px 8px', borderRadius: '12px', backgroundColor: '#2196f3', color: 'white' }}>
369
- {item.condition}
370
- </span>
371
- )}
372
- <span style={{ padding: '2px 8px', borderRadius: '12px', backgroundColor: '#ff9800', color: 'white' }}>
373
- {categoryName}
374
- </span>
375
- <span style={{ padding: '2px 8px', borderRadius: '12px', backgroundColor: canBorrow ? '#7cb342' : '#9e9e9e', color: 'white' }}>
376
- {canBorrow ? 'Borrowable' : 'Not Borrowable'}
377
- </span>
378
- </div>
379
-
380
- {/* Rating & Created */}
381
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
382
- <div>
383
- {renderStars(avgRating)}
384
- <span style={{ marginLeft: 8, color: '#666', fontSize: 12 }}>{Number(avgRating || 0).toFixed(1)}</span>
385
- </div>
386
- <div style={{ color: '#666', fontSize: 12 }}>
387
- Added: {createdAt}
388
- </div>
389
- </div>
390
-
391
- {/* Book button */}
392
- <button
393
- onClick={(e) => {
394
- e.preventDefault();
395
- e.stopPropagation();
396
- console.log('🔥 Book click for item:', item);
397
- try {
398
- if (isOwn) {
399
- alert('You cannot book your own item.');
400
- return;
401
- }
402
- onBookItem(item);
403
- } catch (error) {
404
- console.error('🔥🔥🔥 Error calling onBookItem:', error);
405
- }
406
- }}
407
- style={{
408
- width: '100%',
409
- padding: '10px',
410
- backgroundColor: isOwn ? '#bdbdbd' : '#1976d2',
411
- color: 'white',
412
- border: 'none',
413
- borderRadius: '4px',
414
- cursor: isOwn ? 'not-allowed' : 'pointer',
415
- fontSize: '14px',
416
- fontWeight: 'bold'
417
- }}
418
- disabled={isOwn}
419
- >
420
- {isOwn ? 'Cannot Book Own Item' : 'Book This Item'}
421
- </button>
422
- </div>
423
- );})}
424
- </div>
425
- )}
426
- </>
427
- )}
428
-
429
- <div style={{ marginTop: '30px', textAlign: 'center' }}>
430
- <button onClick={onBack} style={{
431
- padding: '12px 24px',
432
- backgroundColor: '#757575',
433
- color: 'white',
434
- border: 'none',
435
- borderRadius: '4px',
436
- cursor: 'pointer',
437
- fontSize: '16px'
438
- }}>
439
- Back to Home
440
- </button>
441
- </div>
442
- </div>
443
- </div>
444
- );
445
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/services/api.ts DELETED
@@ -1,121 +0,0 @@
1
- // API Configuration
2
- const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
3
-
4
- // Mock API interface for development (will be replaced with axios)
5
- export interface ApiClient {
6
- get: (url: string, config?: any) => Promise<any>;
7
- post: (url: string, data?: any, config?: any) => Promise<any>;
8
- put: (url: string, data?: any, config?: any) => Promise<any>;
9
- patch: (url: string, data?: any, config?: any) => Promise<any>;
10
- delete: (url: string, config?: any) => Promise<any>;
11
- }
12
-
13
- // Fetch-based API client (temporary until axios is installed)
14
- const createFetchClient = (): ApiClient => {
15
- const makeRequest = async (url: string, options: RequestInit = {}) => {
16
- const token = localStorage.getItem('token');
17
- const headers: Record<string, string> = {
18
- 'Content-Type': 'application/json',
19
- ...(options.headers as Record<string, string>),
20
- };
21
-
22
- if (token) {
23
- headers.Authorization = `Bearer ${token}`;
24
- }
25
-
26
- const fullUrl = `${API_BASE_URL}${url}`;
27
- console.log(`🌐 API Request: ${options.method || 'GET'} ${fullUrl}`);
28
- console.log('📤 Request headers:', headers);
29
- if (options.body) {
30
- console.log('📦 Request body:', options.body);
31
- }
32
-
33
- let response: Response;
34
- try {
35
- response = await fetch(fullUrl, {
36
- ...options,
37
- headers,
38
- mode: 'cors', // Explicitly set CORS mode
39
- });
40
-
41
- console.log(`📨 Response: ${response.status} ${response.statusText}`);
42
-
43
- // Log response headers for debugging
44
- console.log('📥 Response headers:', Object.fromEntries(response.headers.entries()));
45
- } catch (error: any) {
46
- console.error('❌ Network Error:', error);
47
- // Handle network errors (CORS, connection refused, etc.)
48
- if (error.name === 'TypeError' || error.message === 'Failed to fetch') {
49
- throw new Error('Failed to fetch');
50
- }
51
- throw error;
52
- }
53
-
54
- if (response.status === 401) {
55
- localStorage.removeItem('token');
56
- localStorage.removeItem('user');
57
- window.location.href = '/login';
58
- throw new Error('Unauthorized');
59
- }
60
-
61
- if (!response.ok) {
62
- let errorData: any = {};
63
- try {
64
- errorData = await response.json();
65
- } catch (e) {
66
- // If response is not JSON, create error based on status
67
- errorData = {
68
- message: `HTTP ${response.status}: ${response.statusText}`
69
- };
70
- }
71
-
72
- const error = new Error(errorData.message || errorData.error || `Request failed with status ${response.status}`);
73
- (error as any).response = {
74
- status: response.status,
75
- data: errorData
76
- };
77
- throw error;
78
- }
79
-
80
- // Handle 204 No Content responses (like DELETE operations)
81
- if (response.status === 204) {
82
- return null;
83
- }
84
-
85
- // Check if response has content before parsing JSON
86
- const contentType = response.headers.get('content-type');
87
- if (contentType && contentType.includes('application/json')) {
88
- return response.json();
89
- } else {
90
- return null;
91
- }
92
- };
93
-
94
- return {
95
- get: (url: string, config?: any) => makeRequest(url, { method: 'GET', ...config }),
96
- post: (url: string, data?: any, config?: any) =>
97
- makeRequest(url, { method: 'POST', body: JSON.stringify(data), ...config }),
98
- put: (url: string, data?: any, config?: any) =>
99
- makeRequest(url, { method: 'PUT', body: JSON.stringify(data), ...config }),
100
- patch: (url: string, data?: any, config?: any) =>
101
- makeRequest(url, { method: 'PATCH', body: JSON.stringify(data), ...config }),
102
- delete: (url: string, config?: any) => makeRequest(url, { method: 'DELETE', ...config }),
103
- };
104
- };
105
-
106
- const api = createFetchClient();
107
-
108
- // Helper function to handle API errors
109
- export const handleApiError = (error: any): string => {
110
- if (error.message) {
111
- return error.message;
112
- }
113
- return 'An unexpected error occurred';
114
- };
115
-
116
- // Helper function to add X-User-Id header when needed
117
- export const addUserIdHeader = (userId: string) => {
118
- return { 'X-User-Id': userId };
119
- };
120
-
121
- export default api;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/services/authService.ts DELETED
@@ -1,48 +0,0 @@
1
- import api from './api';
2
- import type {
3
- LoginRequest,
4
- RegisterRequest,
5
- AuthResponse,
6
- User
7
- } from '../types';
8
-
9
- export const authService = {
10
- // Register a new user
11
- register: async (userData: RegisterRequest): Promise<User> => {
12
- const response = await api.post('/api/auth/register', userData);
13
- return response;
14
- },
15
-
16
- // Login user
17
- login: async (credentials: LoginRequest): Promise<AuthResponse> => {
18
- const response = await api.post('/api/auth/login', credentials);
19
- // Store token and user data
20
- if (response.token) {
21
- localStorage.setItem('token', response.token);
22
- localStorage.setItem('user', JSON.stringify(response.user));
23
- }
24
- return response;
25
- },
26
-
27
- // Logout user
28
- logout: () => {
29
- localStorage.removeItem('token');
30
- localStorage.removeItem('user');
31
- },
32
-
33
- // Get current user from localStorage
34
- getCurrentUser: (): User | null => {
35
- const userStr = localStorage.getItem('user');
36
- return userStr ? JSON.parse(userStr) : null;
37
- },
38
-
39
- // Check if user is authenticated
40
- isAuthenticated: (): boolean => {
41
- return !!localStorage.getItem('token');
42
- },
43
-
44
- // Get auth token
45
- getToken: (): string | null => {
46
- return localStorage.getItem('token');
47
- }
48
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/services/bookingService.ts DELETED
@@ -1,82 +0,0 @@
1
- import api, { addUserIdHeader } from './api';
2
- import type {
3
- Booking,
4
- CreateBookingRequest,
5
- ApiResponse,
6
- BookingStatus
7
- } from '../types';
8
-
9
- export const bookingService = {
10
- // Create new booking request
11
- createBooking: async (bookingData: CreateBookingRequest, borrowerId: string): Promise<Booking> => {
12
- const response = await api.post('/api/bookings', bookingData, {
13
- headers: addUserIdHeader(borrowerId)
14
- });
15
- return response;
16
- },
17
-
18
- // Get all bookings (as borrower)
19
- getMyBookings: async (status?: BookingStatus): Promise<ApiResponse<Booking[]>> => {
20
- const url = status ? `/api/bookings?status=${status}` : '/api/bookings';
21
- const response = await api.get(url);
22
- return response;
23
- },
24
-
25
- // Get bookings for items I own (as owner)
26
- getBookingsReceived: async (): Promise<ApiResponse<Booking[]>> => {
27
- const response = await api.get('/api/bookings/received');
28
- return response;
29
- },
30
-
31
- // Get pending approvals (for owners)
32
- getPendingApprovals: async (): Promise<ApiResponse<Booking[]>> => {
33
- const response = await api.get('/api/bookings/pending-approvals');
34
- return response;
35
- },
36
-
37
- // Get single booking details
38
- getBooking: async (bookingId: string): Promise<Booking> => {
39
- const response = await api.get(`/api/bookings/${bookingId}`);
40
- return response;
41
- },
42
-
43
- // Approve booking (owner)
44
- approveBooking: async (bookingId: string, ownerId: string): Promise<Booking> => {
45
- const response = await api.patch(`/api/bookings/${bookingId}/approve`, null, {
46
- headers: addUserIdHeader(ownerId)
47
- });
48
- return response;
49
- },
50
-
51
- // Reject booking (owner)
52
- rejectBooking: async (bookingId: string, reason: string, ownerId: string): Promise<Booking> => {
53
- const response = await api.patch(`/api/bookings/${bookingId}/reject`, { reason }, {
54
- headers: addUserIdHeader(ownerId)
55
- });
56
- return response;
57
- },
58
-
59
- // Start booking (borrower)
60
- startBooking: async (bookingId: string, borrowerId: string): Promise<Booking> => {
61
- const response = await api.patch(`/api/bookings/${bookingId}/start`, null, {
62
- headers: addUserIdHeader(borrowerId)
63
- });
64
- return response;
65
- },
66
-
67
- // Complete booking (borrower or owner)
68
- completeBooking: async (bookingId: string, userId: string): Promise<Booking> => {
69
- const response = await api.patch(`/api/bookings/${bookingId}/complete`, null, {
70
- headers: addUserIdHeader(userId)
71
- });
72
- return response;
73
- },
74
-
75
- // Cancel booking
76
- cancelBooking: async (bookingId: string, reason: string, userId: string): Promise<Booking> => {
77
- const response = await api.patch(`/api/bookings/${bookingId}/cancel`, { reason }, {
78
- headers: addUserIdHeader(userId)
79
- });
80
- return response;
81
- }
82
- };