import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useState, useEffect, useMemo, useRef } from 'react' import dayjs from 'dayjs' import { useAllProducts, useStores, useBanners, useSlots, useGetEssentialConsts, } from '../hooks/prominent-api-hooks' import { useCartStore } from '../lib/stores/cart-store' import { useCentralSlotStore } from '../lib/stores/central-slot-store' import { AppLayout } from '../components/AppLayout' import { ProductCard } from '../components/ProductCard' import AddToCartDialog from '../components/AddToCartDialog' import { useProductSlotIdentifier } from '../hooks/useProductSlotIdentifier' import { usePopulateCentralStores } from '../hooks/usePopulateCentralStores' import { Store, ImageOff, Loader2, Clock, Truck } from 'lucide-react' // Scroll Indicator Component function ScrollIndicator({ containerRef, itemCount, itemWidth, }: { containerRef: React.RefObject itemCount: number itemWidth: number }) { const [activeIndex, setActiveIndex] = useState(0) useEffect(() => { const container = containerRef.current if (!container) return const handleScroll = () => { const scrollLeft = container.scrollLeft const maxScroll = container.scrollWidth - container.clientWidth const scrollProgress = maxScroll > 0 ? scrollLeft / maxScroll : 0 const totalDots = Math.min(itemCount, 5) const newIndex = Math.round(scrollProgress * (totalDots - 1)) setActiveIndex(Math.min(newIndex, totalDots - 1)) } container.addEventListener('scroll', handleScroll, { passive: true }) handleScroll() return () => container.removeEventListener('scroll', handleScroll) }, [containerRef, itemCount]) const totalDots = Math.min(itemCount, 5) if (totalDots <= 1) return null return (
{Array.from({ length: totalDots }).map((_, i) => (
))}
) } export const Route = createFileRoute('/home/')({ component: HomePage }) // Section Spinner Component function SectionSpinner({ label }: { label?: string }) { return (
{label &&

{label}

}
) } function HomePage() { const navigate = useNavigate() const { data: productsData, isLoading: isProductsLoading } = useAllProducts() const { data: storesData, isLoading: isStoresLoading } = useStores() const { data: bannersData } = useBanners() const { data: slotsData, isLoading: isSlotsLoading } = useSlots() const { data: essentialConsts } = useGetEssentialConsts() // Handle bootstrapping: products/stores/slots are disabled until essentialConsts provides cacheUrl // NOTE: with persisted caches, render immediately from placeholderData; no startup spinner. const storesLoading = isStoresLoading const productsLoading = isProductsLoading const slotsLoading = isSlotsLoading const { setAddedToCartProduct } = useCartStore() const { getQuickestSlot } = useProductSlotIdentifier() const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap) // Refs for scrollable sections const slotsScrollRef = useRef(null) // Populate central stores with slots and product data usePopulateCentralStores() const stores = storesData?.stores || [] // const banners = bannersData?.banners || [] const allProducts = productsData?.products || [] const slots = slotsData?.slots || [] // Explore Products tabs — tags in tagsOrder, products in per-tag sortOrder const dashboardTags = productsData?.tags || [] const [selectedTagId, setSelectedTagId] = useState(null) const activeTagId = selectedTagId ?? dashboardTags[0]?.id ?? null const [expandedByTagId, setExpandedByTagId] = useState>({}) const handleToggleExpand = (tagId: number) => { setExpandedByTagId((prev) => ({ ...prev, [tagId]: true })) } // Auto-scroll the active tab into view (matches user-ui tab behavior) const tabsScrollRef = useRef(null) const tabPositions = useRef([]) const activeTabIndex = Math.max( 0, dashboardTags.findIndex((t: any) => t.id === activeTagId) ) useEffect(() => { const x = tabPositions.current[activeTabIndex] if (x != null) { tabsScrollRef.current?.scrollTo({ left: Math.max(0, x - 16), behavior: 'smooth' }) } }, [activeTabIndex]) const productsByTagId = useMemo(() => { const map: Record = {} for (const tag of dashboardTags) { const productById = new Map() for (const product of allProducts) { productById.set(product.id, product) } // tag.productIds is already in the admin-curated order (backend sorts by sortOrder). const orderedIds = (tag.productIds || []).filter((id: number) => productById.has(id)) const ordered: any[] = [] const rest: any[] = [] for (const id of orderedIds) { const product = productById.get(id) const isOutOfStock = Boolean(productSlotsMap[id]?.isOutOfStock) || !getQuickestSlot(id) if (isOutOfStock) rest.push(product) else ordered.push(product) } // Products in the tag's productIds but not in the curated order (added later) — availability sort. const seen = new Set(orderedIds) const extra = allProducts .filter((product: any) => (tag.productIds?.includes(product.id) ?? false) && !seen.has(product.id)) .sort((a: any, b: any) => { const slotA = getQuickestSlot(a.id) const slotB = getQuickestSlot(b.id) const aOutOfStock = Boolean(productSlotsMap[a.id]?.isOutOfStock) || !slotA const bOutOfStock = Boolean(productSlotsMap[b.id]?.isOutOfStock) || !slotB if (aOutOfStock && !bOutOfStock) return 1 if (!aOutOfStock && bOutOfStock) return -1 return 0 }) map[tag.id] = [...ordered, ...rest, ...extra] } return map }, [dashboardTags, allProducts, getQuickestSlot, productSlotsMap]) const activeTagProducts = activeTagId != null ? (productsByTagId[activeTagId] || []) : [] // Sort products: in-stock first, then by slot availability const sortedProducts = useMemo(() => { return [...allProducts] .filter((p) => typeof p.id === 'number') .sort((a, b) => { const slotA = getQuickestSlot(a.id) const slotB = getQuickestSlot(b.id) const aOutOfStock = Boolean(productSlotsMap[a.id]?.isOutOfStock) || !slotA const bOutOfStock = Boolean(productSlotsMap[b.id]?.isOutOfStock) || !slotB if (aOutOfStock && !bOutOfStock) return 1 if (!aOutOfStock && bOutOfStock) return -1 return 0 }) }, [productsData, productSlotsMap]) // Sort slots by delivery time const sortedSlots = useMemo(() => { const now = dayjs() return [...slots] .filter((slot) => dayjs(slot.deliveryTime).isAfter(now)) .sort((a, b) => { const deliveryDiff = dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime)) if (deliveryDiff !== 0) return deliveryDiff return dayjs(a.freezeTime).diff(dayjs(b.freezeTime)) }) }, [slots]) const handleProductPress = (id: number) => { navigate({ to: '/home/product/$id', params: { id: String(id) }, }) } const handleAddToCart = (product: any) => { setAddedToCartProduct({ productId: product.id, product }) } return (
{/* Welcome band — desktop editorial opener */}

Smiles delivered with

The best harvest, delivered to your door

Chicken, mutton, fish and more, cut to order from our own stores. Reserve a delivery slot and we will bring it cold and clean.

{/* Download App Banner */}
Freshyo logo

Get the FreshYo App

Download for exclusive offers & faster checkout

Get App
{/* Explore Products Section — first, matching user-ui home order */} {dashboardTags.length > 0 && (

Browse by Category

Explore Products

{/* Tab strip — flat, brand underline active (matches user-ui home) */}
{dashboardTags.map((tag: any, i: number) => { const active = activeTagId === tag.id return ( ) })}
{/* Active tag products — brand-tinted container (matches user-ui) */}
{activeTagProducts.length > 0 ? ( <>
{activeTagProducts .slice(0, expandedByTagId[activeTagId] ? undefined : 6) .map((product: any) => ( handleProductPress(product.id)} showDeliveryInfo={false} miniView={true} useAddToCartDialog={true} /> ))}
{activeTagProducts.length > 6 && !expandedByTagId[activeTagId] && (
)} ) : (

No products in this category yet

)}
)} {/* Stores Section */}

