Spaces:
Sleeping
Sleeping
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.
- src/app/product/page.tsx +326 -277
- 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="
|
| 158 |
-
{/*
|
| 159 |
-
<div className="
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 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 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
<
|
| 237 |
-
|
| 238 |
-
{product.
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
</div>
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
</div>
|
| 250 |
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 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 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
<div className="flex-1 min-w-0">
|
| 284 |
-
<div className="
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
</div>
|
| 287 |
-
<
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 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 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 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 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
|
|
|
|
|
|
| 361 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
</div>
|
| 363 |
|
| 364 |
-
{/*
|
| 365 |
-
<section className="mt-
|
| 366 |
-
<div className="flex items-center justify-between mb-
|
| 367 |
-
<h2 className="text-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 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-
|
| 380 |
<div>
|
| 381 |
<div className="text-sm font-semibold mb-1">Rating</div>
|
| 382 |
-
<div className="flex gap-
|
| 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 |
-
<
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 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-
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
</Card>
|
| 420 |
) : (
|
| 421 |
<div className="space-y-3">
|
| 422 |
-
{reviews.map((r) => (
|
| 423 |
-
<Card key={r.id} className="p-
|
| 424 |
-
<div className="flex items-start gap-
|
| 425 |
-
<div className="w-
|
| 426 |
{(r.reviewer_name || 'A').charAt(0).toUpperCase()}
|
| 427 |
</div>
|
| 428 |
<div className="flex-1">
|
| 429 |
-
<div className="flex items-center gap-
|
| 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-
|
| 439 |
{[1, 2, 3, 4, 5].map((s) => (
|
| 440 |
-
<Star key={s} className={`w-3
|
| 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-
|
| 445 |
-
<
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 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 |
-
{/*
|
| 463 |
-
|
| 464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 467 |
-
<span className="text-
|
| 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-
|
| 471 |
-
<span className="text-
|
| 472 |
</button>
|
| 473 |
-
<Link href="/
|
| 474 |
-
<MessageCircle className="w-5 h-5 text-
|
| 475 |
-
<span className="text-
|
| 476 |
</Link>
|
| 477 |
|
| 478 |
-
{/* Right:
|
| 479 |
-
<
|
| 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
|
| 492 |
>
|
| 493 |
-
|
| 494 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 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(
|
| 30 |
-
if (result.success)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 99 |
return (
|
| 100 |
-
<div className="
|
| 101 |
-
<Play className="w-12 h-12 mx-auto text-
|
| 102 |
-
|
| 103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
</div>
|
| 105 |
);
|
| 106 |
}
|
| 107 |
|
| 108 |
return (
|
| 109 |
-
<div
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 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 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 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 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
-
|
| 148 |
-
<div
|
| 149 |
-
|
| 150 |
-
|
| 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 |
-
{
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 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 |
-
{/*
|
| 187 |
<button
|
| 188 |
-
onClick={() =>
|
| 189 |
-
className="
|
| 190 |
>
|
| 191 |
-
<
|
| 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 |
-
{/*
|
| 198 |
-
<
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
</div>
|
| 213 |
-
<span className="text-xs text-white font-bold">Share</span>
|
| 214 |
-
</button>
|
| 215 |
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
</div>
|
| 224 |
-
<span className="text-xs text-white font-bold">Save</span>
|
| 225 |
-
</button>
|
| 226 |
-
</div>
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
<span className="
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
</div>
|
| 240 |
|
| 241 |
-
{/*
|
| 242 |
-
|
| 243 |
-
<
|
| 244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 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 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
</Link>
|
| 274 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 286 |
</div>
|
| 287 |
|
| 288 |
-
{/*
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
<
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 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 |
}
|