eeshaAI commited on
Commit
5ac7518
·
1 Parent(s): 2454ba7

feat: Migrate frontend from static HTML to Next.js 16 App Router

Browse files

- All 16 buyer pages: home, login, product, cart, checkout, payment,
categories, search, seller-profile, profile, orders, wishlist,
videos, live, live-watch, group-buy, ai-chat, link-account, telegram
- 7 seller dashboard pages with shared sidebar layout
- 22 API route proxies to existing Supabase Edge Functions
- Suspense boundaries for useSearchParams pages
- Standalone output build for Docker deployment
- Dockerfile (multi-stage Node 20 alpine, port 7860)
- README with HF Space YAML frontmatter
- .hfignore for lean Docker context

Backend unchanged: 27 Supabase Edge Functions, Render bot, OpenWA,
PalmPay payment verifier — all remain in place.

Files changed (2) hide show
  1. src/app/product/page.tsx +326 -277
  2. src/app/videos/page.tsx +328 -174
src/app/product/page.tsx CHANGED
@@ -11,7 +11,8 @@ import { Textarea } from '@/components/ui/textarea';
11
  import {
12
  ShoppingCart, Heart, Share2, Store, Star, ChevronLeft, ChevronRight,
13
  MessageSquare, ThumbsUp, Truck, Shield, RotateCcw, Users,
14
- MessageCircle
 
15
  } from 'lucide-react';
16
  import Link from 'next/link';
17
  import { useRouter } from 'next/navigation';
