291 lines
10 KiB
TypeScript
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>
|
|
);
|
|
}
|