240 lines
8.9 KiB
TypeScript
240 lines
8.9 KiB
TypeScript
import React from 'react'
|
|
import { Quantifier, MiniQuantifier } from 'web-components'
|
|
import { useGetCart, useUpdateCartItem, useRemoveFromCart, useAddToCart } from '../hooks/cart-query-hooks'
|
|
import { useCartStore } from '../lib/stores/cart-store'
|
|
import { useCentralSlotStore } from '../lib/stores/central-slot-store'
|
|
import { useProductSlotIdentifier } from '../hooks/useProductSlotIdentifier'
|
|
import dayjs from 'dayjs'
|
|
import { Truck, ShoppingCart, ImageOff } from 'lucide-react'
|
|
|
|
interface ProductCardProps {
|
|
item: any
|
|
onPress?: () => void
|
|
showDeliveryInfo?: boolean
|
|
miniView?: boolean
|
|
useAddToCartDialog?: boolean
|
|
}
|
|
|
|
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}` }
|
|
}
|
|
|
|
export function ProductCard({
|
|
item,
|
|
onPress,
|
|
showDeliveryInfo = true,
|
|
miniView = false,
|
|
useAddToCartDialog = false,
|
|
}: ProductCardProps) {
|
|
const [imageError, setImageError] = React.useState(false)
|
|
const [imageLoading, setImageLoading] = React.useState(true)
|
|
|
|
const imageUri = item.images?.[0]
|
|
|
|
const { data: cartData } = useGetCart('regular')
|
|
const { getQuickestSlot } = useProductSlotIdentifier()
|
|
const { setAddedToCartProduct } = useCartStore()
|
|
const updateCartItem = useUpdateCartItem('regular')
|
|
const removeFromCart = useRemoveFromCart('regular')
|
|
const addToCart = useAddToCart('regular')
|
|
|
|
// Find current quantity from cart data
|
|
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id)
|
|
const quantity = cartItem?.quantity || 0
|
|
|
|
// Get slots data from central store
|
|
const slots = useCentralSlotStore((state) => state.slots)
|
|
const productSlotsMap = useCentralSlotStore((state) => state.productSlotsMap)
|
|
|
|
// Create slot lookup map
|
|
const slotMap = React.useMemo(() => {
|
|
const map: Record<number, any> = {}
|
|
slots?.forEach((slot: any) => {
|
|
map[slot.id] = slot
|
|
})
|
|
return map
|
|
}, [slots])
|
|
|
|
// Get cart item's slot delivery time if item is in cart
|
|
const cartSlot = cartItem?.slotId ? slotMap[cartItem.slotId] : null
|
|
const displayDeliveryDate = cartSlot?.deliveryTime || item.nextDeliveryDate
|
|
|
|
// Precompute the next slot and determine display out of stock status
|
|
const slotId = getQuickestSlot(item.id)
|
|
const productSlotInfo = productSlotsMap[item.id]
|
|
const isOutOfStockFromSlots = productSlotInfo?.isOutOfStock
|
|
const displayIsOutOfStock = isOutOfStockFromSlots || !slotId
|
|
|
|
const handleQuantityChange = (newQuantity: number) => {
|
|
if (useAddToCartDialog) {
|
|
setAddedToCartProduct({ productId: item.id, product: item })
|
|
} else if (newQuantity === 0 && cartItem) {
|
|
removeFromCart.mutate(cartItem.id)
|
|
} else if (newQuantity === 1 && !cartItem) {
|
|
const slotId = getQuickestSlot(item.id)
|
|
if (!slotId) {
|
|
alert('No available delivery slot for this product')
|
|
return
|
|
}
|
|
const slot = slotMap[slotId]
|
|
const deliveryTime = slot ? dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A') : ''
|
|
addToCart.mutate(
|
|
{ productId: item.id, quantity: 1, slotId, storeId: item.storeId },
|
|
{
|
|
onSuccess: () => {
|
|
alert(`Added ${item.name} for delivery at ${deliveryTime}`)
|
|
},
|
|
}
|
|
)
|
|
} else if (cartItem) {
|
|
updateCartItem.mutate({ productId: item.id, quantity: newQuantity })
|
|
}
|
|
}
|
|
|
|
const discountPercent =
|
|
item.marketPrice && Number(item.marketPrice) > Number(item.price)
|
|
? Math.round(((Number(item.marketPrice) - Number(item.price)) / Number(item.marketPrice)) * 100)
|
|
: 0
|
|
|
|
return (
|
|
<div
|
|
className="group flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white transition-all hover:border-brand-300 hover:shadow-md"
|
|
onClick={onPress}
|
|
>
|
|
{/* Image — grows to fill card height in miniView so content area stays tight */}
|
|
<div
|
|
className={`relative w-full overflow-hidden bg-gray-50 ${
|
|
miniView ? 'min-h-[110px] flex-1' : 'aspect-[4/3] shrink-0'
|
|
}`}
|
|
>
|
|
{imageUri && !imageError ? (
|
|
<img
|
|
src={imageUri}
|
|
alt={item.name}
|
|
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
onError={() => setImageError(true)}
|
|
onLoad={() => setImageLoading(false)}
|
|
/>
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center bg-gray-50">
|
|
<ImageOff className="h-8 w-8 text-gray-300" />
|
|
</div>
|
|
)}
|
|
|
|
{imageLoading && imageUri && !imageError && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
|
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-brand-500 border-t-transparent" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Discount label hidden for now
|
|
{discountPercent > 0 && (
|
|
<div className="absolute left-3 top-3 rounded bg-brand-600 px-2 py-0.5">
|
|
<p className="text-[10px] font-black uppercase tracking-wide text-white">
|
|
{discountPercent}% OFF
|
|
</p>
|
|
</div>
|
|
)}
|
|
*/}
|
|
|
|
{displayIsOutOfStock && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/50">
|
|
<div className="rounded-full bg-gray-900 px-3 py-1">
|
|
<p className="text-xs font-bold text-white">Out of Stock</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{miniView && (
|
|
<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-brand-50"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleQuantityChange(1)
|
|
}}
|
|
>
|
|
<ShoppingCart className="h-4 w-4 text-brand-600" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex flex-1 flex-col px-1.5 pb-2.5 pt-2.5">
|
|
<p className="mb-1 text-[13px] font-medium leading-snug text-gray-900 line-clamp-2 sm:text-sm">
|
|
{item.name}
|
|
</p>
|
|
|
|
<div className="mb-1 flex items-baseline">
|
|
<p className="text-[15px] font-extrabold text-brand-600 sm:text-lg">₹{item.price}</p>
|
|
{discountPercent > 0 && (
|
|
<p className="ml-2 text-[11px] text-gray-400 line-through sm:text-xs">₹{item.marketPrice}</p>
|
|
)}
|
|
</div>
|
|
|
|
<p className="mb-1 text-[11px] font-medium text-gray-500 sm:text-xs">
|
|
Qty: <span className="font-semibold text-brand-700/70">{formatQuantity(item.productQuantity || 1, item.unitNotation).display}</span>
|
|
</p>
|
|
|
|
{/* Delivery date hidden for now
|
|
{showDeliveryInfo && displayDeliveryDate && (
|
|
<div className="mb-1.5 self-start">
|
|
<div className="flex items-center">
|
|
<Truck className="h-3 w-3 shrink-0 text-brand-700/70" />
|
|
<p className="ml-1.5 text-[10px] font-bold leading-tight text-brand-700/70">
|
|
{dayjs(displayDeliveryDate).format('ddd, DD MMM')}
|
|
</p>
|
|
</div>
|
|
<p className="pl-[18px] pt-0.5 text-[10px] font-bold leading-tight text-brand-700/70">
|
|
{dayjs(displayDeliveryDate).format('h:mm A')}
|
|
</p>
|
|
</div>
|
|
)}
|
|
*/}
|
|
|
|
{!miniView && (
|
|
<div className="mt-auto">
|
|
{displayIsOutOfStock ? (
|
|
<div className="rounded-lg bg-gray-50 py-2 text-center">
|
|
<p className="text-xs font-bold uppercase tracking-wide text-gray-400">
|
|
Unavailable
|
|
</p>
|
|
</div>
|
|
) : quantity > 0 ? (
|
|
<Quantifier
|
|
value={quantity}
|
|
setValue={handleQuantityChange}
|
|
step={item.incrementStep}
|
|
unit={item.unitNotation}
|
|
/>
|
|
) : (
|
|
<button
|
|
className="flex w-full items-center justify-center gap-1.5 rounded-lg bg-brand-600 py-2.5 text-xs font-bold uppercase tracking-wide text-white transition-colors hover:bg-brand-700"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
handleQuantityChange(1)
|
|
}}
|
|
>
|
|
<ShoppingCart className="h-4 w-4" />
|
|
<span className="md:hidden">Add</span>
|
|
<span className="hidden md:inline">Add to Cart</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|