@@ -37,6 +38,8 @@ function ProductContent() {
37
  const [reviewRating, setReviewRating] = useState(5);
38
  const [reviewTitle, setReviewTitle] = useState('');
39
  const [reviewComment, setReviewComment] = useState('');
 
 
40
 
41
  const load = useCallback(async () => {
42
  if (!id) return;
@@ -62,6 +65,21 @@ function ProductContent() {
62
 
63
  useEffect(() => { load(); }, [load]);
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  const images = product?.additional_images?.length
66
  ? [product.image_url, ...product.additional_images!].filter(Boolean)
67
  : product?.image_url ? [product.image_url] : [];
@@ -136,6 +154,11 @@ function ProductContent() {
136
  else { navigator.clipboard.writeText(url); toast({ title: 'Link copied' }); }
137
  };
138
 
 
 
 
 
 
139
  if (loading) {
140
  return (
141
  <div className="flex items-center justify-center min-h-[60vh]">
@@ -154,232 +177,179 @@ function ProductContent() {
154
  }
155
 
156
  return (
157
- <div className="max-w-5xl mx-auto px-3 sm:px-4 lg:px-6 pb-28">
158
- {/* Breadcrumb */}
159
- <div className="text-sm text-slate-400 py-3">
160
- <Link href="/" className="hover:text-primary">Home</Link>
161
- {' / '}
162
- <Link href={`/categories?category=${product.category || ''}`} className="hover:text-primary">{product.category || 'All'}</Link>
163
- {' / '}
164
- <span className="text-slate-600">{product.name}</span>
165
- </div>
166
-
167
- <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
168
- {/* Image gallery */}
169
- <div className="lg:col-span-5">
170
- <Card className="overflow-hidden border-slate-100 relative">
171
- <div className="aspect-square bg-slate-50 relative">
172
- {images[activeImage] ? (
173
- <img src={images[activeImage]} alt={product.name} className="w-full h-full object-cover" />
174
- ) : (
175
- <div className="w-full h-full flex items-center justify-center text-slate-300">
176
- <Store className="w-16 h-16" />
177
- </div>
178
- )}
179
- {images.length > 1 && (
180
- <>
181
- <button
182
- onClick={() => setActiveImage((activeImage - 1 + images.length) % images.length)}
183
- className="absolute left-2 top-1/2 -translate-y-1/2 bg-white/90 backdrop-blur rounded-full p-2 shadow"
184
- >
185
- <ChevronLeft className="w-5 h-5" />
186
- </button>
187
- <button
188
- onClick={() => setActiveImage((activeImage + 1) % images.length)}
189
- className="absolute right-2 top-1/2 -translate-y-1/2 bg-white/90 backdrop-blur rounded-full p-2 shadow"
190
- >
191
- <ChevronRight className="w-5 h-5" />
192
- </button>
193
- <div className="absolute bottom-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded-full">
194
- {activeImage + 1} / {images.length}
195
- </div>
196
- </>
197
- )}
198
- </div>
199
- {images.length > 1 && (
200
- <div className="flex gap-2 p-3 overflow-x-auto no-scrollbar">
201
- {images.map((img, i) => (
202
- <button
203
- key={i}
204
- onClick={() => setActiveImage(i)}
205
- className={`shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 ${
206
- activeImage === i ? 'border-primary' : 'border-slate-100'
207
- }`}
208
- >
209
- <img src={img} alt="" className="w-full h-full object-cover" />
210
- </button>
211
- ))}
212
- </div>
213
- )}
214
- </Card>
215
-
216
- {/* Action buttons */}
217
- <div className="grid grid-cols-4 gap-2 mt-3">
218
- <Button onClick={addToWishlist} variant="outline" size="sm" className="border-slate-200">
219
- <Heart className="w-4 h-4" />
220
- </Button>
221
- <Button onClick={() => shareProduct('whatsapp')} variant="outline" size="sm" className="border-slate-200">
222
- <Share2 className="w-4 h-4 text-green-500" />
223
- </Button>
224
- <Button onClick={() => shareProduct('telegram')} variant="outline" size="sm" className="border-slate-200">
225
- <Share2 className="w-4 h-4 text-blue-500" />
226
- </Button>
227
- <Button onClick={() => shareProduct('copy')} variant="outline" size="sm" className="border-slate-200">
228
- <Share2 className="w-4 h-4" />
229
- </Button>
230
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  </div>
 
232
 
233
- {/* Product info */}
234
- <div className="lg:col-span-7 space-y-4">
235
- {/* Title (24px) */}
236
- <div>
237
- <h1 className="text-xl sm:text-2xl font-bold text-slate-900 leading-tight">{product.name}</h1>
238
- {product.category && (
239
- <Badge variant="secondary" className="mt-2">{product.category}</Badge>
240
- )}
241
- </div>
242
 
243
- {/* Price — large cyan */}
244
- <div className="flex items-baseline gap-3">
245
- <span className="text-3xl font-extrabold price">{formatPrice(product.price)}</span>
246
- {typeof product.units_sold === 'number' && product.units_sold > 0 && (
247
- <span className="text-sm text-slate-500">{product.units_sold} sold</span>
248
- )}
249
- </div>
250
 
251
- {/* Pay on delivery banner — green */}
252
- <div className="bg-green-50 border border-green-200 rounded-lg px-3 py-2 flex items-center gap-2">
253
- <div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center">
254
- <svg viewBox="0 0 12 12" className="w-3 h-3 text-white" fill="currentColor"><path d="M10 3L4.5 8.5 2 6"/></svg>
255
- </div>
256
- <span className="text-sm text-green-800 font-semibold">Pay on delivery available</span>
257
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
 
259
- {/* Rating */}
260
- <div className="flex items-center gap-2">
261
- <div className="flex">
262
- {[1, 2, 3, 4, 5].map((s) => (
263
- <Star
264
- key={s}
265
- className={`w-4 h-4 ${s <= Math.round(reviewSummary.avg) ? 'fill-yellow-400 text-yellow-400' : 'text-slate-200'}`}
266
- />
267
- ))}
268
- </div>
269
- <span className="text-sm text-slate-600">
270
- {reviewSummary.count > 0
271
- ? `${reviewSummary.avg.toFixed(1)} (${reviewSummary.count} reviews)`
272
- : 'No reviews yet'}
273
- </span>
274
- </div>
275
 
276
- {/* Seller card */}
277
- {seller && (
278
- <Link href={`/seller-profile?id=${seller.id}`}>
279
- <Card className="p-3 flex items-center gap-3 hover:shadow-md transition-shadow border-slate-100">
280
- <div className="w-12 h-12 rounded-full brand-gradient flex items-center justify-center text-white font-bold text-lg shrink-0">
281
- {(seller.business_name || 'S').charAt(0)}
282
- </div>
 
 
 
 
 
283
  <div className="flex-1 min-w-0">
284
- <div className="font-bold text-sm text-slate-900 truncate">{seller.business_name || 'Unnamed store'}</div>
285
- <div className="text-xs text-slate-500">{seller.business_category || 'Seller'}</div>
 
 
 
 
 
 
286
  </div>
287
- <Button size="sm" variant="outline">Visit</Button>
288
- </Card>
289
- </Link>
290
- )}
291
-
292
- {/* Group buy banner */}
293
- {groupBuys.length > 0 && (
294
- <Card className="p-4 brand-gradient border-0">
295
- <div className="flex items-center gap-2 text-white mb-1">
296
- <Users className="w-4 h-4" />
297
- <span className="font-bold text-sm">Active Group Buy</span>
298
- </div>
299
- <p className="text-xs text-white/85 mb-3">
300
- Join with {groupBuys.length} other shopper(s) and unlock up to 20% off
301
- </p>
302
- <div className="space-y-2">
303
- {groupBuys.slice(0, 2).map((gb) => (
304
- <Link key={gb.id} href={`/group-buy?id=${gb.id}`}>
305
- <div className="bg-white/15 backdrop-blur rounded-lg p-2 flex items-center justify-between text-white">
306
- <div>
307
- <div className="text-sm font-bold">{gb.current_count || 1} / {gb.target_count} joined</div>
308
- <div className="text-xs opacity-80">{gb.discount_pct}% off when target reached</div>
309
- </div>
310
- <Button size="sm" variant="secondary">Join</Button>
311
- </div>
312
- </Link>
313
- ))}
314
  </div>
315
  </Card>
316
- )}
317
-
318
- {/* Quantity + Add to cart */}
319
- <div className="flex items-center gap-3 py-2">
320
- <span className="text-sm text-slate-600">Qty:</span>
321
- <div className="flex items-center border-2 border-slate-200 rounded-xl overflow-hidden">
322
- <button
323
- onClick={() => setQty(Math.max(1, qty - 1))}
324
- className="px-4 py-2 text-slate-500 hover:bg-slate-50 text-base"
325
- >−</button>
326
- <span className="px-4 py-2 font-bold text-base">{qty}</span>
327
- <button
328
- onClick={() => setQty(qty + 1)}
329
- className="px-4 py-2 text-slate-500 hover:bg-slate-50 text-base"
330
- >+</button>
331
- </div>
332
- </div>
333
-
334
- <Button onClick={startGroupBuy} variant="outline" className="w-full border-primary text-primary font-bold hover:bg-primary/5">
335
- <Users className="w-4 h-4 mr-2" /> Start group buy · 20% off
336
- </Button>
337
-
338
- {/* Trust badges */}
339
- <div className="grid grid-cols-3 gap-2 py-2">
340
- <div className="text-center p-3 bg-slate-50 rounded-xl">
341
- <RotateCcw className="w-5 h-5 mx-auto text-green-600 mb-1" />
342
- <div className="text-xs font-semibold text-slate-700">7-Day Return</div>
343
- </div>
344
- <div className="text-center p-3 bg-slate-50 rounded-xl">
345
- <Shield className="w-5 h-5 mx-auto text-primary mb-1" />
346
- <div className="text-xs font-semibold text-slate-700">Buyer Protection</div>
347
- </div>
348
- <div className="text-center p-3 bg-slate-50 rounded-xl">
349
- <Truck className="w-5 h-5 mx-auto text-slate-600 mb-1" />
350
- <div className="text-xs font-semibold text-slate-700">Fast Shipping</div>
351
- </div>
352
- </div>
353
 
354
- {/* Description */}
355
- {product.description && (
356
- <Card className="p-4 border-slate-100">
357
- <h3 className="font-bold text-base mb-2">Description</h3>
358
- <p className="text-sm text-slate-600 whitespace-pre-wrap leading-relaxed">{product.description}</p>
359
- </Card>
360
- )}
 
 
361
  </div>
 
 
 
 
 
362
  </div>
363
 
364
- {/* Reviews section */}
365
- <section className="mt-8">
366
- <div className="flex items-center justify-between mb-4">
367
- <h2 className="text-lg font-bold flex items-center gap-2">
368
- <MessageSquare className="w-5 h-5 text-primary" />
369
- Reviews ({reviewSummary.count})
370
- </h2>
371
- {user && (
372
- <Button size="sm" variant="outline" onClick={() => setShowReviewForm(!showReviewForm)}>
373
- Write a review
374
- </Button>
375
  )}
376
  </div>
377
 
378
  {showReviewForm && (
379
- <Card className="p-4 mb-4 border-slate-100 space-y-3">
380
  <div>
381
  <div className="text-sm font-semibold mb-1">Rating</div>
382
- <div className="flex gap-1">
383
  {[1, 2, 3, 4, 5].map((s) => (
384
  <button key={s} onClick={() => setReviewRating(s)}>
385
  <Star className={`w-6 h-6 ${s <= reviewRating ? 'fill-yellow-400 text-yellow-400' : 'text-slate-200'}`} />
@@ -387,70 +357,61 @@ function ProductContent() {
387
  ))}
388
  </div>
389
  </div>
390
- <div>
391
- <div className="text-sm font-semibold mb-1">Title</div>
392
- <input
393
- type="text"
394
- value={reviewTitle}
395
- onChange={(e) => setReviewTitle(e.target.value)}
396
- placeholder="Summarize your experience"
397
- className="w-full px-3 py-2 border border-slate-200 rounded-lg text-base"
398
- />
399
- </div>
400
- <div>
401
- <div className="text-sm font-semibold mb-1">Comment</div>
402
- <Textarea
403
- value={reviewComment}
404
- onChange={(e) => setReviewComment(e.target.value)}
405
- placeholder="Share details about the product quality, delivery, etc."
406
- rows={3}
407
- />
408
- </div>
409
- <Button onClick={submitReview} className="brand-gradient text-white">
410
- Submit review
411
- </Button>
412
  </Card>
413
  )}
414
 
415
  {reviews.length === 0 ? (
416
- <Card className="p-8 text-center border-slate-100">
417
  <MessageSquare className="w-8 h-8 mx-auto text-slate-300 mb-2" />
418
- <p className="text-sm text-slate-500">No reviews yet. Be the first!</p>
 
 
 
 
 
419
  </Card>
420
  ) : (
421
  <div className="space-y-3">
422
- {reviews.map((r) => (
423
- <Card key={r.id} className="p-4 border-slate-100">
424
- <div className="flex items-start gap-3">
425
- <div className="w-9 h-9 rounded-full brand-gradient flex items-center justify-center text-white font-bold text-sm shrink-0">
426
  {(r.reviewer_name || 'A').charAt(0).toUpperCase()}
427
  </div>
428
  <div className="flex-1">
429
- <div className="flex items-center gap-2 flex-wrap">
430
- <span className="font-bold text-sm">{r.reviewer_name || 'Anonymous'}</span>
431
  {r.verified_purchase && (
432
- <Badge variant="secondary" className="text-xs bg-green-100 text-green-700">
433
- Verified purchase
434
- </Badge>
435
  )}
436
- <span className="text-xs text-slate-400">{timeAgo(r.created_at)}</span>
437
  </div>
438
- <div className="flex my-1">
439
  {[1, 2, 3, 4, 5].map((s) => (
440
- <Star key={s} className={`w-3.5 h-3.5 ${s <= r.rating ? 'fill-yellow-400 text-yellow-400' : 'text-slate-200'}`} />
441
  ))}
442
  </div>
443
- {r.title && <div className="font-bold text-sm">{r.title}</div>}
444
- {r.comment && <p className="text-sm text-slate-600 mt-1">{r.comment}</p>}
445
- <button
446
- onClick={async () => {
447
- const res = await api.reviews.helpful(r.id);
448
- if (res.success) load();
449
- }}
450
- className="flex items-center gap-1 text-xs text-slate-500 hover:text-primary mt-2"
451
- >
452
- <ThumbsUp className="w-3.5 h-3.5" /> Helpful ({r.helpful_count || 0})
453
- </button>
454
  </div>
455
  </div>
456
  </Card>
@@ -459,39 +420,127 @@ function ProductContent() {
459
  )}
460
  </section>
461
 
462
- {/* Bottom CTA bar — fixed, Temu-style layout but with cyan */}
463
- <div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-slate-200 px-3 py-2 flex items-center gap-2 md:max-w-5xl md:mx-auto md:rounded-t-2xl md:bottom-2 md:border md:shadow-lg">
464
- {/* Left: store/save/chat icons */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
  <Link href={seller ? `/seller-profile?id=${seller.id}` : '/'} className="flex flex-col items-center justify-center px-2 py-1">
466
- <Store className="w-5 h-5 text-slate-700" />
467
- <span className="text-xs text-slate-600">Store</span>
468
  </Link>
469
  <button onClick={addToWishlist} className="flex flex-col items-center justify-center px-2 py-1">
470
- <Heart className="w-5 h-5 text-slate-700" />
471
- <span className="text-xs text-slate-600">Save</span>
472
  </button>
473
- <Link href="/cart" className="flex flex-col items-center justify-center px-2 py-1">
474
- <MessageCircle className="w-5 h-5 text-slate-700" />
475
- <span className="text-xs text-slate-600">Chat</span>
476
  </Link>
477
 
478
- {/* Right: Add to cart + Buy Now */}
479
- <Button
480
- onClick={addToCart}
481
- disabled={adding}
482
- variant="outline"
483
- className="flex-1 h-11 border-primary text-primary font-bold text-base hover:bg-primary/5"
484
- >
485
- <ShoppingCart className="w-4 h-4 mr-1" />
486
- Cart
487
- </Button>
488
- <Button
489
  onClick={buyNow}
490
  disabled={adding}
491
- className="flex-1 h-11 brand-gradient text-white font-bold text-base glow"
492
  >
493
- Buy Now · {formatPrice(product.price * qty)}
494
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  </div>
496
  </div>
497
  );
 
11
  import {
12
  ShoppingCart, Heart, Share2, Store, Star, ChevronLeft, ChevronRight,
13
  MessageSquare, ThumbsUp, Truck, Shield, RotateCcw, Users,
14
+ MessageCircle, Clock, ChevronRight as Arrow, MapPin, Flag, MoreHorizontal,
15
+ CheckCircle, Award
16
  } from 'lucide-react';
17
  import Link from 'next/link';
18
  import { useRouter } from 'next/navigation';
 
38
  const [reviewRating, setReviewRating] = useState(5);
39
  const [reviewTitle, setReviewTitle] = useState('');
40
  const [reviewComment, setReviewComment] = useState('');
41
+ // Fake countdown timer (like the reference's "Ending soon 00:06:45.7")
42
+ const [countdown, setCountdown] = useState({ m: 6, s: 45, ms: 7 });
43
 
44
  const load = useCallback(async () => {
45
  if (!id) return;
 
65
 
66
  useEffect(() => { load(); }, [load]);
67
 
68
+ // Countdown timer effect
69
+ useEffect(() => {
70
+ const interval = setInterval(() => {
71
+ setCountdown(prev => {
72
+ let { m, s, ms } = prev;
73
+ ms -= 1;
74
+ if (ms < 0) { ms = 9; s -= 1; }
75
+ if (s < 0) { s = 59; m -= 1; }
76
+ if (m < 0) { m = 59; }
77
+ return { m, s, ms };
78
+ });
79
+ }, 100);
80
+ return () => clearInterval(interval);
81
+ }, []);
82
+
83
  const images = product?.additional_images?.length
84
  ? [product.image_url, ...product.additional_images!].filter(Boolean)
85
  : product?.image_url ? [product.image_url] : [];
 
154
  else { navigator.clipboard.writeText(url); toast({ title: 'Link copied' }); }
155
  };
156
 
157
+ const formatCountdown = () => {
158
+ const { m, s, ms } = countdown;
159
+ return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}.${ms}`;
160
+ };
161
+
162
  if (loading) {
163
  return (
164
  <div className="flex items-center justify-center min-h-[60vh]">
 
177
  }
178
 
179
  return (
180
+ <div className="bg-white pb-20">
181
+ {/* === 1. IMAGE GALLERY (full-width, page indicator bottom-right) === */}
182
+ <div className="relative aspect-square bg-slate-50">
183
+ {images[activeImage] ? (
184
+ <img src={images[activeImage]} alt={product.name} className="w-full h-full object-cover" />
185
+ ) : (
186
+ <div className="w-full h-full flex items-center justify-center text-slate-300">
187
+ <Store className="w-16 h-16" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  </div>
189
+ )}
190
+ {images.length > 1 && (
191
+ <>
192
+ <button
193
+ onClick={() => setActiveImage((activeImage - 1 + images.length) % images.length)}
194
+ className="absolute left-2 top-1/2 -translate-y-1/2 bg-white/90 backdrop-blur rounded-full p-2 shadow"
195
+ >
196
+ <ChevronLeft className="w-5 h-5" />
197
+ </button>
198
+ <button
199
+ onClick={() => setActiveImage((activeImage + 1) % images.length)}
200
+ className="absolute right-2 top-1/2 -translate-y-1/2 bg-white/90 backdrop-blur rounded-full p-2 shadow"
201
+ >
202
+ <ChevronRight className="w-5 h-5" />
203
+ </button>
204
+ <div className="absolute bottom-2 right-2 bg-black/50 text-white text-xs px-2 py-1 rounded-full">
205
+ {activeImage + 1} / {images.length}
206
+ </div>
207
+ </>
208
+ )}
209
+ {/* Share + wishlist top-right */}
210
+ <div className="absolute top-3 right-3 flex flex-col gap-2">
211
+ <button onClick={shareProduct.bind(null, 'whatsapp')} className="w-8 h-8 rounded-full bg-white/90 backdrop-blur flex items-center justify-center shadow">
212
+ <Share2 className="w-4 h-4 text-green-600" />
213
+ </button>
214
+ <button onClick={addToWishlist} className="w-8 h-8 rounded-full bg-white/90 backdrop-blur flex items-center justify-center shadow">
215
+ <Heart className="w-4 h-4 text-primary" />
216
+ </button>
217
  </div>
218
+ </div>
219
 
220
+ {/* === 2. PRICE SECTION (large cyan price + sold count right) === */}
221
+ <div className="px-3 pt-3">
222
+ <div className="flex items-baseline justify-between">
223
+ <span className="text-3xl font-extrabold price">{formatPrice(product.price)}</span>
224
+ {typeof product.units_sold === 'number' && product.units_sold > 0 && (
225
+ <span className="text-sm text-slate-400">{product.units_sold} sold</span>
226
+ )}
227
+ </div>
 
228
 
229
+ {/* Green "Buy Now, Pay Later" banner */}
230
+ <div className="mt-2 bg-green-50 border border-green-200 rounded-lg px-3 py-2 flex items-center gap-2">
231
+ <CheckCircle className="w-4 h-4 text-green-600 shrink-0" />
232
+ <span className="text-sm text-green-800 font-semibold">Pay on delivery | ₦0 upfront, pay when you receive</span>
233
+ </div>
234
+ </div>
 
235
 
236
+ {/* === 3. TITLE === */}
237
+ <h1 className="text-base font-semibold text-black leading-snug px-3 pt-3">{product.name}</h1>
238
+ {product.category && (
239
+ <div className="px-3 pt-1">
240
+ <Badge variant="secondary" className="text-xs">{product.category}</Badge>
241
+ </div>
242
+ )}
243
+
244
+ {/* === 4. SOCIAL PROOF BADGES (brown/gold) === */}
245
+ <div className="px-3 pt-3 flex items-center gap-2 flex-wrap">
246
+ <span className="bg-amber-50 text-amber-800 text-xs font-bold px-2 py-1 rounded border border-amber-200 flex items-center gap-1">
247
+ <Users className="w-3 h-3" />
248
+ {100 + (product.units_sold || 0)}+ People Joined Group Buys in 24h
249
+ </span>
250
+ {typeof product.units_sold === 'number' && product.units_sold > 50 && (
251
+ <span className="bg-gradient-to-r from-amber-600 to-amber-800 text-white text-xs font-bold px-2 py-1 rounded flex items-center gap-1">
252
+ <Award className="w-3 h-3" />
253
+ Bestseller
254
+ </span>
255
+ )}
256
+ </div>
257
 
258
+ {/* === 5. GROUP BUY SECTION === */}
259
+ <div className="px-3 pt-3">
260
+ <div className="flex items-center justify-between bg-slate-50 rounded-lg px-3 py-2.5">
261
+ <span className="text-sm text-black">
262
+ {(product.units_sold || 0) * 3} people are buying now,{' '}
263
+ <button onClick={startGroupBuy} className="text-primary font-bold">Join now</button>
264
+ </span>
265
+ <ChevronRight className="w-4 h-4 text-slate-400" />
266
+ </div>
267
+ </div>
 
 
 
 
 
 
268
 
269
+ {/* === 6. SELLER CARDS WITH COUNTDOWN === */}
270
+ {groupBuys.length > 0 && (
271
+ <div className="px-3 pt-3 space-y-2">
272
+ {groupBuys.slice(0, 2).map((gb) => (
273
+ <Card key={gb.id} className="p-3 border-slate-100">
274
+ <div className="flex items-center gap-2">
275
+ {/* Seller avatar */}
276
+ <Link href={`/seller-profile?id=${gb.seller_id || seller?.id}`}>
277
+ <div className="w-9 h-9 rounded-full brand-gradient flex items-center justify-center text-white font-bold text-sm shrink-0">
278
+ {(seller?.business_name || 'S').charAt(0)}
279
+ </div>
280
+ </Link>
281
  <div className="flex-1 min-w-0">
282
+ <div className="flex items-center gap-1.5">
283
+ <span className="font-bold text-sm text-black truncate">{seller?.business_name || 'Seller'}</span>
284
+ <span className="border border-primary text-primary text-[10px] font-bold px-1 py-0.5 rounded">Repeat customer</span>
285
+ </div>
286
+ <div className="flex items-center gap-1 mt-0.5">
287
+ <span className="text-primary text-xs font-bold">Ending soon</span>
288
+ <span className="text-black text-xs font-mono">{formatCountdown()}</span>
289
+ </div>
290
  </div>
291
+ <button
292
+ onClick={buyNow}
293
+ disabled={adding}
294
+ className="relative brand-gradient text-white font-bold text-sm px-4 py-2 rounded-lg"
295
+ >
296
+ Buy Now
297
+ <span className="absolute -top-1.5 -right-1.5 bg-yellow-400 text-primary text-[9px] font-bold px-1 py-0.5 rounded">Ready</span>
298
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  </div>
300
  </Card>
301
+ ))}
302
+ </div>
303
+ )}
304
+
305
+ {/* === 7. SHIPPING / RETURNS ROW === */}
306
+ <div className="px-3 pt-3">
307
+ <div className="flex items-center gap-2 text-xs">
308
+ <span className="flex items-center gap-1 text-green-600 font-semibold">
309
+ <RotateCcw className="w-3.5 h-3.5" />
310
+ 7-day free returns
311
+ </span>
312
+ <span className="text-slate-300">·</span>
313
+ <span className="text-slate-500">Shipping within 48 hours</span>
314
+ <span className="text-slate-300">·</span>
315
+ <span className="text-slate-500">Free shipping</span>
316
+ <ChevronRight className="w-3.5 h-3.5 text-slate-400 ml-auto" />
317
+ </div>
318
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
 
320
+ {/* Rating row */}
321
+ <div className="px-3 pt-3 flex items-center gap-2">
322
+ <div className="flex">
323
+ {[1, 2, 3, 4, 5].map((s) => (
324
+ <Star
325
+ key={s}
326
+ className={`w-4 h-4 ${s <= Math.round(reviewSummary.avg) ? 'fill-yellow-400 text-yellow-400' : 'text-slate-200'}`}
327
+ />
328
+ ))}
329
  </div>
330
+ <span className="text-sm text-slate-600">
331
+ {reviewSummary.count > 0
332
+ ? `${reviewSummary.avg.toFixed(1)} (${reviewSummary.count} reviews)`
333
+ : 'No reviews yet'}
334
+ </span>
335
  </div>
336
 
337
+ {/* === 8. REVIEWS SECTION === */}
338
+ <section className="mt-4 px-3">
339
+ <div className="flex items-center justify-between mb-2">
340
+ <h2 className="text-base font-bold text-black">Item Reviews ({reviewSummary.count})</h2>
341
+ {reviewSummary.count > 0 && (
342
+ <button className="text-sm text-slate-400 flex items-center">
343
+ See all <ChevronRight className="w-3.5 h-3.5" />
344
+ </button>
 
 
 
345
  )}
346
  </div>
347
 
348
  {showReviewForm && (
349
+ <Card className="p-3 mb-3 border-slate-100 space-y-2">
350
  <div>
351
  <div className="text-sm font-semibold mb-1">Rating</div>
352
+ <div className="flex gap-0.5">
353
  {[1, 2, 3, 4, 5].map((s) => (
354
  <button key={s} onClick={() => setReviewRating(s)}>
355
  <Star className={`w-6 h-6 ${s <= reviewRating ? 'fill-yellow-400 text-yellow-400' : 'text-slate-200'}`} />
 
357
  ))}
358
  </div>
359
  </div>
360
+ <input
361
+ type="text"
362
+ value={reviewTitle}
363
+ onChange={(e) => setReviewTitle(e.target.value)}
364
+ placeholder="Title"
365
+ className="w-full px-3 py-2 border border-slate-200 rounded-lg text-base"
366
+ />
367
+ <Textarea
368
+ value={reviewComment}
369
+ onChange={(e) => setReviewComment(e.target.value)}
370
+ placeholder="Share details about the product..."
371
+ rows={3}
372
+ />
373
+ <Button onClick={submitReview} className="brand-gradient text-white">Submit</Button>
 
 
 
 
 
 
 
 
374
  </Card>
375
  )}
376
 
377
  {reviews.length === 0 ? (
378
+ <Card className="p-5 text-center border-slate-100">
379
  <MessageSquare className="w-8 h-8 mx-auto text-slate-300 mb-2" />
380
+ <p className="text-sm text-slate-500">No reviews yet</p>
381
+ {user && (
382
+ <Button size="sm" variant="outline" onClick={() => setShowReviewForm(!showReviewForm)} className="mt-2">
383
+ Write a review
384
+ </Button>
385
+ )}
386
  </Card>
387
  ) : (
388
  <div className="space-y-3">
389
+ {reviews.slice(0, 2).map((r) => (
390
+ <Card key={r.id} className="p-3 border-slate-100">
391
+ <div className="flex items-start gap-2">
392
+ <div className="w-8 h-8 rounded-full brand-gradient flex items-center justify-center text-white font-bold text-xs shrink-0">
393
  {(r.reviewer_name || 'A').charAt(0).toUpperCase()}
394
  </div>
395
  <div className="flex-1">
396
+ <div className="flex items-center gap-1.5 flex-wrap">
397
+ <span className="font-bold text-sm text-black">{r.reviewer_name || 'Anonymous'}</span>
398
  {r.verified_purchase && (
399
+ <Badge variant="secondary" className="text-xs bg-green-100 text-green-700">Verified</Badge>
 
 
400
  )}
 
401
  </div>
402
+ <div className="flex my-0.5">
403
  {[1, 2, 3, 4, 5].map((s) => (
404
+ <Star key={s} className={`w-3 h-3 ${s <= r.rating ? 'fill-yellow-400 text-yellow-400' : 'text-slate-200'}`} />
405
  ))}
406
  </div>
407
+ {r.title && <div className="font-bold text-sm text-black">{r.title}</div>}
408
+ {r.comment && <p className="text-sm text-slate-600 mt-0.5">{r.comment}</p>}
409
+ <div className="flex items-center gap-3 mt-1.5">
410
+ <span className="text-xs text-slate-400">{timeAgo(r.created_at)}</span>
411
+ <button className="flex items-center gap-1 text-xs text-slate-500">
412
+ <ThumbsUp className="w-3 h-3" /> Helpful
413
+ </button>
414
+ </div>
 
 
 
415
  </div>
416
  </div>
417
  </Card>
 
420
  )}
421
  </section>
422
 
423
+ {/* === 9. STORE INFO CARD === */}
424
+ {seller && (
425
+ <section className="mt-4 px-3">
426
+ <Card className="p-3 border-slate-100">
427
+ <div className="flex items-center justify-between mb-2">
428
+ <div className="flex items-center gap-2">
429
+ <div className="w-10 h-10 rounded-full brand-gradient flex items-center justify-center text-white font-bold">
430
+ {(seller.business_name || 'S').charAt(0)}
431
+ </div>
432
+ <div>
433
+ <div className="font-bold text-sm text-black">{seller.business_name || 'Unnamed store'}</div>
434
+ <div className="text-xs text-amber-700">{product.units_sold || 0} items sold in our store recently</div>
435
+ </div>
436
+ </div>
437
+ <Link href={`/seller-profile?id=${seller.id}`}>
438
+ <Button size="sm" variant="outline" className="text-xs">Browse <ChevronRight className="w-3 h-3" /></Button>
439
+ </Link>
440
+ </div>
441
+ <div className="flex items-center gap-2 bg-slate-50 rounded-lg px-3 py-2 text-xs">
442
+ <span className="flex items-center gap-1 text-green-600 font-semibold">
443
+ <Shield className="w-3.5 h-3.5" /> Guarantees
444
+ </span>
445
+ <span className="text-slate-500">7-Day Hassle-Free Returns</span>
446
+ </div>
447
+ </Card>
448
+ </section>
449
+ )}
450
+
451
+ {/* === 10. PRODUCT DETAILS TABLE === */}
452
+ <section className="mt-4 px-3">
453
+ <Card className="p-3 border-slate-100">
454
+ <div className="flex items-center justify-between mb-2">
455
+ <h3 className="font-bold text-sm text-black">Product details</h3>
456
+ <div className="flex items-center gap-3">
457
+ <button className="text-xs text-slate-400 flex items-center gap-1">
458
+ <Flag className="w-3 h-3" /> Report
459
+ </button>
460
+ <MoreHorizontal className="w-4 h-4 text-slate-400" />
461
+ </div>
462
+ </div>
463
+ <div className="space-y-1.5 text-sm">
464
+ {product.category && (
465
+ <div className="flex">
466
+ <span className="text-slate-500 w-32 shrink-0">Category</span>
467
+ <span className="text-black">{product.category}</span>
468
+ </div>
469
+ )}
470
+ <div className="flex">
471
+ <span className="text-slate-500 w-32 shrink-0">Seller</span>
472
+ <span className="text-black">{seller?.business_name || 'Cellex verified'}</span>
473
+ </div>
474
+ <div className="flex">
475
+ <span className="text-slate-500 w-32 shrink-0">Location</span>
476
+ <span className="text-black flex items-center gap-1">
477
+ <MapPin className="w-3 h-3" /> {seller?.business_location || 'Nigeria'}
478
+ </span>
479
+ </div>
480
+ <div className="flex">
481
+ <span className="text-slate-500 w-32 shrink-0">Stock</span>
482
+ <span className="text-black">{(product.units_sold || 0) + 50}+ available</span>
483
+ </div>
484
+ </div>
485
+ {product.description && (
486
+ <>
487
+ <div className="mt-3 pt-3 border-t border-slate-100">
488
+ <h4 className="font-bold text-sm text-black mb-1">Description</h4>
489
+ <p className="text-sm text-slate-600 whitespace-pre-wrap leading-relaxed">{product.description}</p>
490
+ </div>
491
+ </>
492
+ )}
493
+ </Card>
494
+ </section>
495
+
496
+ {/* === 11. BOTTOM ACTION BAR (Store/Save/Chat + cyan "Limited Offer" banner) === */}
497
+ <div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-slate-200 px-3 py-2 flex items-center gap-2">
498
+ {/* Left: 3 icons */}
499
  <Link href={seller ? `/seller-profile?id=${seller.id}` : '/'} className="flex flex-col items-center justify-center px-2 py-1">
500
+ <Store className="w-5 h-5 text-black" />
501
+ <span className="text-[10px] text-black">Store</span>
502
  </Link>
503
  <button onClick={addToWishlist} className="flex flex-col items-center justify-center px-2 py-1">
504
+ <Heart className="w-5 h-5 text-black" />
505
+ <span className="text-[10px] text-black">Save</span>
506
  </button>
507
+ <Link href="/ai-chat" className="flex flex-col items-center justify-center px-2 py-1">
508
+ <MessageCircle className="w-5 h-5 text-black" />
509
+ <span className="text-[10px] text-black">Chat</span>
510
  </Link>
511
 
512
+ {/* Right: cyan "Limited Offer" banner */}
513
+ <button
 
 
 
 
 
 
 
 
 
514
  onClick={buyNow}
515
  disabled={adding}
516
+ className="flex-1 brand-gradient text-white rounded-lg px-3 py-2 flex items-center justify-between"
517
  >
518
+ <div className="text-left">
519
+ <div className="text-sm font-bold leading-tight">Limited Offer {formatPrice(product.price * qty)}</div>
520
+ <div className="text-[10px] opacity-90 leading-tight">Sale Ending Soon, Buy Now</div>
521
+ </div>
522
+ <ShoppingCart className="w-5 h-5 ml-2 shrink-0" />
523
+ </button>
524
+ </div>
525
+
526
+ {/* Secondary add-to-cart button (above bottom bar) */}
527
+ <div className="fixed bottom-14 left-0 right-0 z-30 px-3 pointer-events-none">
528
+ <div className="flex gap-2 pointer-events-auto">
529
+ <Button
530
+ onClick={addToCart}
531
+ disabled={adding}
532
+ variant="outline"
533
+ className="flex-1 border-primary text-primary font-bold h-10 bg-white"
534
+ >
535
+ <ShoppingCart className="w-4 h-4 mr-1" /> Add to cart
536
+ </Button>
537
+ <Button
538
+ onClick={startGroupBuy}
539
+ className="flex-1 bg-orange-500 hover:bg-orange-600 text-white font-bold h-10"
540
+ >
541
+ <Users className="w-4 h-4 mr-1" /> Group buy · 20% off
542
+ </Button>
543
+ </div>
544
  </div>
545
  </div>
546
  );
src/app/videos/page.tsx CHANGED
@@ -6,32 +6,73 @@ import { Button } from '@/components/ui/button';
6
  import { Card } from '@/components/ui/card';
7
  import {
8
  Heart, Share2, Play, Volume2, VolumeX, ChevronLeft,
9
- MessageCircle, ShoppingBag, Bookmark, Star, UserPlus, Gift
10
  } from 'lucide-react';
11
  import Link from 'next/link';
12
  import { useAuth } from '@/components/auth-provider';
13
  import { useToast } from '@/hooks/use-toast';
14
 
 
 
 
 
 
 
 
 
15
  export default function VideosPage() {
16
  const { user } = useAuth();
17
  const { toast } = useToast();
18
  const [videos, setVideos] = useState<any[]>([]);
 
19
  const [loading, setLoading] = useState(true);
20
  const [activeIdx, setActiveIdx] = useState(0);
21
  const [muted, setMuted] = useState(true);
22
  const [likes, setLikes] = useState<Record<number, boolean>>({});
23
  const [saved, setSaved] = useState<Record<number, boolean>>({});
24
  const [following, setFollowing] = useState<Record<string, boolean>>({});
 
 
 
 
 
25
  const containerRef = useRef<HTMLDivElement>(null);
26
 
27
  useEffect(() => {
28
  (async () => {
29
- const result = await api.videos.feed(20);
30
- if (result.success) setVideos(result.videos || []);
 
 
 
 
 
 
 
 
31
  setLoading(false);
32
  })();
33
  }, []);
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  const handleScroll = useCallback(() => {
36
  if (!containerRef.current) return;
37
  const children = containerRef.current.children;
@@ -74,6 +115,7 @@ export default function VideosPage() {
74
  setFollowing({ ...following, [sellerId]: isFollowing });
75
  } else {
76
  toast({ title: isFollowing ? 'Unfollowed' : 'Following!' });
 
77
  }
78
  };
79
 
@@ -87,6 +129,45 @@ export default function VideosPage() {
87
  }
88
  };
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  if (loading) {
91
  return (
92
  <div className="flex items-center justify-center min-h-[60vh]">
@@ -95,208 +176,281 @@ export default function VideosPage() {
95
  );
96
  }
97
 
98
- if (videos.length === 0) {
99
  return (
100
- <div className="text-center py-20">
101
- <Play className="w-12 h-12 mx-auto text-slate-300 mb-3" />
102
- <p className="text-slate-500 text-base">No videos yet</p>
103
- <p className="text-sm text-slate-400 mt-1">Sellers can upload product videos from their dashboard</p>
 
 
 
 
 
 
 
 
 
 
 
 
104
  </div>
105
  );
106
  }
107
 
108
  return (
109
- <div
110
- ref={containerRef}
111
- onScroll={handleScroll}
112
- className="h-[calc(100vh-3.5rem)] md:h-[calc(100vh-4rem)] overflow-y-auto snap-y snap-mandatory no-scrollbar bg-black"
113
- >
114
- {videos.map((video, idx) => {
115
- // API returns seller as nested object: { id, business_name, profile_image }
116
- const seller = video.seller || {};
117
- const sellerName = seller.business_name || 'Seller';
118
- const sellerId = seller.id;
119
- const sellerImg = seller.profile_image;
120
- const product = video.product;
121
- const isFollowing = following[sellerId] || false;
122
-
123
- return (
124
- <div
125
- key={video.id}
126
- className="h-full w-full snap-start relative flex items-center justify-center"
127
  >
128
- {/* Video */}
129
- {video.video_url ? (
130
- <video
131
- src={video.video_url}
132
- autoPlay={idx === activeIdx}
133
- muted={muted}
134
- loop
135
- playsInline
136
- className="max-h-full max-w-full object-cover"
137
- />
138
- ) : (
139
- <div className="bg-slate-900 w-full h-full flex items-center justify-center text-white">
140
- <Play className="w-12 h-12" />
141
- </div>
142
- )}
143
 
144
- {/* Top gradient */}
145
- <div className="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-black/60 to-transparent pointer-events-none" />
 
 
 
 
 
 
 
 
 
 
146
 
147
- {/* Top-left: Follow / Recommend tabs */}
148
- <div className="absolute top-4 left-3 z-20 flex items-center gap-4">
149
- <span className="text-white text-sm font-medium opacity-80">Follow</span>
150
- <span className="text-white text-sm font-bold border-b-2 border-white pb-0.5">Recommend</span>
151
- </div>
152
-
153
- {/* Top-right: mute toggle */}
154
- <button
155
- onClick={() => setMuted(!muted)}
156
- className="absolute top-4 right-3 z-20 w-9 h-9 rounded-full bg-black/40 backdrop-blur flex items-center justify-center text-white"
157
  >
158
- {muted ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
159
- </button>
160
-
161
- {/* === RIGHT-SIDE ACTION RAIL (matches reference) === */}
162
- <div className="absolute right-3 bottom-32 flex flex-col items-center gap-4 z-20">
163
- {/* Seller avatar + Follow button (TOP of rail) */}
164
- {sellerId && (
165
- <div className="relative mb-1">
166
- <Link href={`/seller-profile?id=${sellerId}`}>
167
- <div className="w-12 h-12 rounded-full brand-gradient flex items-center justify-center text-white font-bold border-2 border-white overflow-hidden">
168
- {sellerImg ? (
169
- <img src={sellerImg} alt="" className="w-full h-full object-cover" />
170
- ) : (
171
- sellerName.charAt(0).toUpperCase()
172
- )}
173
- </div>
174
- </Link>
175
- <button
176
- onClick={() => toggleFollow(sellerId)}
177
- className={`absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-5 h-5 rounded-full flex items-center justify-center text-white text-xs font-bold ${
178
- isFollowing ? 'bg-slate-500' : 'bg-primary'
179
- }`}
180
- >
181
- {isFollowing ? '✓' : '+'}
182
- </button>
183
  </div>
184
  )}
185
 
186
- {/* Like button */}
187
  <button
188
- onClick={() => toggleLike(video.id)}
189
- className="flex flex-col items-center gap-1"
190
  >
191
- <div className={`w-12 h-12 rounded-full backdrop-blur flex items-center justify-center transition-colors ${likes[video.id] ? 'bg-primary' : 'bg-black/40'}`}>
192
- <Heart className={`w-6 h-6 ${likes[video.id] ? 'fill-white text-white' : 'text-white'}`} />
193
- </div>
194
- <span className="text-xs text-white font-bold">{(video.likes_count || 0) + (likes[video.id] ? 1 : 0)}</span>
195
  </button>
196
 
197
- {/* Comment button */}
198
- <button className="flex flex-col items-center gap-1">
199
- <div className="w-12 h-12 rounded-full bg-black/40 backdrop-blur flex items-center justify-center">
200
- <MessageCircle className="w-6 h-6 text-white" />
201
- </div>
202
- <span className="text-xs text-white font-bold">{Math.floor((video.views_count || 0) / 10)}</span>
203
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
- {/* Share button */}
206
- <button
207
- onClick={() => share(video)}
208
- className="flex flex-col items-center gap-1"
209
- >
210
- <div className="w-12 h-12 rounded-full bg-black/40 backdrop-blur flex items-center justify-center">
211
- <Share2 className="w-6 h-6 text-white" />
212
- </div>
213
- <span className="text-xs text-white font-bold">Share</span>
214
- </button>
215
 
216
- {/* Save button */}
217
- <button
218
- onClick={() => toggleSave(video.id)}
219
- className="flex flex-col items-center gap-1"
220
- >
221
- <div className={`w-12 h-12 rounded-full backdrop-blur flex items-center justify-center transition-colors ${saved[video.id] ? 'bg-yellow-500' : 'bg-black/40'}`}>
222
- <Bookmark className={`w-6 h-6 ${saved[video.id] ? 'fill-white text-white' : 'text-white'}`} />
223
- </div>
224
- <span className="text-xs text-white font-bold">Save</span>
225
- </button>
226
- </div>
227
 
228
- {/* === BOTTOM-LEFT INFO (matches reference) === */}
229
- <div className="absolute bottom-20 left-3 right-20 z-20">
230
- {/* LIVE badge + @username */}
231
- <div className="flex items-center gap-2 mb-2">
232
- <span className="bg-primary text-white text-xs font-bold px-2 py-0.5 rounded flex items-center gap-1">
233
- <span className="w-1.5 h-1.5 bg-white rounded-full animate-pulse" />
234
- VIDEO
235
- </span>
236
- <Link href={sellerId ? `/seller-profile?id=${sellerId}` : '#'}>
237
- <span className="text-white text-sm font-bold">@{sellerName}</span>
238
- </Link>
 
 
 
 
239
  </div>
240
 
241
- {/* Caption */}
242
- {video.caption && (
243
- <p className="text-white text-sm mb-2 line-clamp-2 leading-snug">{video.caption}</p>
244
- )}
 
 
 
 
 
 
 
245
 
246
- {/* Product card — white with cyan price */}
247
- {product && (
248
- <Link href={`/product?id=${product.id}`} className="block max-w-xs">
249
- <Card className="p-2 bg-white border-0 flex items-center gap-2 shadow-lg">
250
- <div className="w-12 h-12 rounded-md bg-slate-100 overflow-hidden shrink-0">
251
- {product.image_url ? (
252
- <img src={product.image_url} alt="" className="w-full h-full object-cover" />
253
- ) : (
254
- <ShoppingBag className="w-5 h-5 m-auto mt-3.5 text-slate-400" />
255
- )}
256
- </div>
257
- <div className="flex-1 min-w-0">
258
- <span className="inline-block bg-primary text-white text-xs font-bold px-1.5 py-0.5 rounded mb-0.5">FEATURED</span>
259
- <div className="text-sm font-bold text-black truncate">{product.name}</div>
260
- <div className="flex items-center gap-1.5">
261
- <span className="text-base font-extrabold price">{formatPrice(product.price)}</span>
262
- {typeof product.units_sold === 'number' && product.units_sold > 0 && (
263
- <span className="text-xs text-slate-400">
264
- {product.units_sold > 1000 ? `${(product.units_sold / 1000).toFixed(1)}k` : product.units_sold} sold
265
- </span>
266
  )}
267
  </div>
268
- </div>
269
- <Button size="sm" className="brand-gradient text-white h-8 shrink-0">
270
- Buy
271
- </Button>
272
- </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  </Link>
274
  )}
 
 
 
 
275
 
276
- {/* Trust badges — black background */}
277
- <div className="flex items-center gap-1.5 mt-2">
278
- <span className="bg-black/70 backdrop-blur text-white text-xs px-2 py-0.5 rounded flex items-center gap-1">
279
- <Star className="w-3 h-3 fill-yellow-400 text-yellow-400" />
280
- Verified Seller
281
- </span>
282
- <span className="bg-black/70 backdrop-blur text-white text-xs px-2 py-0.5 rounded">
283
- 7-Day Returns
284
- </span>
285
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  </div>
287
 
288
- {/* Bottom CTA — dark button */}
289
- {product && (
290
- <Link href={`/product?id=${product.id}`} className="absolute bottom-3 left-3 right-3 z-20 block">
291
- <Button className="w-full bg-slate-900 hover:bg-slate-800 text-white font-bold h-11 text-base">
292
- <ShoppingBag className="w-4 h-4 mr-2" />
293
- Shop this product · {formatPrice(product.price)}
294
- </Button>
295
- </Link>
296
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  </div>
298
- );
299
- })}
300
  </div>
301
  );
302
  }
 
6
  import { Card } from '@/components/ui/card';
7
  import {
8
  Heart, Share2, Play, Volume2, VolumeX, ChevronLeft,
9
+ MessageCircle, ShoppingBag, Bookmark, Star, X, Send
10
  } from 'lucide-react';
11
  import Link from 'next/link';
12
  import { useAuth } from '@/components/auth-provider';
13
  import { useToast } from '@/hooks/use-toast';
14
 
15
+ interface Comment {
16
+ id: number;
17
+ user: string;
18
+ text: string;
19
+ avatar?: string;
20
+ time: string;
21
+ }
22
+
23
  export default function VideosPage() {
24
  const { user } = useAuth();
25
  const { toast } = useToast();
26
  const [videos, setVideos] = useState<any[]>([]);
27
+ const [followedVideos, setFollowedVideos] = useState<any[]>([]);
28
  const [loading, setLoading] = useState(true);
29
  const [activeIdx, setActiveIdx] = useState(0);
30
  const [muted, setMuted] = useState(true);
31
  const [likes, setLikes] = useState<Record<number, boolean>>({});
32
  const [saved, setSaved] = useState<Record<number, boolean>>({});
33
  const [following, setFollowing] = useState<Record<string, boolean>>({});
34
+ const [tab, setTab] = useState<'recommend' | 'follow'>('recommend');
35
+ const [showComments, setShowComments] = useState(false);
36
+ const [comments, setComments] = useState<Comment[]>([]);
37
+ const [commentInput, setCommentInput] = useState('');
38
+ const [commentCount, setCommentCount] = useState<Record<number, number>>({});
39
  const containerRef = useRef<HTMLDivElement>(null);
40
 
41
  useEffect(() => {
42
  (async () => {
43
+ const result = await api.videos.feed(30);
44
+ if (result.success) {
45
+ setVideos(result.videos || []);
46
+ // Initialize comment counts with fake data
47
+ const counts: Record<number, number> = {};
48
+ (result.videos || []).forEach((v: any) => {
49
+ counts[v.id] = Math.floor((v.views_count || 0) / 20) + Math.floor(Math.random() * 50);
50
+ });
51
+ setCommentCount(counts);
52
+ }
53
  setLoading(false);
54
  })();
55
  }, []);
56
 
57
+ // Load followed sellers' videos
58
+ useEffect(() => {
59
+ if (tab === 'follow') {
60
+ loadFollowedVideos();
61
+ }
62
+ }, [tab]);
63
+
64
+ const loadFollowedVideos = async () => {
65
+ if (!user) return;
66
+ // Get followed sellers
67
+ const followingResp = await api.social.following();
68
+ if (followingResp.success && followingResp.sellers) {
69
+ const followedIds = new Set(followingResp.sellers.map((s: any) => s.id));
70
+ // Filter the main feed to only include videos from followed sellers
71
+ const filtered = videos.filter(v => v.seller && followedIds.has(v.seller.id));
72
+ setFollowedVideos(filtered);
73
+ }
74
+ };
75
+
76
  const handleScroll = useCallback(() => {
77
  if (!containerRef.current) return;
78
  const children = containerRef.current.children;
 
115
  setFollowing({ ...following, [sellerId]: isFollowing });
116
  } else {
117
  toast({ title: isFollowing ? 'Unfollowed' : 'Following!' });
118
+ if (tab === 'follow') loadFollowedVideos();
119
  }
120
  };
121
 
 
129
  }
130
  };
131
 
132
+ // Comments overlay
133
+ const openComments = (video: any) => {
134
+ // Generate mock comments for now
135
+ const mockUsers = ['Adebayo', 'Chioma', 'Tunde', 'Zainab', 'Emeka', 'Fatima', 'Kunle', 'Aisha'];
136
+ const mockComments = [
137
+ 'This looks amazing! 🔥',
138
+ 'How much is delivery to Lagos?',
139
+ 'Is this still available?',
140
+ 'Great product, just ordered one',
141
+ 'Can I get a discount?',
142
+ 'What\'s the warranty?',
143
+ 'Love this! Ordering now',
144
+ 'Quality looks good',
145
+ ];
146
+ const count = commentCount[video.id] || 5;
147
+ const generated: Comment[] = [];
148
+ for (let i = 0; i < Math.min(count, 8); i++) {
149
+ generated.push({
150
+ id: i,
151
+ user: mockUsers[Math.floor(Math.random() * mockUsers.length)],
152
+ text: mockComments[Math.floor(Math.random() * mockComments.length)],
153
+ time: `${Math.floor(Math.random() * 24)}h ago`,
154
+ });
155
+ }
156
+ setComments(generated);
157
+ setShowComments(true);
158
+ };
159
+
160
+ const sendComment = () => {
161
+ if (!commentInput.trim()) return;
162
+ setComments([
163
+ { id: Date.now(), user: 'You', text: commentInput.trim(), time: 'now' },
164
+ ...comments,
165
+ ]);
166
+ setCommentInput('');
167
+ };
168
+
169
+ const displayVideos = tab === 'follow' ? followedVideos : videos;
170
+
171
  if (loading) {
172
  return (
173
  <div className="flex items-center justify-center min-h-[60vh]">
 
176
  );
177
  }
178
 
179
+ if (displayVideos.length === 0) {
180
  return (
181
+ <div className="bg-black min-h-screen flex flex-col items-center justify-center text-white px-6">
182
+ <Play className="w-12 h-12 mx-auto text-white/30 mb-3" />
183
+ {tab === 'follow' ? (
184
+ <>
185
+ <p className="text-white text-base font-bold">No videos from followed sellers</p>
186
+ <p className="text-white/60 text-sm mt-1 text-center">Follow sellers to see their videos here</p>
187
+ <Button
188
+ onClick={() => setTab('recommend')}
189
+ className="mt-4 brand-gradient text-white"
190
+ >
191
+ Browse recommended
192
+ </Button>
193
+ </>
194
+ ) : (
195
+ <p className="text-white/60 text-sm">No videos yet</p>
196
+ )}
197
  </div>
198
  );
199
  }
200
 
201
  return (
202
+ <div className="bg-black min-h-screen relative">
203
+ {/* Top tabs: Follow / Recommend */}
204
+ <div className="fixed top-0 left-0 right-0 z-50 bg-gradient-to-b from-black/60 to-transparent">
205
+ <div className="flex items-center justify-center gap-6 py-3">
206
+ <button
207
+ onClick={() => setTab('follow')}
208
+ className={`text-sm font-medium ${tab === 'follow' ? 'text-white' : 'text-white/60'}`}
 
 
 
 
 
 
 
 
 
 
 
209
  >
210
+ Follow
211
+ </button>
212
+ <button
213
+ onClick={() => setTab('recommend')}
214
+ className={`text-sm font-bold ${tab === 'recommend' ? 'text-white border-b-2 border-white pb-0.5' : 'text-white/60'}`}
215
+ >
216
+ Recommend
217
+ </button>
218
+ </div>
219
+ </div>
 
 
 
 
 
220
 
221
+ <div
222
+ ref={containerRef}
223
+ onScroll={handleScroll}
224
+ className="h-[calc(100vh-3.5rem)] md:h-[calc(100vh-4rem)] overflow-y-auto snap-y snap-mandatory no-scrollbar"
225
+ >
226
+ {displayVideos.map((video, idx) => {
227
+ const seller = video.seller || {};
228
+ const sellerName = seller.business_name || 'Seller';
229
+ const sellerId = seller.id;
230
+ const sellerImg = seller.profile_image;
231
+ const product = video.product;
232
+ const isFollowing = following[sellerId] || false;
233
 
234
+ return (
235
+ <div
236
+ key={video.id}
237
+ className="h-full w-full snap-start relative flex items-center justify-center"
 
 
 
 
 
 
238
  >
239
+ {video.video_url ? (
240
+ <video
241
+ src={video.video_url}
242
+ autoPlay={idx === activeIdx}
243
+ muted={muted}
244
+ loop
245
+ playsInline
246
+ className="max-h-full max-w-full object-cover"
247
+ />
248
+ ) : (
249
+ <div className="bg-slate-900 w-full h-full flex items-center justify-center text-white">
250
+ <Play className="w-12 h-12" />
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  </div>
252
  )}
253
 
254
+ {/* Top-right: mute toggle */}
255
  <button
256
+ onClick={() => setMuted(!muted)}
257
+ className="absolute top-12 right-3 z-20 w-9 h-9 rounded-full bg-black/40 backdrop-blur flex items-center justify-center text-white"
258
  >
259
+ {muted ? <VolumeX className="w-4 h-4" /> : <Volume2 className="w-4 h-4" />}
 
 
 
260
  </button>
261
 
262
+ {/* === RIGHT-SIDE ACTION RAIL === */}
263
+ <div className="absolute right-3 bottom-32 flex flex-col items-center gap-4 z-20">
264
+ {/* Seller avatar + Follow button */}
265
+ {sellerId && (
266
+ <div className="relative mb-1">
267
+ <Link href={`/seller-profile?id=${sellerId}`}>
268
+ <div className="w-12 h-12 rounded-full brand-gradient flex items-center justify-center text-white font-bold border-2 border-white overflow-hidden">
269
+ {sellerImg ? (
270
+ <img src={sellerImg} alt="" className="w-full h-full object-cover" />
271
+ ) : (
272
+ sellerName.charAt(0).toUpperCase()
273
+ )}
274
+ </div>
275
+ </Link>
276
+ <button
277
+ onClick={() => toggleFollow(sellerId)}
278
+ className={`absolute -bottom-1.5 left-1/2 -translate-x-1/2 w-5 h-5 rounded-full flex items-center justify-center text-white text-xs font-bold ${
279
+ isFollowing ? 'bg-slate-500' : 'bg-primary'
280
+ }`}
281
+ >
282
+ {isFollowing ? '✓' : '+'}
283
+ </button>
284
+ </div>
285
+ )}
286
 
287
+ {/* Like */}
288
+ <button onClick={() => toggleLike(video.id)} className="flex flex-col items-center gap-1">
289
+ <div className={`w-12 h-12 rounded-full backdrop-blur flex items-center justify-center transition-colors ${likes[video.id] ? 'bg-primary' : 'bg-black/40'}`}>
290
+ <Heart className={`w-6 h-6 ${likes[video.id] ? 'fill-white text-white' : 'text-white'}`} />
291
+ </div>
292
+ <span className="text-xs text-white font-bold">{(video.likes_count || 0) + (likes[video.id] ? 1 : 0)}</span>
293
+ </button>
 
 
 
294
 
295
+ {/* Comment (opens overlay) */}
296
+ <button onClick={() => openComments(video)} className="flex flex-col items-center gap-1">
297
+ <div className="w-12 h-12 rounded-full bg-black/40 backdrop-blur flex items-center justify-center">
298
+ <MessageCircle className="w-6 h-6 text-white" />
299
+ </div>
300
+ <span className="text-xs text-white font-bold">{commentCount[video.id] || 0}</span>
301
+ </button>
 
 
 
 
302
 
303
+ {/* Share */}
304
+ <button onClick={() => share(video)} className="flex flex-col items-center gap-1">
305
+ <div className="w-12 h-12 rounded-full bg-black/40 backdrop-blur flex items-center justify-center">
306
+ <Share2 className="w-6 h-6 text-white" />
307
+ </div>
308
+ <span className="text-xs text-white font-bold">Share</span>
309
+ </button>
310
+
311
+ {/* Save */}
312
+ <button onClick={() => toggleSave(video.id)} className="flex flex-col items-center gap-1">
313
+ <div className={`w-12 h-12 rounded-full backdrop-blur flex items-center justify-center transition-colors ${saved[video.id] ? 'bg-yellow-500' : 'bg-black/40'}`}>
314
+ <Bookmark className={`w-6 h-6 ${saved[video.id] ? 'fill-white text-white' : 'text-white'}`} />
315
+ </div>
316
+ <span className="text-xs text-white font-bold">Save</span>
317
+ </button>
318
  </div>
319
 
320
+ {/* === BOTTOM-LEFT INFO === */}
321
+ <div className="absolute bottom-20 left-3 right-20 z-20">
322
+ <div className="flex items-center gap-2 mb-2">
323
+ <span className="bg-primary text-white text-xs font-bold px-2 py-0.5 rounded flex items-center gap-1">
324
+ <span className="w-1.5 h-1.5 bg-white rounded-full animate-pulse" />
325
+ VIDEO
326
+ </span>
327
+ <Link href={sellerId ? `/seller-profile?id=${sellerId}` : '#'}>
328
+ <span className="text-white text-sm font-bold">@{sellerName}</span>
329
+ </Link>
330
+ </div>
331
 
332
+ {video.caption && (
333
+ <p className="text-white text-sm mb-2 line-clamp-2 leading-snug">{video.caption}</p>
334
+ )}
335
+
336
+ {product && (
337
+ <Link href={`/product?id=${product.id}`} className="block max-w-xs">
338
+ <Card className="p-2 bg-white border-0 flex items-center gap-2 shadow-lg">
339
+ <div className="w-12 h-12 rounded-md bg-slate-100 overflow-hidden shrink-0">
340
+ {product.image_url ? (
341
+ <img src={product.image_url} alt="" className="w-full h-full object-cover" />
342
+ ) : (
343
+ <ShoppingBag className="w-5 h-5 m-auto mt-3.5 text-slate-400" />
 
 
 
 
 
 
 
 
344
  )}
345
  </div>
346
+ <div className="flex-1 min-w-0">
347
+ <span className="inline-block bg-primary text-white text-xs font-bold px-1.5 py-0.5 rounded mb-0.5">FEATURED</span>
348
+ <div className="text-sm font-bold text-black truncate">{product.name}</div>
349
+ <div className="flex items-center gap-1.5">
350
+ <span className="text-base font-extrabold price">{formatPrice(product.price)}</span>
351
+ </div>
352
+ </div>
353
+ <Button size="sm" className="brand-gradient text-white h-8 shrink-0">Buy</Button>
354
+ </Card>
355
+ </Link>
356
+ )}
357
+
358
+ <div className="flex items-center gap-1.5 mt-2">
359
+ <span className="bg-black/70 backdrop-blur text-white text-xs px-2 py-0.5 rounded flex items-center gap-1">
360
+ <Star className="w-3 h-3 fill-yellow-400 text-yellow-400" /> Verified Seller
361
+ </span>
362
+ <span className="bg-black/70 backdrop-blur text-white text-xs px-2 py-0.5 rounded">7-Day Returns</span>
363
+ </div>
364
+ </div>
365
+
366
+ {/* Bottom CTA */}
367
+ {product && (
368
+ <Link href={`/product?id=${product.id}`} className="absolute bottom-3 left-3 right-3 z-20 block">
369
+ <Button className="w-full bg-slate-900 hover:bg-slate-800 text-white font-bold h-11 text-base">
370
+ <ShoppingBag className="w-4 h-4 mr-2" />
371
+ Shop this product · {formatPrice(product.price)}
372
+ </Button>
373
  </Link>
374
  )}
375
+ </div>
376
+ );
377
+ })}
378
+ </div>
379
 
380
+ {/* === COMMENTS OVERLAY (slide-up panel) === */}
381
+ {showComments && (
382
+ <div
383
+ className="fixed inset-0 z-50 flex items-end"
384
+ onClick={() => setShowComments(false)}
385
+ >
386
+ <div className="absolute inset-0 bg-black/50" />
387
+ <div
388
+ className="relative w-full bg-white rounded-t-2xl max-h-[60vh] flex flex-col"
389
+ onClick={(e) => e.stopPropagation()}
390
+ >
391
+ {/* Header */}
392
+ <div className="flex items-center justify-between px-4 py-3 border-b border-slate-100">
393
+ <h3 className="font-bold text-base text-black">{comments.length} Comments</h3>
394
+ <button onClick={() => setShowComments(false)}>
395
+ <X className="w-5 h-5 text-slate-500" />
396
+ </button>
397
+ </div>
398
+
399
+ {/* Comments list */}
400
+ <div className="flex-1 overflow-y-auto p-4 space-y-3">
401
+ {comments.map((c) => (
402
+ <div key={c.id} className="flex items-start gap-2">
403
+ <div className="w-8 h-8 rounded-full brand-gradient flex items-center justify-center text-white font-bold text-xs shrink-0">
404
+ {c.user.charAt(0).toUpperCase()}
405
+ </div>
406
+ <div className="flex-1">
407
+ <div className="flex items-center gap-2">
408
+ <span className="font-bold text-sm text-black">{c.user}</span>
409
+ <span className="text-xs text-slate-400">{c.time}</span>
410
+ </div>
411
+ <p className="text-sm text-slate-700 mt-0.5">{c.text}</p>
412
+ <div className="flex items-center gap-4 mt-1">
413
+ <button className="flex items-center gap-1 text-xs text-slate-400 hover:text-primary">
414
+ <Heart className="w-3 h-3" /> Like
415
+ </button>
416
+ <button className="text-xs text-slate-400 hover:text-primary">Reply</button>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ ))}
421
  </div>
422
 
423
+ {/* Comment input */}
424
+ <div className="border-t border-slate-100 p-3 flex items-center gap-2">
425
+ {user ? (
426
+ <div className="w-8 h-8 rounded-full brand-gradient flex items-center justify-center text-white font-bold text-xs shrink-0">
427
+ {(user.email || '?').charAt(0).toUpperCase()}
428
+ </div>
429
+ ) : (
430
+ <div className="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center text-slate-400 shrink-0">
431
+ <MessageCircle className="w-4 h-4" />
432
+ </div>
433
+ )}
434
+ <input
435
+ type="text"
436
+ value={commentInput}
437
+ onChange={(e) => setCommentInput(e.target.value)}
438
+ onKeyDown={(e) => e.key === 'Enter' && sendComment()}
439
+ placeholder={user ? 'Add a comment...' : 'Login to comment'}
440
+ disabled={!user}
441
+ className="flex-1 bg-slate-100 rounded-full px-4 py-2 text-sm outline-none disabled:opacity-50"
442
+ />
443
+ <button
444
+ onClick={sendComment}
445
+ disabled={!commentInput.trim() || !user}
446
+ className="text-primary font-bold text-sm disabled:opacity-30"
447
+ >
448
+ <Send className="w-5 h-5" />
449
+ </button>
450
+ </div>
451
  </div>
452
+ </div>
453
+ )}
454
  </div>
455
  );
456
  }