Our Outlets

Shop by Store

{storesLoading ? ( ) : (
{stores.map((store: any) => (
navigate({ to: '/stores/$storeId', params: { storeId: String(store.id) }, }) } />
))}
)}
{/* Upcoming Delivery Slots Section */}

Plan Ahead

Upcoming Delivery Slots

{slotsLoading ? ( ) : ( <>
{sortedSlots.slice(0, 5).map((slot) => ( ))}
)}
{/* All Products Section */}

The Full Counter

All Available Products

Browse our complete selection

{productsLoading ? ( ) : (
{sortedProducts.map((product) => ( handleProductPress(product.id)} showDeliveryInfo={true} miniView={true} useAddToCartDialog={true} /> ))}
)}
) } function BannerCarousel({ banners }: { banners: any[] }) { const [index, setIndex] = useState(0) const images = banners.map((b: any) => b.imageUrl).filter(Boolean) useEffect(() => { if (images.length <= 1) return const timer = setInterval(() => { setIndex((i) => (i + 1) % images.length) }, 4000) return () => clearInterval(timer) }, [images.length]) if (images.length === 0) return null return (
Banner {images.length > 1 && ( <>
{images.map((_: any, i: number) => (
)}
) } function StoreCard({ store, onClick }: { store: any; onClick: () => void }) { return (
{store.signedImageUrl ? ( {store.name} ) : ( )}

{store.name.replace(/^The\s+/i, '')}

) } function SlotCard({ slot }: { slot: any }) { const navigate = useNavigate() const now = dayjs() const freezeTime = dayjs(slot.freezeTime) const isClosingSoon = freezeTime.diff(now, 'hour') < 4 && freezeTime.isAfter(now) const formatTimeRange = (deliveryTime: string) => { const time = dayjs(deliveryTime) const endTime = time.add(1, 'hour') const startPeriod = time.format('A') const endPeriod = endTime.format('A') if (startPeriod === endPeriod) { return `${time.format('h')}-${endTime.format('h')} ${startPeriod}` } else { return `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}` } } return (
navigate({ to: '/slot-view', search: { slotId: slot.id } })} className={`min-w-70 shrink-0 cursor-pointer rounded-xl border bg-white p-5 shadow-sm transition-all hover:shadow-md ${ isClosingSoon ? 'border-l-4 border-l-amber-400' : 'border-l-4 border-l-brand-600' }`} >
{isClosingSoon && (

CLOSING SOON

)}

Delivery At

{formatTimeRange(slot.deliveryTime)}

{dayjs(slot.deliveryTime).format('ddd, MMM DD')}

Order By

{dayjs(slot.freezeTime).format('h:mm A')}

{dayjs(slot.freezeTime).format('ddd, MMM DD')}

{slot.products?.slice(0, 3).map((p: any, i: number) => (
0 ? '-ml-3' : ''}`} > {p.images?.[0] ? ( ) : (
)}
))}

View all {slot.products?.length || 0} items

) }