freshyo/apps/web-ui/src/routes/flash.tsx
2026-09-08 13:55:59 +05:30

291 lines
10 KiB
TypeScript

import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState, useMemo, useRef, useEffect } from 'react'
import { MiniQuantifier } from 'web-components'
import { useAllProducts, useStores } from '../hooks/prominent-api-hooks'
import { useCentralProductStore } from '../lib/stores/central-product-store'
import { useCentralSlotStore } from '../lib/stores/central-slot-store'
import { useAddToCart, useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
import { usePopulateCentralStores } from '../hooks/usePopulateCentralStores'
import { usePopulateCentralProductStore } from '../hooks/usePopulateCentralProductStore'
import { AppLayout } from '../components/AppLayout'
import { Zap, ShoppingCart } from 'lucide-react'
import type { CompactProductCardProps } from '@packages/shared'
export const Route = createFileRoute('/flash')({
component: FlashDeliveryPage,
})
function FlashDeliveryPage() {
const navigate = useNavigate()
// Populate central stores so flash-eligible products resolve on direct load
usePopulateCentralStores()
usePopulateCentralProductStore()
const { data: storesData } = useStores()
const { productsById } = useCentralProductStore()
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap)
const stores = storesData?.stores || []
const addToCart = useAddToCart('flash')
const { data: cartData } = useGetCart('flash')
// Store tabs — one tab per store (same model as home's category tabs)
const storesWithFlash = useMemo(() => {
return stores.filter((store: any) =>
Object.values(productsById).some(
(p: any) =>
p?.storeId === store.id &&
productSlotsMap[p.id]?.isFlashAvailable &&
!productSlotsMap[p.id]?.isOutOfStock
)
)
}, [stores, productsById, productSlotsMap])
const [selectedStoreId, setSelectedStoreId] = useState<number | null>(null)
const activeStoreId = selectedStoreId ?? storesWithFlash[0]?.id ?? null
// Auto-scroll the active store tab into view (matches user-ui tab behavior)
const tabsScrollRef = useRef<HTMLDivElement>(null)
const tabPositions = useRef<number[]>([])
const activeTabIndex = Math.max(
0,
storesWithFlash.findIndex((s: any) => s.id === activeStoreId)
)
useEffect(() => {
const x = tabPositions.current[activeTabIndex]
if (x != null) {
tabsScrollRef.current?.scrollTo({ left: Math.max(0, x - 16), behavior: 'smooth' })
}
}, [activeTabIndex])
// Get flash products from central store
const allFlashProducts = useMemo(() => {
return Object.values(productsById).filter(
(product: any) =>
product &&
productSlotsMap[product.id]?.isFlashAvailable &&
!productSlotsMap[product.id]?.isOutOfStock
)
}, [productsById, productSlotsMap])
// Products to show: active store's flash items, or all flash items when no stores
const filteredProducts = useMemo(() => {
if (activeStoreId != null) {
return allFlashProducts.filter((p: any) => p.storeId === activeStoreId)
}
return allFlashProducts
}, [activeStoreId, allFlashProducts])
const handleAddToCart = (productId: number) => {
const item = filteredProducts.find((p: any) => p.id === productId)
addToCart.mutate(
{ skuId: productId, quantity: 1, storeId: item?.storeId ?? 0 },
{
onSuccess: () => {
alert(`Added ${item?.name || 'item'} for 1 Hr Delivery`)
},
}
)
}
return (
<AppLayout isFlashDelivery={true}>
<div className="min-h-screen bg-gray-50">
{/* Page header */}
<div className="border-b border-gray-200 bg-white">
<div className="mx-auto flex w-full max-w-7xl items-center gap-2 px-4 py-5 md:px-8">
<Zap className="h-6 w-6 text-flash-500" fill="currentColor" />
<div>
<p className="text-[11px] font-black uppercase tracking-[0.22em] text-flash-500">
Express Counter
</p>
<h1 className="display-2 text-gray-900">Delivery within 1 hour</h1>
</div>
</div>
</div>
<div className="mx-auto w-full max-w-7xl px-4 py-6 pb-32 md:px-8 md:pb-16">
{/* Info Banner */}
<div className="mb-4 rounded-xl border border-yellow-200 bg-yellow-50 p-3">
<p className="text-sm text-yellow-800">
Get these products delivered within 1 hour! Only available for select items.
</p>
</div>
{/* Store tabs — same model as home page tabs */}
{storesWithFlash.length > 0 && (
<div
ref={tabsScrollRef}
className="scrollbar-hide -mx-4 mb-5 flex gap-1 overflow-x-auto px-4 md:mx-0 md:px-0"
>
{storesWithFlash.map((store: any, i: number) => {
const active = activeStoreId === store.id
return (
<button
key={store.id}
ref={(el) => {
tabPositions.current[i] = el?.offsetLeft ?? 0
}}
onClick={() => setSelectedStoreId(store.id)}
className="shrink-0 cursor-pointer px-3 py-2"
>
<span
className={`block text-sm font-bold ${
active ? 'text-flash-600' : 'text-gray-500'
}`}
>
{store.name.replace(/^The\s+/i, '')}
</span>
<span
className={`mt-1 block h-1 rounded-full ${
active ? 'bg-flash-500' : 'bg-transparent'
}`}
/>
</button>
)
})}
</div>
)}
<div className="mb-4 flex items-center justify-between">
<p className="text-lg font-extrabold text-gray-900">
{activeStoreId != null
? stores.find((s: any) => s.id === activeStoreId)?.name.replace(/^The\s+/i, '') ||
'Store Products'
: 'All Products'}
</p>
<p className="text-sm text-gray-500">
{filteredProducts.length} items
</p>
</div>
<div className="grid grid-cols-3 gap-3 sm:gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{filteredProducts.map((product: any) => (
<CompactProductCard
key={product.id}
item={product}
handleAddToCart={handleAddToCart}
onPress={() =>
navigate({
to: '/flash/product/$id',
params: { id: String(product.id) },
})
}
/>
))}
</div>
{filteredProducts.length === 0 && (
<div className="py-10 text-center">
<p className="font-medium text-gray-400">
No flash delivery products available.
</p>
</div>
)}
</div>
</div>
</AppLayout>
)
}
const formatQuantity = (
quantity: number,
unit: string,
): { value: string; display: string } => {
if (unit?.toLowerCase() === 'kg' && quantity < 1) {
return {
value: `${Math.round(quantity * 1000)} g`,
display: `${Math.round(quantity * 1000)}g`,
};
}
return { value: `${quantity} ${unit}(s)`, display: `${quantity}${unit}` };
};
function CompactProductCard({
item,
handleAddToCart,
onPress,
}: CompactProductCardProps) {
const { data: cartData } = useGetCart('flash');
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap);
const updateCartItem = useUpdateCartItem('flash');
const removeFromCart = useRemoveFromCart('flash');
const cartItem = cartData?.items?.find(
(cartItem: any) => cartItem.skuId === item.id,
);
const quantity = cartItem?.quantity || 0;
const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock;
const handleQuantityChange = (newQuantity: number) => {
if (newQuantity === 0 && cartItem) {
removeFromCart.mutate(cartItem.id);
} else if (newQuantity === 1 && !cartItem) {
handleAddToCart(item.id);
} else if (cartItem) {
updateCartItem.mutate({ skuId: cartItem.skuId, quantity: newQuantity });
}
};
const price = item.flashPrice ?? item.price
return (
<div
onClick={onPress}
className="group overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-flash-300 hover:shadow-md"
>
<div className="relative">
<img
src={item.images?.[0]}
alt={item.name}
className="aspect-square w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
/>
{isOutOfStock && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/40">
<p className="text-xs font-bold text-white">Out of Stock</p>
</div>
)}
<div className="absolute bottom-2 right-2">
{quantity > 0 ? (
<MiniQuantifier
value={quantity}
setValue={handleQuantityChange}
step={item.incrementStep}
/>
) : (
<div
className="flex h-9 w-9 items-center justify-center rounded-full bg-white shadow-md transition-colors hover:bg-flash-25"
onClick={(e) => {
e.stopPropagation();
handleQuantityChange(1);
}}
>
<ShoppingCart className="h-4 w-4 text-flash-500" />
</div>
)}
</div>
</div>
<div className="p-3">
<p className="mb-1 line-clamp-2 text-xs font-bold text-gray-900">{item.name}</p>
<div className="flex flex-wrap items-baseline gap-x-1.5">
<p className="text-sm font-extrabold text-flash-500">{price}</p>
{item.marketPrice && Number(item.marketPrice) > Number(item.price) && (
<p className="text-[11px] text-gray-400 line-through">
{item.marketPrice}
</p>
)}
</div>
<p className="mt-0.5 text-[11px] text-gray-500">
<span className="font-semibold text-flash-500">
{formatQuantity(item.productQuantity || 1, item.unit || item.unitNotation).display}
</span>
</p>
</div>
</div>
);
}