'use client'; import { useEffect, useState, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { api, formatPrice, type Product } from '@/lib/api'; import { motion } from 'framer-motion'; import { Search, Heart, MessageCircle, Send, Bookmark, Store, ChevronRight, Play, CheckCircle, Bell, User, Sparkles, Home as HomeIcon, Users } from 'lucide-react'; import Link from 'next/link'; import { useAuth } from '@/components/auth-provider'; import { useToast } from '@/hooks/use-toast'; import { useOptimisticUI } from '@/components/optimistic-ui'; import { PageSkeleton } from '@/components/page-skeleton'; interface FeedPost { type: 'video' | 'product'; id: string; videoId?: number; productId?: number; sellerId?: string; sellerSlug?: string; sellerName: string; sellerImage?: string; mediaUrl: string; caption: string; likes: number; views?: number; comments: number; product?: Product; soldCount?: number; createdAt?: string; isLive?: boolean; verified?: boolean; liked?: boolean; } export default function HomePage() { const router = useRouter(); const { user, isSeller, unreadMessages } = useAuth(); const { toast } = useToast(); const { burst } = useOptimisticUI(); const [feed, setFeed] = useState([]); const [stories, setStories] = useState([]); const [sellers, setSellers] = useState([]); const [liveSessions, setLiveSessions] = useState([]); const [shorts, setShorts] = useState([]); const [loading, setLoading] = useState(true); const [likedPosts, setLikedPosts] = useState>(new Set()); const viewedPosts = useRef>(new Set()); const [savedPosts, setSavedPosts] = useState>(new Set()); const [following, setFollowing] = useState>(new Set()); const searchBarRef = useRef(null); // IntersectionObserver for hiding GlobalSpotlight FAB when top search is visible useEffect(() => { const el = searchBarRef.current; if (!el) return; const observer = new IntersectionObserver( ([entry]) => { window.dispatchEvent( new CustomEvent('searchbar-visibility', { detail: { visible: entry.isIntersecting }, }) ); }, { threshold: 0 } ); observer.observe(el); return () => observer.disconnect(); }, []); // Track views — when a post enters the viewport, send a view feedback to Gorse const trackView = (postId: string, videoId?: number, productId?: number) => { if (viewedPosts.current.has(postId)) return; viewedPosts.current.add(postId); const itemId = videoId ? String(videoId) : productId ? String(productId) : postId; api.feedback(itemId, 'view', 0.3); }; // Note: unreadMessages is fetched by AuthProvider (shared across all pages // via useAuth context) and polled every 30 seconds. No duplicate fetch here. useEffect(() => { (async () => { try { // Use REAL AI-driven feed (Gorse → Chroma personalization → real trending) // No more hardcoded api.products.home() — let the recommender drive the feed. const [vidResp, recommendResp, storiesResp, liveResp, sellersResp] = await Promise.all([ api.videos.feed(20), api.recommend.home(40), api.stories.activeBar().catch(() => ({ success: false })), api.live.list('live').catch(() => ({ success: false })), api.social.discover(60).catch(() => ({ success: false })), ]); const sellerMap = new Map(); if (sellersResp.success) { const sellersList = sellersResp.sellers || []; setSellers(sellersList); sellersList.forEach((s: any) => { sellerMap.set(s.id, { name: s.business_name || s.farm_name || 'Seller', image: s.profile_image, slug: s.slug }); }); } const posts: FeedPost[] = []; if (vidResp.success) { (vidResp.videos || []).forEach((v: any) => { const seller = v.seller || {}; // If the video seller doesn't have a slug, look it up via sellerMap const sellerInfo = seller.id ? sellerMap.get(seller.id) : null; posts.push({ type: 'video', id: `vid-${v.id}`, videoId: v.id, productId: v.product?.id, liked: v.liked || false, sellerId: seller.id, sellerSlug: seller.slug || sellerInfo?.slug, sellerName: seller.business_name || 'Seller', sellerImage: seller.profile_image, mediaUrl: v.video_url || '', caption: v.caption || '', // REAL counts from DB — no more Math.floor(views/20) fake math likes: v.likes_count || 0, views: v.views_count || 0, comments: v.comments_count || 0, product: v.product, soldCount: v.product?.units_sold, createdAt: v.created_at, verified: true, }); }); } // AI-driven product feed — posts come from the recommender (Gorse/Chroma/trending). // Each product's likes/views/comments are REAL numbers from Supabase, never faked. if (recommendResp.success) { // The recommend API returns a flat `products` array (regardless of source). // No more flashDeals/trending/newArrivals hardcoded buckets. const allProducts: Product[] = recommendResp.products || []; allProducts.forEach((p: any) => { const sellerInfo = p.seller_id ? sellerMap.get(p.seller_id) : null; posts.push({ type: 'product', id: `prod-${p.id}`, productId: p.id, sellerId: p.seller_id, sellerSlug: sellerInfo?.slug, sellerName: sellerInfo?.name || 'Cellex Seller', sellerImage: sellerInfo?.image, mediaUrl: p.image_url || '', caption: p.name, // REAL counts — no more Math.floor(units_sold * 0.3) fake math. // If a count is not tracked in the DB yet, show 0 (honest) instead of a lie. likes: p.likes_count || 0, views: p._views_count || p.views_count || 0, comments: p.comments_count || p.review_count || 0, product: p, soldCount: p.units_sold, verified: true, }); }); } // Interleave videos and products const videoPosts = posts.filter(p => p.type === 'video'); const productPosts = posts.filter(p => p.type === 'product'); const interleaved: FeedPost[] = []; const maxLen = Math.max(videoPosts.length, productPosts.length); for (let i = 0; i < maxLen; i++) { if (i < videoPosts.length) interleaved.push(videoPosts[i]); if (i < productPosts.length) interleaved.push(productPosts[i]); } setFeed(interleaved); // Initialize liked posts from API response const initialLiked = new Set(); interleaved.forEach(p => { if (p.liked) initialLiked.add(p.id); }); setLikedPosts(initialLiked); if (storiesResp.success) setStories(storiesResp.stories || []); if (liveResp.success) setLiveSessions(liveResp.sessions || []); // Store video posts as Shorts (for the Shorts section). // These are the same videos that appear in the feed, but presented // as vertical 9:16 cards in a horizontal scroll (YouTube Shorts style). if (vidResp.success) { const videoShorts = (vidResp.videos || []).slice(0, 10).map((v: any) => ({ id: v.id, videoUrl: v.video_url || '', caption: v.caption || '', views: v.views_count || 0, likes: v.likes_count || 0, seller: v.seller || {}, product: v.product, createdAt: v.created_at, })); setShorts(videoShorts); } } catch (e) { console.error('Feed load error:', e); } finally { setLoading(false); } })(); }, []); const toggleLike = (postId: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!user) { router.push('/login'); return; } const post = feed.find(p => p.id === postId); if (!post) return; const isLiking = !likedPosts.has(postId); const newLiked = new Set(likedPosts); if (isLiking) { newLiked.add(postId); burst(e.clientX, e.clientY, 'heart'); } else { newLiked.delete(postId); } setLikedPosts(newLiked); // Persist video likes to the real product_video_likes table via the videos edge function if (post.videoId) { if (isLiking) api.videos.like(post.videoId); else api.videos.unlike(post.videoId); } // Send REAL feedback (persists to product_view_log/buyers_wishlist for products, // fires to Gorse for both products and videos) const itemId = post.videoId ? String(post.videoId) : post.productId ? String(post.productId) : postId; api.feedback(itemId, isLiking ? 'like' : 'unlike', isLiking ? 1 : 0); }; const toggleSave = (postId: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); const post = feed.find(p => p.id === postId); if (!post) return; if (!user) { router.push('/login'); return; } const isSaving = !savedPosts.has(postId); const newSaved = new Set(savedPosts); if (isSaving) { newSaved.add(postId); toast({ title: 'Saved!' }); } else { newSaved.delete(postId); } setSavedPosts(newSaved); // REAL save: persist to buyers_wishlist via the feedback API // (which writes a real row to Supabase + fires Gorse feedback in background) if (post.productId) { api.feedback(String(post.productId), isSaving ? 'save' : 'unsave', isSaving ? 1 : 0, { page: 'feed' }); } else if (post.videoId) { // Video saves — only Gorse feedback (no dedicated table yet) api.feedback(String(post.videoId), isSaving ? 'save' : 'unsave', isSaving ? 1 : 0, { page: 'feed' }); } }; const toggleFollow = (sellerId: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!user) { router.push('/login'); return; } const newFollowing = new Set(following); if (newFollowing.has(sellerId)) { newFollowing.delete(sellerId); api.social.unfollow(sellerId); } else { newFollowing.add(sellerId); api.social.follow(sellerId); } setFollowing(newFollowing); }; const addToCart = (product: Product, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!user) { router.push('/login'); return; } api.cart.add(product.id, 1); burst(e.clientX, e.clientY, 'check'); toast({ title: 'Added to cart!', description: product.name }); // Send feedback to Gorse (strong signal — intent to buy) api.feedback(String(product.id), 'click', 1.5, { page: 'feed' }); }; if (loading) { return ; } return (
{/* Top bar — IG-style: logo left, search center, icons right */}
{/* Logo */} Cellex {/* Search — desktop only (pill button). Mobile users access Smart Search via the Explore/Categories page search bar, or via Cmd+K on physical keyboards. */} {/* Spacer on mobile (search hidden) */}
{/* RIGHT SIDE — role-dependent. Badges are FUNCTIONAL: only show when there's actual unread data. */} {isSeller ? (
{user && unreadMessages > 0 && ( {unreadMessages > 9 ? '9+' : unreadMessages} )}
) : ( /* Buyer: Notifications + Account icons in header. Account links to /profile (personal profile). Mobile nav no longer has Account — it has Shorts instead. */
{user && ( )}
)}
{/* Stories section — IG-style horizontal scroll with gradient rings */} {stories.length > 0 && (
{stories.slice(0, 12).map((s: any, i: number) => { const storyHref = s.slug ? `/${s.slug}` : `/seller-profile?id=${s.seller_id || ''}`; return (
{s.profile_image ? ( ) : (
)}
{s.business_name || 'Seller'} ); })}
)} {/* Live Auctions section — eBay-style horizontal scroll of live seller cards. Replaces the old small "LIVE NOW" pill. Each card shows the seller, their live title, viewer count, and a Watch Live CTA. */} {liveSessions.length > 0 && ( )} {/* Shorts section — YouTube Shorts / IG Reels style horizontal scroll. Vertical 9:16 video thumbnails with caption + views. Appears above the feed so users can discover video content quickly. */} {shorts.length > 0 && ( )} {/* Feed — IG-style. Every 3 feed posts, insert a horizontal "Suggested Sellers" carousel. The carousel shows 3 seller cards followed by a "See all" card that links to /sellers. Sellers are rotated so each carousel shows a different batch. */}
{feed.map((post, index) => { // Insert a seller carousel AFTER every 3rd post (index 2, 5, 8, ...) const showSellers = sellers.length > 0 && (index + 1) % 3 === 0; // Rotate the seller batch: carousel 0 shows sellers[0..2], carousel 1 shows sellers[3..5], etc. const carouselIndex = Math.floor((index + 1) / 3) - 1; const sellerBatch = sellers.slice(carouselIndex * 3, carouselIndex * 3 + 3); return (
toggleLike(post.id, e)} onSave={(e) => toggleSave(post.id, e)} onFollow={(e) => post.sellerId && toggleFollow(post.sellerId, e)} onAddToCart={(e) => post.product && addToCart(post.product, e)} trackView={trackView} /> {showSellers && sellerBatch.length > 0 && ( toggleFollow(sellerId, e)} /> )}
); })}
{/* End of feed — IG-style */}

