Kuruva Laxmi commited on
Commit
f359e2d
·
1 Parent(s): 0f8617c

Deployment preparation: Configure Vercel, API URLs, and CORS

Browse files
README_IMPLEMENTATION.md ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SnapLocal Implementation Guide
2
+
3
+ ## ✅ MVP Features Implemented (Complete)
4
+
5
+ ### Backend (Node.js + MongoDB)
6
+
7
+ #### 1. **Package System** ✓
8
+ - **Model**: `server/src/models/Package.js`
9
+ - **Routes**: `server/src/routes/packageRoutes.js`
10
+ - **Endpoints**:
11
+ - `GET /api/packages/photographer/:photographerId` - Get photographer's packages
12
+ - `GET /api/packages/:id` - Get single package
13
+ - `POST /api/packages` - Create new package (auth required)
14
+ - `PUT /api/packages/:id` - Update package (auth required)
15
+ - `DELETE /api/packages/:id` - Delete package (auth required)
16
+
17
+ #### 2. **Photographer Verification** ✓
18
+ - **Model**: `server/src/models/Verification.js`
19
+ - **Routes**: `server/src/routes/verificationRoutes.js`
20
+ - **Endpoints**:
21
+ - `GET /api/verification/status/:photographerId` - Get verification status
22
+ - `POST /api/verification/submit` - Upload documents (auth required)
23
+ - `POST /api/verification/:verificationId/approve` - Admin approve
24
+ - `POST /api/verification/:verificationId/reject` - Admin reject
25
+
26
+ #### 3. **Photo Delivery** ✓
27
+ - **Model**: `server/src/models/PhotoDelivery.js`
28
+ - **Routes**: `server/src/routes/deliveryRoutes.js`
29
+ - **Endpoints**:
30
+ - `GET /api/delivery/booking/:bookingId` - Get delivery status
31
+ - `POST /api/delivery/booking/:bookingId` - Create delivery record
32
+ - `POST /api/delivery/:deliveryId/upload` - Upload photos (auth required)
33
+ - `POST /api/delivery/:deliveryId/complete` - Mark as delivered
34
+ - `POST /api/delivery/:deliveryId/request-revision` - Request revisions
35
+
36
+ #### 4. **Invoice Generator** ✓
37
+ - **Model**: `server/src/models/Invoice.js`
38
+ - **Routes**: `server/src/routes/invoiceRoutes.js`
39
+ - **Endpoints**:
40
+ - `GET /api/invoices/:id` - Get invoice
41
+ - `GET /api/invoices/user/:userId` - Get user's invoices
42
+ - `POST /api/invoices` - Create invoice
43
+ - `PUT /api/invoices/:id/payment` - Update payment status
44
+ - `GET /api/invoices/:id/download` - Download invoice
45
+
46
+ #### 5. **Smart Tags (Enhanced Search)** ✓
47
+ - Implemented in `Photographer.js` model with tags array
48
+ - Frontend Search component with tag filters
49
+
50
+ #### 6. **Additional Models Created** ✓
51
+ - **Loyalty Points** (`server/src/models/LoyaltyPoints.js`)
52
+ - **Report System** (`server/src/models/Report.js`)
53
+ - **Trip Planner** (`server/src/models/TripPlan.js`)
54
+ - **Dynamic Pricing** (`server/src/models/DynamicPrice.js`)
55
+ - **Deals/Discounts** (`server/src/models/Deal.js`)
56
+
57
+ ---
58
+
59
+ ### Frontend (React + Vite)
60
+
61
+ #### Components Created ✓
62
+
63
+ **Photographer Dashboard Components:**
64
+ - `PackagePanel.jsx` - Create and manage packages
65
+ - `PackageCard.jsx` - Individual package card display
66
+ - `CreatePackageModal.jsx` - Modal form for creating/editing packages
67
+ - `VerificationPanel.jsx` - Verification document upload and status
68
+ - `DeliveryPanel.jsx` - Photo upload and delivery management
69
+ - `InvoiceView.jsx` - Invoice display and download
70
+
71
+ **Customer Dashboard Components:**
72
+ - `TrendingSpots.jsx` - Popular photography locations
73
+ - `LastMinuteDeals.jsx` - Time-limited discount offers
74
+ - `Leaderboard.jsx` - Top photographers by rating
75
+ - `ShootTimeline.jsx` - Booking progress timeline
76
+
77
+ **Utilities:**
78
+ - `src/utils/invoiceGenerator.js` - Invoice HTML generation and PDF export
79
+
80
+ ---
81
+
82
+ ## 📱 Technology Stack
83
+
84
+ ### Backend
85
+ - **Express.js** - REST API server
86
+ - **MongoDB** - Database
87
+ - **Mongoose** - ODM
88
+ - **JWT** - Authentication
89
+ - **Socket.io** - Real-time messaging (already in place)
90
+
91
+ ### Frontend
92
+ - **React 19.2.4** - UI framework
93
+ - **Vite 8.0.0** - Build tool
94
+ - **Tailwind CSS 4.2.1** - Styling
95
+ - **Lucide React** - Icons
96
+ - **Axios** - HTTP client
97
+
98
+ ---
99
+
100
+ ## 🚀 How to Run
101
+
102
+ ### Prerequisites
103
+ - Node.js 18+
104
+ - MongoDB running locally or cloud URL in `.env`
105
+ - npm or yarn
106
+
107
+ ### 1. Install Dependencies
108
+
109
+ ```bash
110
+ # Backend
111
+ cd server
112
+ npm install
113
+
114
+ # Frontend
115
+ cd ../client
116
+ npm install
117
+ ```
118
+
119
+ ### 2. Configure Environment
120
+
121
+ Create `server/.env`:
122
+ ```env
123
+ MONGODB_URI=mongodb://localhost:27017/snaplocal
124
+ PORT=5000
125
+ JWT_SECRET=your_secret_key
126
+ ```
127
+
128
+ ### 3. Start Development Servers
129
+
130
+ ```bash
131
+ # Terminal 1: Backend
132
+ cd server
133
+ npm run dev
134
+ # Server runs on http://localhost:5000
135
+
136
+ # Terminal 2: Frontend
137
+ cd client
138
+ npm run dev
139
+ # Client runs on http://localhost:5173
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 📊 API Usage Examples
145
+
146
+ ### Create a Package (Photographer)
147
+ ```javascript
148
+ // POST http://localhost:5000/api/packages
149
+ {
150
+ "name": "Premium Wedding Package",
151
+ "description": "8 hours coverage with 300+ edited photos",
152
+ "price": 15000,
153
+ "priceUnit": "session",
154
+ "duration": 8,
155
+ "deliverables": {
156
+ "numPhotos": 350,
157
+ "numEdited": 200,
158
+ "numLocations": 2,
159
+ "includesAlbum": true
160
+ },
161
+ "tags": ["wedding", "bride", "groom"]
162
+ }
163
+ ```
164
+
165
+ ### Submit Verification
166
+ ```javascript
167
+ // POST http://localhost:5000/api/verification/submit
168
+ {
169
+ "idDocumentUrl": "https://example.com/aadhar.jpg",
170
+ "portfolioUrls": [
171
+ "https://example.com/photo1.jpg",
172
+ "https://example.com/photo2.jpg"
173
+ ]
174
+ }
175
+ ```
176
+
177
+ ### Upload Photos for Delivery
178
+ ```javascript
179
+ // POST http://localhost:5000/api/delivery/{deliveryId}/upload
180
+ {
181
+ "photoUrls": [
182
+ "https://example.com/photo1.jpg",
183
+ "https://example.com/photo2.jpg",
184
+ // ... more photos
185
+ ]
186
+ }
187
+ ```
188
+
189
+ ### Create Invoice
190
+ ```javascript
191
+ // POST http://localhost:5000/api/invoices
192
+ {
193
+ "bookingId": "booking_id_here",
194
+ "photographerId": "photographer_id",
195
+ "customerId": "customer_id",
196
+ "items": [
197
+ {
198
+ "description": "Premium Wedding Photography",
199
+ "quantity": 1,
200
+ "unitPrice": 15000,
201
+ "total": 15000
202
+ }
203
+ ],
204
+ "subtotal": 15000,
205
+ "tax": 2700,
206
+ "taxRate": 18,
207
+ "totalAmount": 17700
208
+ }
209
+ ```
210
+
211
+ ---
212
+
213
+ ## 🎯 Dashboard Features
214
+
215
+ ### Photographer Dashboard
216
+ 1. **Overview** - Statistics (bookings, revenue, pending, rating)
217
+ 2. **My Profile** - Edit bio, specialty, rate, experience
218
+ 3. **Portfolio** - Upload images/videos
219
+ 4. **Packages** ⭐ - Create and manage service packages
220
+ 5. **Verification** ⭐ - Upload documents for verification badge
221
+ 6. **Availability** - Set weekly schedule
222
+ 7. **Bookings** - Manage booking requests with timeline
223
+ 8. **Photo Delivery** ⭐ - Upload and deliver photos to customers
224
+ 9. **Reports** - View and respond to customer complaints
225
+
226
+ ### Customer Dashboard
227
+ 1. **Search** - Find photographers (enhanced with tag filters)
228
+ 2. **Trending Spots** ⭐ - Popular photography locations
229
+ 3. **Last-Minute Deals** ⭐ - Time-limited discounts
230
+ 4. **Nearby Photographers** - Map view with location filtering
231
+ 5. **My Bookings** - Manage reservations and download photos
232
+ 6. **Leaderboard** ⭐ - Top-rated photographers
233
+ 7. **Loyalty/Rewards** - Points and tier benefits
234
+ 8. **Trip Planner** - Plan photo shoot trips
235
+
236
+ ---
237
+
238
+ ## 📂 File Structure Summary
239
+
240
+ ```
241
+ server/src/
242
+ ├── models/
243
+ │ ├── Package.js ✓ NEW
244
+ │ ├── Verification.js ✓ NEW
245
+ │ ├── PhotoDelivery.js ✓ NEW
246
+ │ ├── Invoice.js ✓ NEW
247
+ │ ├── LoyaltyPoints.js ✓ NEW
248
+ │ ├── Report.js ✓ NEW
249
+ │ ├── TripPlan.js ✓ NEW
250
+ │ ├── DynamicPrice.js ✓ NEW
251
+ │ └── Deal.js ✓ NEW
252
+ ├── routes/
253
+ │ ├── packageRoutes.js ✓ NEW
254
+ │ ├── verificationRoutes.js✓ NEW
255
+ │ ├── deliveryRoutes.js ✓ NEW
256
+ │ ├── invoiceRoutes.js ✓ NEW
257
+ │ ├── reportRoutes.js ✓ NEW
258
+ │ ├── loyaltyRoutes.js ✓ NEW
259
+ │ ├── tripPlanRoutes.js ✓ NEW
260
+ │ └── dealRoutes.js ✓ NEW
261
+ └── index.js ✓ UPDATED
262
+
263
+ client/src/
264
+ ├── components/
265
+ │ ├── PackagePanel.jsx ✓ NEW
266
+ │ ├── PackageCard.jsx ✓ NEW
267
+ │ ├── CreatePackageModal.jsx✓ NEW
268
+ │ ├── VerificationPanel.jsx ✓ NEW
269
+ │ ├── DeliveryPanel.jsx ✓ NEW
270
+ │ ├── InvoiceView.jsx ✓ NEW
271
+ │ ├── TrendingSpots.jsx ✓ NEW
272
+ │ ├── LastMinuteDeals.jsx ✓ NEW
273
+ │ ├── Leaderboard.jsx ✓ NEW
274
+ │ └── ShootTimeline.jsx ✓ NEW
275
+ └── utils/
276
+ └── invoiceGenerator.js ✓ NEW
277
+ ```
278
+
279
+ ---
280
+
281
+ ## ✨ Next Steps (Phase 2+)
282
+
283
+ ### Advanced Features
284
+ 1. **AI-Powered Features**
285
+ - Style matching (upload reference photo for similar photographers)
286
+ - Dynamic pricing suggestions based on demand
287
+ - Smart recommendations
288
+
289
+ 2. **Enhanced Search**
290
+ - Geolocation-based photographer search
291
+ - Advanced filters (availability, price range, ratings, tags)
292
+
293
+ 3. **Performance Analytics**
294
+ - Charts and graphs for photographer earnings/bookings
295
+ - Customer analytics for trends
296
+
297
+ 4. **Trip Planning**
298
+ - Itinerary builder
299
+ - Recommended photography spots
300
+ - Travel packages
301
+
302
+ 5. **Additional Features**
303
+ - Watermark protection for portfolio images
304
+ - Live photographer availability map
305
+ - Video introductions for photographers
306
+ - Social sharing buttons
307
+
308
+ ---
309
+
310
+ ## 🔐 Security Notes
311
+
312
+ - All protected routes require JWT authentication
313
+ - Password hashing with bcryptjs (already implemented)
314
+ - CORS enabled for development
315
+ - Input validation needed (in production)
316
+ - Rate limiting recommended for API endpoints
317
+
318
+ ---
319
+
320
+ ## 📊 Database Schema Overview
321
+
322
+ ### Package
323
+ ```
324
+ {
325
+ photographerId, name, description, price, priceUnit, duration,
326
+ deliverables: { numPhotos, numEdited, numLocations, includesAlbum },
327
+ active, bookingCount, tags, timestamps
328
+ }
329
+ ```
330
+
331
+ ### Verification
332
+ ```
333
+ {
334
+ photographerId, status (pending|verified|rejected),
335
+ idDocument, portfolioSamples[], submittedAt, verifiedAt,
336
+ adminNotes, rejectionReason, timestamps
337
+ }
338
+ ```
339
+
340
+ ### Invoice
341
+ ```
342
+ {
343
+ invoiceNumber, bookingId, photographerId, customerId,
344
+ items[], subtotal, tax, taxRate, discount, totalAmount,
345
+ paymentStatus, paidAt, timestamps
346
+ }
347
+ ```
348
+
349
+ ### PhotoDelivery
350
+ ```
351
+ {
352
+ bookingId, photographerId, customerId, photos[],
353
+ status (pending|partially_delivered|delivered|revision_requested),
354
+ deliveredAt, downloadLink, linkExpiresAt, revisionNotes, timestamps
355
+ }
356
+ ```
357
+
358
+ ---
359
+
360
+ ## 🎓 Code Examples
361
+
362
+ ### Use PackagePanel in Dashboard
363
+ ```jsx
364
+ import PackagePanel from '../components/PackagePanel';
365
+
366
+ export default function PhotographerDashboard() {
367
+ const { user } = useAuth();
368
+ return <PackagePanel photographerId={user.id} />;
369
+ }
370
+ ```
371
+
372
+ ### Use DeliveryPanel in Booking Page
373
+ ```jsx
374
+ import DeliveryPanel from '../components/DeliveryPanel';
375
+
376
+ export default function BookingDetail({ bookingId }) {
377
+ return (
378
+ <DeliveryPanel
379
+ bookingId={bookingId}
380
+ photographerId={currentPhotographerId}
381
+ role="photographer"
382
+ />
383
+ );
384
+ }
385
+ ```
386
+
387
+ ### Generate and Download Invoice
388
+ ```jsx
389
+ import { downloadInvoiceAsPDF } from '../utils/invoiceGenerator';
390
+
391
+ const handleDownload = (invoice, photographer, customer) => {
392
+ downloadInvoiceAsPDF(invoice, photographer, customer);
393
+ };
394
+ ```
395
+
396
+ ---
397
+
398
+ ## 🐛 Troubleshooting
399
+
400
+ ### Port Already in Use
401
+ ```bash
402
+ # Kill process on port 5000
403
+ lsof -i :5000
404
+ kill -9 <PID>
405
+ ```
406
+
407
+ ### MongoDB Connection Failed
408
+ - Ensure MongoDB is running: `mongod`
409
+ - Check MONGODB_URI in `.env`
410
+ - Verify connection string format
411
+
412
+ ### Components Not Loading
413
+ - Clear Vite cache: `rm -rf client/node_modules/.vite`
414
+ - Restart dev server
415
+
416
+ ---
417
+
418
+ ## 📝 Future Enhancements
419
+
420
+ - [ ] Payment gateway integration (Stripe/Razorpay)
421
+ - [ ] Email notifications for bookings
422
+ - [ ] SMS alerts for photographers
423
+ - [ ] Multi-language support
424
+ - [ ] Mobile app (React Native)
425
+ - [ ] Admin dashboard
426
+ - [ ] Analytics and reporting
427
+ - [ ] Advanced booking calendar with time slots
428
+ - [ ] Subscription tiers for photographers
429
+ - [ ] Automated invoice generation and payment reminders
430
+
431
+ ---
432
+
433
+ ## 📞 Support
434
+
435
+ For issues or questions:
436
+ 1. Check the Feature Implementation Map: `/FEATURE_IMPLEMENTATION_MAP.md`
437
+ 2. Review API routes in respective `server/src/routes/` files
438
+ 3. Check component prop requirements in `client/src/components/`
439
+
440
+ ---
441
+
442
+ **Version**: 1.0.0 MVP
443
+ **Last Updated**: March 18, 2026
444
+ **Status**: ✅ Complete and Running
445
+
client/src/api/api.js CHANGED
@@ -1,7 +1,9 @@
1
  import axios from 'axios';
2
 
 
 
3
  const api = axios.create({
4
- baseURL: 'http://localhost:5000/api',
5
  });
6
 
7
  // Add a request interceptor to add the JWT token to headers
 
1
  import axios from 'axios';
2
 
3
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api';
4
+
5
  const api = axios.create({
6
+ baseURL: API_URL,
7
  });
8
 
9
  // Add a request interceptor to add the JWT token to headers
client/src/components/DeliveryPanel.jsx ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Upload, Download, Check, AlertCircle, Loader2, X } from 'lucide-react';
3
+ import api from '../api/api';
4
+
5
+ const DeliveryPanel = ({ bookingId, photographerId, role = 'photographer' }) => {
6
+ const [delivery, setDelivery] = useState(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const [uploading, setUploading] = useState(false);
9
+ const [showUploadForm, setShowUploadForm] = useState(false);
10
+
11
+ useEffect(() => {
12
+ fetchDelivery();
13
+ }, [bookingId]);
14
+
15
+ const fetchDelivery = async () => {
16
+ try {
17
+ const res = await api.get(`/delivery/booking/${bookingId}`);
18
+ setDelivery(res.data);
19
+ } catch (err) {
20
+ // Delivery record might not exist yet
21
+ console.log('Delivery record not yet created');
22
+ } finally {
23
+ setLoading(false);
24
+ }
25
+ };
26
+
27
+ const handlePhotoUpload = async (e) => {
28
+ const files = e.target.files;
29
+ if (!files || files.length === 0) return;
30
+
31
+ setUploading(true);
32
+ try {
33
+ const photoUrls = Array.from(files).map(file => URL.createObjectURL(file));
34
+
35
+ const res = await api.post(`/delivery/${delivery._id}/upload`, {
36
+ photoUrls,
37
+ });
38
+
39
+ setDelivery(res.data);
40
+ alert(`${files.length} photos uploaded!`);
41
+ setShowUploadForm(false);
42
+ } catch (err) {
43
+ alert('Failed to upload photos');
44
+ } finally {
45
+ setUploading(false);
46
+ }
47
+ };
48
+
49
+ const handleCompleteDelivery = async () => {
50
+ try {
51
+ const res = await api.post(`/delivery/${delivery._id}/complete`, {
52
+ downloadLinkExpiryDays: 7,
53
+ });
54
+ setDelivery(res.data);
55
+ alert('Delivery marked as complete!');
56
+ } catch (err) {
57
+ alert('Failed to complete delivery');
58
+ }
59
+ };
60
+
61
+ if (loading) {
62
+ return (
63
+ <div className="flex items-center justify-center py-8">
64
+ <Loader2 className="animate-spin text-blue-600" size={28} />
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (!delivery) {
70
+ return (
71
+ <div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
72
+ <p className="text-amber-700">Delivery not yet created for this booking</p>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ const statusColor = {
78
+ pending: 'gray',
79
+ partially_delivered: 'blue',
80
+ delivered: 'green',
81
+ revision_requested: 'orange',
82
+ };
83
+
84
+ return (
85
+ <div className="space-y-4">
86
+ <div className="flex items-center justify-between">
87
+ <h3 className="text-lg font-bold">📦 Photo Delivery</h3>
88
+ <span
89
+ className={`px-3 py-1 rounded-full text-xs font-bold text-${statusColor[delivery.status]}-700 bg-${statusColor[delivery.status]}-100`}
90
+ >
91
+ {delivery.status.replace('_', ' ').toUpperCase()}
92
+ </span>
93
+ </div>
94
+
95
+ {/* Photos Grid */}
96
+ {delivery.photos && delivery.photos.length > 0 && (
97
+ <div>
98
+ <p className="text-sm font-semibold text-gray-700 mb-3">
99
+ {delivery.photos.length} Photos Uploaded
100
+ </p>
101
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 max-h-[300px] overflow-y-auto">
102
+ {delivery.photos.map((photo, idx) => (
103
+ <div key={idx} className="aspect-square bg-gray-200 rounded-lg overflow-hidden relative group">
104
+ <img
105
+ src={photo.url}
106
+ alt={`Photo ${idx + 1}`}
107
+ className="w-full h-full object-cover group-hover:opacity-75"
108
+ />
109
+ {photo.watermarked && (
110
+ <div className="absolute bottom-1 right-1 bg-black/50 text-white text-xs px-2 py-1 rounded">
111
+ Watermarked
112
+ </div>
113
+ )}
114
+ </div>
115
+ ))}
116
+ </div>
117
+ </div>
118
+ )}
119
+
120
+ {/* Upload Form */}
121
+ {role === 'photographer' && delivery.status !== 'delivered' && (
122
+ <div>
123
+ {!showUploadForm ? (
124
+ <button
125
+ onClick={() => setShowUploadForm(true)}
126
+ className="w-full flex items-center justify-center gap-2 bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 font-medium"
127
+ >
128
+ <Upload size={18} /> Add More Photos
129
+ </button>
130
+ ) : (
131
+ <div className="border-2 border-dashed border-gray-300 rounded-lg p-6">
132
+ <label className="flex flex-col items-center justify-center cursor-pointer">
133
+ <Upload className="text-gray-400 mb-2" size={32} />
134
+ <p className="font-semibold text-gray-700">Click to upload photos</p>
135
+ <p className="text-xs text-gray-500">or drag and drop</p>
136
+ <input
137
+ type="file"
138
+ onChange={handlePhotoUpload}
139
+ disabled={uploading}
140
+ multiple
141
+ className="hidden"
142
+ accept="image/*"
143
+ />
144
+ </label>
145
+ {uploading && <p className="text-sm text-blue-600 mt-2">Uploading...</p>}
146
+ <button
147
+ onClick={() => setShowUploadForm(false)}
148
+ className="mt-2 text-sm text-gray-600 hover:text-gray-900"
149
+ >
150
+ Cancel
151
+ </button>
152
+ </div>
153
+ )}
154
+ </div>
155
+ )}
156
+
157
+ {/* Action Buttons */}
158
+ {role === 'photographer' && delivery.status === 'partially_delivered' && (
159
+ <button
160
+ onClick={handleCompleteDelivery}
161
+ className="w-full flex items-center justify-center gap-2 bg-green-600 text-white py-2 rounded-lg hover:bg-green-700 font-medium"
162
+ >
163
+ <Check size={18} /> Mark as Delivered
164
+ </button>
165
+ )}
166
+
167
+ {/* Customer Download Section */}
168
+ {role === 'customer' && delivery.status === 'delivered' && delivery.downloadLink && (
169
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4">
170
+ <div className="flex items-center gap-2 text-green-700 font-semibold mb-3">
171
+ <Check size={20} />
172
+ Photos Delivered!
173
+ </div>
174
+ <p className="text-sm text-green-600 mb-3">
175
+ Download link expires on{' '}
176
+ {new Date(delivery.linkExpiresAt).toLocaleDateString()}
177
+ </p>
178
+ <a
179
+ href={delivery.downloadLink}
180
+ className="inline-flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 font-medium"
181
+ >
182
+ <Download size={18} /> Download Photos
183
+ </a>
184
+ </div>
185
+ )}
186
+
187
+ {/* Revision Request */}
188
+ {delivery.status === 'revision_requested' && (
189
+ <div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
190
+ <div className="flex items-center gap-2 text-amber-700 font-semibold mb-2">
191
+ <AlertCircle size={20} />
192
+ Revision Requested
193
+ </div>
194
+ <p className="text-sm text-amber-700 mb-3">{delivery.revisionNotes}</p>
195
+ {role === 'photographer' && (
196
+ <p className="text-xs text-amber-600">
197
+ Please review and upload revised photos
198
+ </p>
199
+ )}
200
+ </div>
201
+ )}
202
+ </div>
203
+ );
204
+ };
205
+
206
+ export default DeliveryPanel;
client/src/components/InvoiceView.jsx ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Download, Eye, Loader2 } from 'lucide-react';
3
+ import api from '../api/api';
4
+ import { downloadInvoiceAsPDF, downloadInvoiceAsJSON } from '../utils/invoiceGenerator';
5
+
6
+ const InvoiceView = ({ invoiceId, photographer, customer }) => {
7
+ const [invoice, setInvoice] = useState(null);
8
+ const [loading, setLoading] = useState(true);
9
+
10
+ useEffect(() => {
11
+ fetchInvoice();
12
+ }, [invoiceId]);
13
+
14
+ const fetchInvoice = async () => {
15
+ try {
16
+ const res = await api.get(`/invoices/${invoiceId}`);
17
+ setInvoice(res.data);
18
+ } catch (err) {
19
+ console.error('Failed to load invoice:', err);
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ };
24
+
25
+ if (loading) {
26
+ return (
27
+ <div className="flex items-center justify-center py-8">
28
+ <Loader2 className="animate-spin text-blue-600" size={28} />
29
+ </div>
30
+ );
31
+ }
32
+
33
+ if (!invoice) {
34
+ return <div className="text-center text-gray-600">Invoice not found</div>;
35
+ }
36
+
37
+ const statusColor = {
38
+ pending: 'amber',
39
+ paid: 'green',
40
+ overdue: 'red',
41
+ };
42
+
43
+ const color = statusColor[invoice.paymentStatus];
44
+
45
+ return (
46
+ <div className="space-y-6">
47
+ {/* Header */}
48
+ <div className="bg-white border border-gray-200 rounded-lg p-6">
49
+ <div className="flex items-center justify-between mb-4">
50
+ <div>
51
+ <h2 className="text-2xl font-bold text-gray-900">Invoice</h2>
52
+ <p className="text-lg text-gray-600 font-semibold">{invoice.invoiceNumber}</p>
53
+ </div>
54
+ <div className={`px-4 py-2 rounded-lg bg-${color}-100 text-${color}-700 font-bold capitalize`}>
55
+ {invoice.paymentStatus}
56
+ </div>
57
+ </div>
58
+
59
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
60
+ <div>
61
+ <p className="text-gray-500">Issue Date</p>
62
+ <p className="font-semibold">{new Date(invoice.issueDate).toLocaleDateString()}</p>
63
+ </div>
64
+ <div>
65
+ <p className="text-gray-500">Due Date</p>
66
+ <p className="font-semibold">{new Date(invoice.dueDate).toLocaleDateString()}</p>
67
+ </div>
68
+ <div>
69
+ <p className="text-gray-500">From</p>
70
+ <p className="font-semibold">{photographer?.firstName} {photographer?.lastName}</p>
71
+ </div>
72
+ <div>
73
+ <p className="text-gray-500">To</p>
74
+ <p className="font-semibold">{customer?.firstName} {customer?.lastName}</p>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ {/* Items Table */}
80
+ <div className="bg-white border border-gray-200 rounded-lg overflow-hidden">
81
+ <table className="w-full">
82
+ <thead className="bg-gray-50 border-b">
83
+ <tr>
84
+ <th className="px-6 py-3 text-left text-sm font-semibold text-gray-700">Description</th>
85
+ <th className="px-6 py-3 text-right text-sm font-semibold text-gray-700">Quantity</th>
86
+ <th className="px-6 py-3 text-right text-sm font-semibold text-gray-700">Unit Price</th>
87
+ <th className="px-6 py-3 text-right text-sm font-semibold text-gray-700">Total</th>
88
+ </tr>
89
+ </thead>
90
+ <tbody>
91
+ {invoice.items?.map((item, idx) => (
92
+ <tr key={idx} className="border-b hover:bg-gray-50">
93
+ <td className="px-6 py-3 text-gray-900">{item.description}</td>
94
+ <td className="px-6 py-3 text-right text-gray-900">{item.quantity}</td>
95
+ <td className="px-6 py-3 text-right text-gray-900">₹{item.unitPrice.toFixed(2)}</td>
96
+ <td className="px-6 py-3 text-right text-gray-900">₹{item.total.toFixed(2)}</td>
97
+ </tr>
98
+ ))}
99
+ </tbody>
100
+ </table>
101
+
102
+ {/* Totals */}
103
+ <div className="bg-gray-50 px-6 py-4 space-y-2 text-sm">
104
+ <div className="flex justify-between">
105
+ <span className="text-gray-700">Subtotal</span>
106
+ <span className="font-semibold text-gray-900">₹{invoice.subtotal.toFixed(2)}</span>
107
+ </div>
108
+ <div className="flex justify-between">
109
+ <span className="text-gray-700">Tax ({invoice.taxRate}%)</span>
110
+ <span className="font-semibold text-gray-900">₹{invoice.tax.toFixed(2)}</span>
111
+ </div>
112
+ {invoice.discount > 0 && (
113
+ <div className="flex justify-between">
114
+ <span className="text-gray-700">Discount</span>
115
+ <span className="font-semibold text-gray-900">-₹{invoice.discount.toFixed(2)}</span>
116
+ </div>
117
+ )}
118
+ <div className="border-t pt-2 flex justify-between text-base font-bold">
119
+ <span className="text-gray-900">Total Amount</span>
120
+ <span className="text-blue-600">₹{invoice.totalAmount.toFixed(2)}</span>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ {/* Action Buttons */}
126
+ <div className="flex gap-3">
127
+ <button
128
+ onClick={() => downloadInvoiceAsPDF(invoice, photographer, customer)}
129
+ className="flex-1 flex items-center justify-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 font-medium"
130
+ >
131
+ <Download size={18} /> Download PDF
132
+ </button>
133
+ <button
134
+ onClick={() => downloadInvoiceAsJSON(invoice)}
135
+ className="flex-1 flex items-center justify-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 font-medium"
136
+ >
137
+ <Eye size={18} /> Export JSON
138
+ </button>
139
+ </div>
140
+ </div>
141
+ );
142
+ };
143
+
144
+ export default InvoiceView;
client/src/components/LastMinuteDeals.jsx ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Clock, TrendingDown } from 'lucide-react';
3
+ import api from '../api/api';
4
+
5
+ const LastMinuteDeals = () => {
6
+ const [deals, setDeals] = useState([
7
+ {
8
+ _id: '1',
9
+ photographerId: { firstName: 'Rohit', lastName: 'Sharma', profilePicture: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=300&q=80' },
10
+ title: '20% Wedding Deal',
11
+ discountPercentage: 20,
12
+ originalPrice: 5000,
13
+ discountedPrice: 4000,
14
+ validUntil: new Date(Date.now() + 3 * 60 * 60 * 1000), // 3 hours
15
+ },
16
+ {
17
+ _id: '2',
18
+ photographerId: { firstName: 'Divya', lastName: 'Singh', profilePicture: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=300&q=80' },
19
+ title: '15% Portrait Discount',
20
+ discountPercentage: 15,
21
+ originalPrice: 2500,
22
+ discountedPrice: 2125,
23
+ validUntil: new Date(Date.now() + 5 * 60 * 60 * 1000), // 5 hours
24
+ },
25
+ {
26
+ _id: '3',
27
+ photographerId: { firstName: 'Mohan', lastName: 'Khan', profilePicture: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=300&q=80' },
28
+ title: '25% Event Package Deal',
29
+ discountPercentage: 25,
30
+ originalPrice: 8000,
31
+ discountedPrice: 6000,
32
+ validUntil: new Date(Date.now() + 2 * 60 * 60 * 1000), // 2 hours
33
+ },
34
+ ]);
35
+
36
+ const calculateTimeLeft = (expiresAt) => {
37
+ const now = new Date();
38
+ const diff = expiresAt - now;
39
+ const hours = Math.floor(diff / (60 * 60 * 1000));
40
+ const minutes = Math.floor((diff % (60 * 60 * 1000)) / (60 * 1000));
41
+ return { hours, minutes };
42
+ };
43
+
44
+ return (
45
+ <div>
46
+ <div className="flex items-center justify-between mb-6">
47
+ <h2 className="text-2xl font-bold">🏷️ Last-Minute Deals</h2>
48
+ <a href="/search?deals=true" className="text-blue-600 hover:text-blue-700 font-medium">
49
+ View All →
50
+ </a>
51
+ </div>
52
+
53
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
54
+ {deals.map(deal => {
55
+ const { hours, minutes } = calculateTimeLeft(deal.validUntil);
56
+ return (
57
+ <div key={deal._id} className="bg-white border border-red-200 rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
58
+ {/* Header with Discount Badge */}
59
+ <div className="bg-gradient-to-r from-red-500 to-red-600 text-white p-4 flex items-center justify-between">
60
+ <div>
61
+ <h3 className="font-bold text-lg">{deal.title}</h3>
62
+ <p className="text-red-100 text-sm"><strong>{deal.discountPercentage}% OFF</strong></p>
63
+ </div>
64
+ <div className="bg-white/20 px-3 py-2 rounded-lg text-center">
65
+ <p className="text-sm font-bold">{'🔴'}</p>
66
+ <p className="text-xs">Limited</p>
67
+ </div>
68
+ </div>
69
+
70
+ {/* Photographer Info */}
71
+ <div className="p-4 border-b border-gray-200 flex items-center gap-3">
72
+ <img
73
+ src={deal.photographerId.profilePicture}
74
+ alt={deal.photographerId.firstName}
75
+ className="w-10 h-10 rounded-full object-cover"
76
+ />
77
+ <div>
78
+ <p className="font-semibold text-gray-900">
79
+ {deal.photographerId.firstName} {deal.photographerId.lastName}
80
+ </p>
81
+ <p className="text-xs text-gray-500">Professional Photographer</p>
82
+ </div>
83
+ </div>
84
+
85
+ {/* Pricing */}
86
+ <div className="p-4 space-y-2">
87
+ <div className="flex items-center gap-2">
88
+ <span className="text-sm text-gray-500 line-through">₹{deal.originalPrice}</span>
89
+ <span className="text-2xl font-bold text-red-600">₹{deal.discountedPrice}</span>
90
+ </div>
91
+
92
+ {/* Time Left */}
93
+ <div className="bg-amber-50 border border-amber-200 rounded-lg p-3 flex items-center gap-2">
94
+ <Clock className="text-amber-600" size={18} />
95
+ <div>
96
+ <p className="font-bold text-amber-900">
97
+ {hours}h {minutes}m left
98
+ </p>
99
+ <p className="text-xs text-amber-700">Offer expires soon!</p>
100
+ </div>
101
+ </div>
102
+
103
+ {/* CTA Button */}
104
+ <button className="w-full mt-3 py-2 bg-red-600 text-white font-bold rounded-lg hover:bg-red-700 transition">
105
+ Book Now!
106
+ </button>
107
+ </div>
108
+ </div>
109
+ );
110
+ })}
111
+ </div>
112
+ </div>
113
+ );
114
+ };
115
+
116
+ export default LastMinuteDeals;
client/src/components/Leaderboard.jsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Trophy, Star, Loader2 } from 'lucide-react';
3
+ import api from '../api/api';
4
+
5
+ const Leaderboard = ({ limit = 5 }) => {
6
+ const [photographers, setPhotographers] = useState([
7
+ {
8
+ _id: '1',
9
+ firstName: 'Rohit',
10
+ lastName: 'Sharma',
11
+ profilePicture: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=300&q=80',
12
+ rating: 4.9,
13
+ reviewCount: 142,
14
+ bookingCount: 345,
15
+ },
16
+ {
17
+ _id: '2',
18
+ firstName: 'Sapna',
19
+ lastName: 'Gupta',
20
+ profilePicture: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=300&q=80',
21
+ rating: 4.8,
22
+ reviewCount: 128,
23
+ bookingCount: 312,
24
+ },
25
+ {
26
+ _id: '3',
27
+ firstName: 'Mohan',
28
+ lastName: 'Khan',
29
+ profilePicture: 'https://images.unsplash.com/photo-1503104834685-c826cabba2e6?auto=format&fit=crop&w=300&q=80',
30
+ rating: 4.7,
31
+ reviewCount: 95,
32
+ bookingCount: 287,
33
+ },
34
+ {
35
+ _id: '4',
36
+ firstName: 'Divya',
37
+ lastName: 'Singh',
38
+ profilePicture: 'https://images.unsplash.com/photo-1507289147ce569074b61281aa07d0a93?auto=format&fit=crop&w=300&q=80',
39
+ rating: 4.6,
40
+ reviewCount: 87,
41
+ bookingCount: 245,
42
+ },
43
+ {
44
+ _id: '5',
45
+ firstName: 'Ahmed',
46
+ lastName: 'Malik',
47
+ profilePicture: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=300&q=80',
48
+ rating: 4.5,
49
+ reviewCount: 76,
50
+ bookingCount: 198,
51
+ },
52
+ ]);
53
+
54
+ const getMedalEmoji = (index) => {
55
+ if (index === 0) return '🥇';
56
+ if (index === 1) return '🥈';
57
+ if (index === 2) return '🥉';
58
+ return '';
59
+ };
60
+
61
+ return (
62
+ <div>
63
+ <div className="flex items-center justify-between mb-6">
64
+ <h2 className="text-2xl font-bold flex items-center gap-2">
65
+ <Trophy className="text-amber-500" size={28} />
66
+ Top Photographers
67
+ </h2>
68
+ <a href="/leaderboard" className="text-blue-600 hover:text-blue-700 font-medium">
69
+ View All →
70
+ </a>
71
+ </div>
72
+
73
+ <div className="space-y-2">
74
+ {photographers.slice(0, limit).map((photographer, index) => (
75
+ <div
76
+ key={photographer._id}
77
+ className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow flex items-center gap-4"
78
+ >
79
+ {/* Rank */}
80
+ <div className="text-2xl font-bold text-gray-400 w-8 text-center">
81
+ {getMedalEmoji(index) || `#${index + 1}`}
82
+ </div>
83
+
84
+ {/* Profile Picture */}
85
+ <img
86
+ src={photographer.profilePicture}
87
+ alt={photographer.firstName}
88
+ className="w-12 h-12 rounded-full object-cover flex-shrink-0"
89
+ />
90
+
91
+ {/* Info */}
92
+ <div className="flex-1 min-w-0">
93
+ <h3 className="font-bold text-gray-900">
94
+ {photographer.firstName} {photographer.lastName}
95
+ </h3>
96
+ <div className="flex items-center gap-4 mt-1">
97
+ <div className="flex items-center gap-1">
98
+ <Star className="w-4 h-4 fill-yellow-400 text-yellow-400" />
99
+ <span className="text-sm font-semibold text-gray-700">
100
+ {photographer.rating.toFixed(1)}
101
+ </span>
102
+ <span className="text-xs text-gray-500">({photographer.reviewCount} reviews)</span>
103
+ </div>
104
+ <div className="text-xs text-gray-600">
105
+ <span className="font-semibold">{photographer.bookingCount}</span> bookings
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ {/* CTA */}
111
+ <a
112
+ href={`/photographer/${photographer._id}`}
113
+ className="px-4 py-2 bg-blue-50 hover:bg-blue-100 text-blue-600 font-medium rounded-lg transition whitespace-nowrap flex-shrink-0"
114
+ >
115
+ View Profile
116
+ </a>
117
+ </div>
118
+ ))}
119
+ </div>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ export default Leaderboard;
client/src/components/ShootTimeline.jsx ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Check, Clock, AlertCircle } from 'lucide-react';
2
+
3
+ const ShootTimeline = ({ steps, currentStep, onStepClick }) => {
4
+ const stepIcons = {
5
+ confirmed: Check,
6
+ in_progress: Clock,
7
+ delivered: Check,
8
+ booked: Clock,
9
+ photos_ready: Check,
10
+ };
11
+
12
+ const stepColors = {
13
+ completed: 'bg-green-600 text-white',
14
+ current: 'bg-blue-600 text-white ring-4 ring-blue-200',
15
+ pending: 'bg-gray-300 text-gray-600',
16
+ };
17
+
18
+ const getStepStatus = (stepId) => {
19
+ const currentIndex = steps.findIndex(s => s.key === currentStep);
20
+ const stepIndex = steps.findIndex(s => s.key === stepId);
21
+
22
+ if (stepIndex < currentIndex) return 'completed';
23
+ if (stepIndex === currentIndex) return 'current';
24
+ return 'pending';
25
+ };
26
+
27
+ return (
28
+ <div className="w-full">
29
+ {/* Timeline */}
30
+ <div className="flex items-center justify-between mb-8">
31
+ {steps.map((step, idx) => {
32
+ const status = getStepStatus(step.key);
33
+ const Icon = step.icon;
34
+ const isLast = idx === steps.length - 1;
35
+
36
+ return (
37
+ <div key={step.key} className="flex items-center flex-1">
38
+ {/* Step Circle */}
39
+ <button
40
+ onClick={() => onStepClick?.(step.key)}
41
+ className={`flex items-center justify-center w-12 h-12 rounded-full font-bold transition-all flex-shrink-0 ${
42
+ stepColors[status]
43
+ } ${status === 'pending' ? 'cursor-not-allowed' : 'cursor-pointer hover:shadow-lg'}`}
44
+ >
45
+ {status === 'completed' ? (
46
+ <Check size={20} />
47
+ ) : (
48
+ <Icon size={20} />
49
+ )}
50
+ </button>
51
+
52
+ {/* Step Label */}
53
+ <div className="ml-3 mr-auto">
54
+ <p className={`font-semibold ${status === 'pending' ? 'text-gray-500' : 'text-gray-900'}`}>
55
+ {step.label}
56
+ </p>
57
+ {status === 'current' && (
58
+ <p className="text-xs text-blue-600 font-medium">In Progress</p>
59
+ )}
60
+ </div>
61
+
62
+ {/* Connector Line */}
63
+ {!isLast && (
64
+ <div
65
+ className={`h-1 flex-1 mx-3 rounded-full transition-all ${
66
+ status === 'completed' ? 'bg-green-600' : 'bg-gray-300'
67
+ }`}
68
+ />
69
+ )}
70
+ </div>
71
+ );
72
+ })}
73
+ </div>
74
+
75
+ {/* Current Step Info */}
76
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
77
+ <p className="text-sm text-blue-900">
78
+ <strong>Current Step:</strong> {steps.find(s => s.key === currentStep)?.label}
79
+ </p>
80
+ </div>
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default ShootTimeline;
client/src/components/TrendingSpots.jsx ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { MapPin, Users, Loader2 } from 'lucide-react';
3
+ import api from '../api/api';
4
+
5
+ const TrendingSpots = () => {
6
+ const [spots, setSpots] = useState([
7
+ {
8
+ id: 1,
9
+ name: 'Charminar',
10
+ location: 'Hyderabad',
11
+ image: 'https://images.unsplash.com/photo-1599707367072-cd519bdda84a?auto=format&fit=crop&w=600&q=80',
12
+ shoots: 234,
13
+ photographers: 123,
14
+ },
15
+ {
16
+ id: 2,
17
+ name: 'Gateway of India',
18
+ location: 'Mumbai',
19
+ image: 'https://images.unsplash.com/photo-1567157577867-05ccb1388e66?auto=format&fit=crop&w=600&q=80',
20
+ shoots: 456,
21
+ photographers: 198,
22
+ },
23
+ {
24
+ id: 3,
25
+ name: 'Taj Mahal',
26
+ location: 'Agra',
27
+ image: 'https://images.unsplash.com/photo-1564507592333-c60657eea523?auto=format&fit=crop&w=600&q=80',
28
+ shoots: 298,
29
+ photographers: 156,
30
+ },
31
+ {
32
+ id: 4,
33
+ name: 'Marina Beach',
34
+ location: 'Chennai',
35
+ image: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=600&q=80',
36
+ shoots: 178,
37
+ photographers: 87,
38
+ },
39
+ ]);
40
+
41
+ return (
42
+ <div>
43
+ <div className="flex items-center justify-between mb-6">
44
+ <h2 className="text-2xl font-bold">🏆 Trending Photo Spots</h2>
45
+ <a href="/search" className="text-blue-600 hover:text-blue-700 font-medium">See All →</a>
46
+ </div>
47
+
48
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
49
+ {spots.map(spot => (
50
+ <div key={spot.id} className="group rounded-lg overflow-hidden hover:shadow-lg transition-shadow cursor-pointer">
51
+ <div className="relative aspect-square overflow-hidden bg-gray-200">
52
+ <img
53
+ src={spot.image}
54
+ alt={spot.name}
55
+ className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
56
+ />
57
+ <div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent flex flex-col justify-end p-4 text-white">
58
+ <h3 className="text-lg font-bold">{spot.name}</h3>
59
+ <p className="text-sm text-gray-200 flex items-center gap-1">
60
+ <MapPin size={14} /> {spot.location}
61
+ </p>
62
+ </div>
63
+ </div>
64
+ <div className="bg-white p-4 border-b border-l border-r border-gray-200">
65
+ <div className="grid grid-cols-2 gap-2 text-sm">
66
+ <div className="text-center">
67
+ <p className="font-bold text-gray-900">{spot.shoots}</p>
68
+ <p className="text-xs text-gray-500">Photos</p>
69
+ </div>
70
+ <div className="text-center border-l border-gray-200">
71
+ <p className="font-bold text-gray-900">{spot.photographers}</p>
72
+ <p className="text-xs text-gray-500">Photographers</p>
73
+ </div>
74
+ </div>
75
+ <a
76
+ href={`/search?q=${spot.name}`}
77
+ className="block mt-3 w-full py-2 text-center bg-blue-50 text-blue-600 font-medium rounded hover:bg-blue-100 transition text-sm"
78
+ >
79
+ View Photographers
80
+ </a>
81
+ </div>
82
+ </div>
83
+ ))}
84
+ </div>
85
+ </div>
86
+ );
87
+ };
88
+
89
+ export default TrendingSpots;
client/src/components/VerificationPanel.jsx ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect } from 'react';
2
+ import { Upload, CheckCircle, AlertCircle, Loader2, FileCheck } from 'lucide-react';
3
+ import api from '../api/api';
4
+
5
+ const VerificationPanel = ({ photographerId }) => {
6
+ const [verification, setVerification] = useState(null);
7
+ const [loading, setLoading] = useState(true);
8
+ const [uploadingID, setUploadingID] = useState(false);
9
+ const [uploadingPortfolio, setUploadingPortfolio] = useState(false);
10
+
11
+ useEffect(() => {
12
+ fetchVerificationStatus();
13
+ }, []);
14
+
15
+ const fetchVerificationStatus = async () => {
16
+ try {
17
+ const res = await api.get(`/verification/status/${photographerId}`);
18
+ setVerification(res.data);
19
+ } catch (err) {
20
+ console.error('Failed to load verification status:', err);
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ };
25
+
26
+ const handleIDUpload = async (e) => {
27
+ const file = e.target.files?.[0];
28
+ if (!file) return;
29
+
30
+ // In real app, upload to Cloudinary or similar
31
+ setUploadingID(true);
32
+ try {
33
+ // Mock upload - in production use FormData with multipart
34
+ const idUrl = URL.createObjectURL(file);
35
+
36
+ const res = await api.post('/verification/submit', {
37
+ idDocumentUrl: idUrl,
38
+ });
39
+
40
+ setVerification(res.data);
41
+ alert('ID uploaded successfully!');
42
+ } catch (err) {
43
+ alert('Failed to upload ID');
44
+ } finally {
45
+ setUploadingID(false);
46
+ }
47
+ };
48
+
49
+ const handlePortfolioUpload = async (e) => {
50
+ const files = e.target.files;
51
+ if (!files || files.length === 0) return;
52
+
53
+ setUploadingPortfolio(true);
54
+ try {
55
+ const portfolioUrls = Array.from(files).map(file => URL.createObjectURL(file));
56
+
57
+ const res = await api.post('/verification/submit', {
58
+ portfolioUrls,
59
+ });
60
+
61
+ setVerification(res.data);
62
+ alert(`${files.length} portfolio images uploaded!`);
63
+ } catch (err) {
64
+ alert('Failed to upload portfolio images');
65
+ } finally {
66
+ setUploadingPortfolio(false);
67
+ }
68
+ };
69
+
70
+ if (loading) {
71
+ return (
72
+ <div className="flex items-center justify-center py-12">
73
+ <Loader2 className="animate-spin text-blue-600" size={32} />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ const statusColor = {
79
+ verified: 'green',
80
+ pending: 'amber',
81
+ rejected: 'red',
82
+ not_started: 'gray',
83
+ };
84
+
85
+ const statusIcon = {
86
+ verified: <CheckCircle className="text-green-600" size={32} />,
87
+ pending: <AlertCircle className="text-amber-600" size={32} />,
88
+ rejected: <AlertCircle className="text-red-600" size={32} />,
89
+ not_started: <AlertCircle className="text-gray-600" size={32} />,
90
+ };
91
+
92
+ const status = verification?.status || 'not_started';
93
+ const color = statusColor[status];
94
+
95
+ return (
96
+ <div>
97
+ <h2 className="text-2xl font-bold mb-6">📋 Photographer Verification</h2>
98
+
99
+ {/* Status Card */}
100
+ <div className={`bg-${color}-50 border border-${color}-200 rounded-lg p-6 mb-6`}>
101
+ <div className="flex items-center gap-4">
102
+ <div>{statusIcon[status]}</div>
103
+ <div>
104
+ <h3 className={`text-lg font-bold text-${color}-900 capitalize`}>
105
+ Status: {status.replace('_', ' ')}
106
+ </h3>
107
+ {verification?.submittedAt && (
108
+ <p className={`text-sm text-${color}-700`}>
109
+ Submitted: {new Date(verification.submittedAt).toLocaleDateString()}
110
+ </p>
111
+ )}
112
+ {verification?.verifiedAt && (
113
+ <p className={`text-sm text-${color}-700`}>
114
+ Verified: {new Date(verification.verifiedAt).toLocaleDateString()}
115
+ </p>
116
+ )}
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ {/* ID Verification Section */}
122
+ <div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
123
+ <h3 className="text-lg font-bold mb-4 flex items-center gap-2">
124
+ <FileCheck size={20} />
125
+ Step 1: ID Verification
126
+ </h3>
127
+
128
+ {verification?.idDocument ? (
129
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
130
+ <div className="flex items-center gap-2 text-green-700 font-semibold">
131
+ <CheckCircle size={20} />
132
+ ID Document Verified ✓
133
+ </div>
134
+ {verification?.idVerifiedAt && (
135
+ <p className="text-sm text-green-600 mt-1">
136
+ Verified on {new Date(verification.idVerifiedAt).toLocaleDateString()}
137
+ </p>
138
+ )}
139
+ </div>
140
+ ) : (
141
+ <div>
142
+ <p className="text-gray-700 mb-4">
143
+ Upload a clear copy of your government-issued ID (Aadhar, PAN, Passport, etc.)
144
+ </p>
145
+ <label className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-500 cursor-pointer transition">
146
+ <div className="text-center">
147
+ <Upload className="mx-auto mb-2 text-gray-400" size={32} />
148
+ <p className="font-semibold text-gray-700">Click to upload ID</p>
149
+ <p className="text-sm text-gray-500">or drag and drop</p>
150
+ </div>
151
+ <input
152
+ type="file"
153
+ onChange={handleIDUpload}
154
+ disabled={uploadingID}
155
+ className="hidden"
156
+ accept="image/*,.pdf"
157
+ />
158
+ </label>
159
+ {uploadingID && <p className="text-sm text-blue-600 mt-2">Uploading...</p>}
160
+ </div>
161
+ )}
162
+ </div>
163
+
164
+ {/* Portfolio Verification Section */}
165
+ <div className="bg-white border border-gray-200 rounded-lg p-6 mb-6">
166
+ <h3 className="text-lg font-bold mb-4 flex items-center gap-2">
167
+ <FileCheck size={20} />
168
+ Step 2: Portfolio Verification
169
+ </h3>
170
+
171
+ {verification?.portfolioSamples?.length > 0 ? (
172
+ <div>
173
+ <p className="text-green-700 font-semibold mb-4">
174
+ ✓ {verification.portfolioSamples.length} portfolio images uploaded
175
+ </p>
176
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
177
+ {verification.portfolioSamples.map((sample, idx) => (
178
+ <div key={idx} className="aspect-square bg-gray-200 rounded-lg overflow-hidden">
179
+ <img
180
+ src={sample.url}
181
+ alt={`Portfolio ${idx + 1}`}
182
+ className="w-full h-full object-cover"
183
+ />
184
+ </div>
185
+ ))}
186
+ </div>
187
+ </div>
188
+ ) : (
189
+ <div>
190
+ <p className="text-gray-700 mb-4">
191
+ Upload 3-5 of your best portfolio samples to showcase your work quality.
192
+ </p>
193
+ <label className="flex items-center justify-center border-2 border-dashed border-gray-300 rounded-lg p-8 hover:border-blue-500 cursor-pointer transition">
194
+ <div className="text-center">
195
+ <Upload className="mx-auto mb-2 text-gray-400" size={32} />
196
+ <p className="font-semibold text-gray-700">Click to upload portfolio</p>
197
+ <p className="text-sm text-gray-500">Upload multiple images (PNG, JPG)</p>
198
+ </div>
199
+ <input
200
+ type="file"
201
+ onChange={handlePortfolioUpload}
202
+ disabled={uploadingPortfolio}
203
+ multiple
204
+ className="hidden"
205
+ accept="image/*"
206
+ />
207
+ </label>
208
+ {uploadingPortfolio && <p className="text-sm text-blue-600 mt-2">Uploading...</p>}
209
+ </div>
210
+ )}
211
+ </div>
212
+
213
+ {/* Admin Notes */}
214
+ {verification?.adminNotes && (
215
+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
216
+ <h3 className="font-bold text-blue-900 mb-2">Admin Notes</h3>
217
+ <p className="text-blue-800">{verification.adminNotes}</p>
218
+ </div>
219
+ )}
220
+
221
+ {/* Rejection Info */}
222
+ {status === 'rejected' && verification?.rejectionReason && (
223
+ <div className="bg-red-50 border border-red-200 rounded-lg p-6 mb-6">
224
+ <h3 className="font-bold text-red-900 mb-2">Rejection Reason</h3>
225
+ <p className="text-red-800 mb-4">{verification.rejectionReason}</p>
226
+ {verification?.resubmissionAllowed && (
227
+ <button className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 font-medium">
228
+ Resubmit Application
229
+ </button>
230
+ )}
231
+ </div>
232
+ )}
233
+ </div>
234
+ );
235
+ };
236
+
237
+ export default VerificationPanel;
client/src/utils/invoiceGenerator.js ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Invoice Generator Utility
2
+ // Generates invoice data and creates downloadable PDF
3
+
4
+ export const generateInvoiceNumber = (count) => {
5
+ return `INV-2026-${String(count + 1).padStart(6, '0')}`;
6
+ };
7
+
8
+ export const createInvoiceData = (booking, photographer, customer) => {
9
+ const subtotal = booking.totalPrice || 0;
10
+ const taxRate = 18; // 18% GST
11
+ const tax = (subtotal * taxRate) / 100;
12
+ const totalAmount = subtotal + tax;
13
+
14
+ const items = [
15
+ {
16
+ description: `${photographer.firstName} ${photographer.lastName} - Photography Session`,
17
+ quantity: 1,
18
+ unitPrice: subtotal,
19
+ total: subtotal,
20
+ },
21
+ ];
22
+
23
+ return {
24
+ issueDate: new Date(),
25
+ dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
26
+ items,
27
+ subtotal,
28
+ tax,
29
+ taxRate,
30
+ totalAmount,
31
+ photographerId: photographer._id,
32
+ customerId: customer._id,
33
+ bookingId: booking._id,
34
+ };
35
+ };
36
+
37
+ export const formatInvoiceForDisplay = (invoice) => {
38
+ return {
39
+ ...invoice,
40
+ issueDate: new Date(invoice.issueDate).toLocaleDateString('en-IN', {
41
+ year: 'numeric',
42
+ month: 'long',
43
+ day: 'numeric',
44
+ }),
45
+ dueDate: new Date(invoice.dueDate).toLocaleDateString('en-IN', {
46
+ year: 'numeric',
47
+ month: 'long',
48
+ day: 'numeric',
49
+ }),
50
+ };
51
+ };
52
+
53
+ // Simple text-based invoice template (can be extended with jsPDF for actual PDF)
54
+ export const generateInvoiceHTML = (invoice, photographer, customer) => {
55
+ const formatted = formatInvoiceForDisplay(invoice);
56
+
57
+ return `
58
+ <!DOCTYPE html>
59
+ <html>
60
+ <head>
61
+ <meta charset="UTF-8">
62
+ <style>
63
+ body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
64
+ .container { max-width: 800px; margin: 0 auto; border: 1px solid #ddd; padding: 40px; }
65
+ .header { text-align: center; margin-bottom: 40px; border-bottom: 2px solid #2563eb; padding-bottom: 20px; }
66
+ .header h1 { margin: 0; color: #2563eb; }
67
+ .header .tagline { color: #666; font-size: 14px; }
68
+ .info-row { display: flex; justify-content: space-between; margin: 20px 0; }
69
+ .info-section { flex: 1; }
70
+ .info-section h3 { margin: 0 0 10px 0; color: #333; }
71
+ .info-section p { margin: 5px 0; color: #666; }
72
+ table { width: 100%; border-collapse: collapse; margin: 30px 0; }
73
+ table thead { background: #f3f4f6; }
74
+ table th, table td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
75
+ .total-row { background: #f3f4f6; font-weight: bold; }
76
+ .tax-row { background: #fff; }
77
+ .grand-total { background: #2563eb; color: white; font-weight: bold; }
78
+ .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center; color: #666; font-size: 12px; }
79
+ .payment-status { margin: 20px 0; padding: 15px; background: #dbeafe; border-left: 4px solid #2563eb; }
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <div class="container">
84
+ <div class="header">
85
+ <h1>INVOICE</h1>
86
+ <p class="tagline">SnapLocal - Professional Photography Services</p>
87
+ </div>
88
+
89
+ <div class="info-row">
90
+ <div class="info-section">
91
+ <h3>Invoice Details</h3>
92
+ <p><strong>Invoice #:</strong> ${invoice.invoiceNumber}</p>
93
+ <p><strong>Issue Date:</strong> ${formatted.issueDate}</p>
94
+ <p><strong>Due Date:</strong> ${formatted.dueDate}</p>
95
+ </div>
96
+ <div class="info-section">
97
+ <h3>From</h3>
98
+ <p><strong>${photographer.firstName} ${photographer.lastName}</strong></p>
99
+ <p>${photographer.bio || 'Professional Photographer'}</p>
100
+ <p>Email: ${photographer.email || 'N/A'}</p>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="info-row">
105
+ <div class="info-section">
106
+ <h3>Bill To</h3>
107
+ <p><strong>${customer.firstName} ${customer.lastName}</strong></p>
108
+ <p>Email: ${customer.email}</p>
109
+ </div>
110
+ </div>
111
+
112
+ <table>
113
+ <thead>
114
+ <tr>
115
+ <th>Description</th>
116
+ <th>Quantity</th>
117
+ <th>Unit Price</th>
118
+ <th>Total</th>
119
+ </tr>
120
+ </thead>
121
+ <tbody>
122
+ ${invoice.items
123
+ .map(
124
+ item =>
125
+ `<tr>
126
+ <td>${item.description}</td>
127
+ <td>${item.quantity}</td>
128
+ <td>₹${item.unitPrice.toFixed(2)}</td>
129
+ <td>₹${item.total.toFixed(2)}</td>
130
+ </tr>`
131
+ )
132
+ .join('')}
133
+ <tr class="total-row">
134
+ <td colspan="3" style="text-align: right;">Subtotal:</td>
135
+ <td>₹${invoice.subtotal.toFixed(2)}</td>
136
+ </tr>
137
+ <tr class="tax-row">
138
+ <td colspan="3" style="text-align: right;">Tax (${invoice.taxRate}%):</td>
139
+ <td>₹${invoice.tax.toFixed(2)}</td>
140
+ </tr>
141
+ ${
142
+ invoice.discount > 0
143
+ ? `<tr class="tax-row">
144
+ <td colspan="3" style="text-align: right;">Discount:</td>
145
+ <td>-₹${invoice.discount.toFixed(2)}</td>
146
+ </tr>`
147
+ : ''
148
+ }
149
+ <tr class="grand-total">
150
+ <td colspan="3" style="text-align: right;">TOTAL AMOUNT DUE:</td>
151
+ <td>₹${invoice.totalAmount.toFixed(2)}</td>
152
+ </tr>
153
+ </tbody>
154
+ </table>
155
+
156
+ <div class="payment-status">
157
+ <strong>Payment Status:</strong> ${invoice.paymentStatus.toUpperCase()}
158
+ ${
159
+ invoice.paidAt
160
+ ? `<p>Paid on ${new Date(invoice.paidAt).toLocaleDateString()}</p>`
161
+ : ''
162
+ }
163
+ </div>
164
+
165
+ <div class="footer">
166
+ <p>Thank you for choosing SnapLocal! This is an automatically generated invoice.</p>
167
+ <p>For support, contact us at support@snaplocal.com</p>
168
+ </div>
169
+ </div>
170
+ </body>
171
+ </html>
172
+ `;
173
+ };
174
+
175
+ export const downloadInvoiceAsPDF = async (invoice, photographer, customer) => {
176
+ // Simple approach: Generate HTML and open in new window for manual PDF save
177
+ const htmlContent = generateInvoiceHTML(invoice, photographer, customer);
178
+ const newWindow = window.open('', '', 'width=900,height=600');
179
+ newWindow.document.write(htmlContent);
180
+ newWindow.document.close();
181
+ newWindow.print();
182
+ };
183
+
184
+ export const downloadInvoiceAsJSON = (invoice) => {
185
+ const dataStr = JSON.stringify(invoice, null, 2);
186
+ const dataBlob = new Blob([dataStr], { type: 'application/json' });
187
+ const url = URL.createObjectURL(dataBlob);
188
+ const link = document.createElement('a');
189
+ link.href = url;
190
+ link.download = `${invoice.invoiceNumber}.json`;
191
+ link.click();
192
+ };
client/vercel.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "buildCommand": "npm run build",
3
+ "outputDirectory": "dist",
4
+ "installCommand": "npm install",
5
+ "framework": "vite",
6
+ "rewrites": [
7
+ {
8
+ "source": "/(.*)",
9
+ "destination": "/index.html"
10
+ }
11
+ ]
12
+ }
server/.env.example ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ PORT=5000
2
+ MONGODB_URI=your_mongodb_atlas_uri
3
+ JWT_SECRET=your_jwt_secret
4
+ ALLOWED_ORIGINS=http://localhost:5173,https://your-vercel-domain.vercel.app
5
+ # Cloudinary Credentials
6
+ CLOUDINARY_CLOUD_NAME=your_cloud_name
7
+ CLOUDINARY_API_KEY=your_api_key
8
+ CLOUDINARY_API_SECRET=your_api_secret
server/src/index.js CHANGED
@@ -11,8 +11,9 @@ const app = express();
11
  const server = http.createServer(app);
12
  const io = new Server(server, {
13
  cors: {
14
- origin: "*", // Adjust in production
15
- methods: ["GET", "POST", "PUT", "DELETE"]
 
16
  }
17
  });
18
 
@@ -73,7 +74,10 @@ app.use((req, res, next) => {
73
  });
74
 
75
  // Middleware
76
- app.use(cors());
 
 
 
77
  app.use(express.json());
78
 
79
  // Routes
 
11
  const server = http.createServer(app);
12
  const io = new Server(server, {
13
  cors: {
14
+ origin: process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : "*",
15
+ methods: ["GET", "POST", "PUT", "DELETE"],
16
+ credentials: true
17
  }
18
  });
19
 
 
74
  });
75
 
76
  // Middleware
77
+ app.use(cors({
78
+ origin: process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : "*",
79
+ credentials: true
80
+ }));
81
  app.use(express.json());
82
 
83
  // Routes