You're all caught up

You've seen all new posts from the last 3 days.

); } function FeedPostCard({ post, index, liked, saved, isFollowing, onLike, onSave, onFollow, onAddToCart, trackView }: { post: FeedPost; index: number; liked: boolean; saved: boolean; isFollowing: boolean; onLike: (e: React.MouseEvent) => void; onSave: (e: React.MouseEvent) => void; onFollow: (e: React.MouseEvent) => void; onAddToCart: (e: React.MouseEvent) => void; trackView: (postId: string, videoId?: number, productId?: number) => void; }) { const videoRef = useRef(null); const [inView, setInView] = useState(false); useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { setInView(entry.isIntersecting); if (entry.isIntersecting) { trackView(post.id, post.videoId, post.productId); } }, { threshold: 0.5 } ); if (videoRef.current) observer.observe(videoRef.current); return () => observer.disconnect(); }, []); useEffect(() => { if (videoRef.current) { if (inView) videoRef.current.play().catch(() => {}); else videoRef.current.pause(); } }, [inView]); const isVideo = post.type === 'video'; const likeCount = post.likes + (liked ? 1 : 0); const fomoText = post.soldCount && post.soldCount > 5 ? `${post.soldCount > 1000 ? `${(post.soldCount / 1000).toFixed(1)}k` : post.soldCount} bought this` : post.views && post.views > 50 ? `${formatCount(post.views)} viewing now` : post.soldCount && post.soldCount > 0 ? `${post.soldCount} bought this` : null; return ( {/* Seller header — IG-style: avatar + username + verified + Follow */}
{post.sellerImage ? ( ) : (
{post.sellerName.charAt(0)}
)}
{post.sellerName} {post.verified && ( )} {post.createdAt && ( <> {timeAgo(post.createdAt)} )}
{post.sellerId && !isFollowing && ( )}
{/* Media — IG-style: square, full-bleed, with hover zoom on images */}
{isVideo ? (
{/* Action bar — IG-style: 24px icons, no labels, gap 16px */}
{/* Likes count — IG-style bold */}
{formatCount(likeCount)} likes
{/* Caption — IG-style: bold username + text */}
{post.sellerName} {post.caption} {post.product?.category && ( #{post.product.category.toLowerCase().replace(/\s+/g, '')} )}
{/* Comments link */} {post.comments > 0 && (
View all {formatCount(post.comments)} comments
)} {/* Timestamp */} {post.createdAt && (
{timeAgo(post.createdAt)} AGO
)} {/* Product CTA — IG-style shoppable tag at bottom */} {post.product && (
{post.product.image_url && ( )}
{post.product.name}
{formatPrice(post.product.price)}
)}
); } function timeAgo(iso?: string): string { if (!iso) return ''; const d = new Date(iso); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return 'JUST NOW'; if (diff < 3600) return Math.floor(diff / 60) + 'M'; if (diff < 86400) return Math.floor(diff / 3600) + 'H'; if (diff < 604800) return Math.floor(diff / 86400) + 'D'; return d.toLocaleDateString(); } function formatCount(n: number): string { if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`; if (n >= 1000) return `${(n / 1000).toFixed(1)}K`; return String(n); } /** * Format the duration a live session has been running. * e.g. "5m", "1h 23m", "2h" */ function formatLiveDuration(startedAt: string): string { const start = new Date(startedAt).getTime(); const diff = Date.now() - start; if (diff < 0) return '0m'; const minutes = Math.floor(diff / 60000); const hours = Math.floor(minutes / 60); if (hours > 0) return `${hours}h ${minutes % 60}m`; return `${minutes}m`; } /** * LiveAuctionsSection — eBay-style horizontal scroll of live seller cards. * * Each card shows: * - Seller avatar with pulsing red LIVE ring * - Seller name * - Live session title (e.g. "Friday tech deals") * - Viewer count with eye icon * - "Watch Live" CTA button * * The section has a dark gradient background to make the LIVE cards pop, * similar to eBay's live auction section. */ function LiveAuctionsSection({ sessions }: { sessions: any[] }) { return (
{/* Dark gradient backdrop inside the card so the LIVE cards pop */}
{/* Section header */}
LIVE NOW · {sessions.length} seller{sessions.length === 1 ? '' : 's'} streaming
See all
{/* Horizontal scroll of live cards */}
{sessions.map((session, index) => { const seller = session.seller || {}; const sellerName = seller.business_name || 'Seller'; const sellerImage = seller.profile_image; const title = session.title || `${sellerName} is live`; const viewers = session.viewer_count || 0; const liveDuration = session.started_at ? formatLiveDuration(session.started_at) : null; return ( {/* Top: avatar + LIVE badge + duration */}
{/* Pulsing red ring around avatar */}
{sellerImage ? ( {sellerName} ) : (
{sellerName.charAt(0).toUpperCase()}
)}
{/* LIVE badge */}
LIVE
{/* Duration */} {liveDuration && (
{liveDuration}
)}
{/* Bottom: title + viewers + CTA */}
{sellerName}
{title}
{formatCount(viewers)} watching Watch Live
); })}
); } /** * ShortsSection — YouTube Shorts / IG Reels style horizontal scroll. * * Each card is a vertical 9:16 video thumbnail with: * - Caption overlay at bottom * - View count + play icon * - Seller name * * Tapping a short navigates to /videos (the full-screen video viewer). */ function ShortsSection({ shorts }: { shorts: any[] }) { return (
{/* Section header */}

Shorts

See all
{/* Horizontal scroll of vertical video cards */}
{shorts.map((short, index) => { const seller = short.seller || {}; const sellerName = seller.business_name || 'Seller'; const caption = short.caption || ''; const views = short.views || 0; const likes = short.likes || 0; const productImage = short.product?.image_url; return ( {/* Vertical 9:16 video thumbnail */}
{short.videoUrl ? (
); } /** * SuggestedSellersCarousel — horizontal scroll of seller cards, inserted * between feed posts every 3 items. Shows 3 sellers + a "See all" card * that links to /sellers. * * Layout matches Instagram's "Suggested for you" pattern: * - Section header: "Suggested Sellers" + "See All" link * - Horizontal scroll of square-ish seller cards (avatar + name + Follow btn) * - Final card: "See all sellers" with chevron */ function SuggestedSellersCarousel({ sellers, carouselIndex, following, onFollow, }: { sellers: any[]; carouselIndex: number; following: Set; onFollow: (sellerId: string, e: React.MouseEvent) => void; }) { // Vary the header label slightly so repeat carousels feel fresh const headerLabel = carouselIndex === 0 ? 'Suggested Sellers' : carouselIndex === 1 ? 'Discover More Sellers' : 'More Sellers to Follow'; return (
{/* Section header */}

{headerLabel}

See All
{/* Horizontal scroll of seller cards */}
{sellers.map((seller, index) => { const sellerId = seller.id; const name = seller.business_name || seller.farm_name || 'Seller'; const image = seller.profile_image; const category = seller.business_category; const isFollowing = following.has(sellerId); const sellerHref = seller.slug ? `/${seller.slug}` : `/seller-profile?id=${sellerId}`; return (
{image ? ( {name} ) : (
{name.charAt(0).toUpperCase()}
)}
{name} {category && (

{category}

)}
); })} {/* "See all sellers" card — links to /sellers page */}
See all sellers Discover more stores
); }