272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import type { FlashDeliveryProps } from '@packages/shared'
|
|
import { useNavigate, useLocation } from '@tanstack/react-router'
|
|
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '../hooks/cart-query-hooks'
|
|
import { useAllProducts } from '../hooks/prominent-api-hooks'
|
|
import { useGetEssentialConsts } from '../hooks/prominent-api-hooks'
|
|
import { MiniQuantifier } from 'web-components'
|
|
import { ShoppingCart, ChevronRight, Clock, X, Package } from 'lucide-react'
|
|
import dayjs from 'dayjs'
|
|
|
|
// Smart time window formatting function
|
|
const formatTimeRange = (deliveryTime: string | Date) => {
|
|
const time = dayjs(deliveryTime)
|
|
const endTime = time.add(1, 'hour')
|
|
const startPeriod = time.format('A')
|
|
const endPeriod = endTime.format('A')
|
|
|
|
let timeRange
|
|
if (startPeriod === endPeriod) {
|
|
timeRange = `${time.format('h')}-${endTime.format('h')} ${startPeriod}`
|
|
} else {
|
|
timeRange = `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}`
|
|
}
|
|
|
|
return `${time.format('ddd, DD MMM ')}${timeRange}`
|
|
}
|
|
|
|
export function FloatingCartBar({ isFlashDelivery = false }: FlashDeliveryProps) {
|
|
const navigate = useNavigate()
|
|
const location = useLocation()
|
|
const isCartPage = location.pathname === '/cart' || location.pathname === '/flash/cart' || location.pathname.startsWith('/me')
|
|
const [isExpanded, setIsExpanded] = useState(false)
|
|
const [quantities, setQuantities] = useState<Record<number, number>>({})
|
|
const cartType = isFlashDelivery ? 'flash' : 'regular'
|
|
|
|
const { data: cartData } = useGetCart(cartType)
|
|
const { data: constsData } = useGetEssentialConsts()
|
|
const { data: productsData } = useAllProducts()
|
|
const updateCartItem = useUpdateCartItem(cartType)
|
|
const removeFromCart = useRemoveFromCart(cartType)
|
|
|
|
const products = productsData?.products || []
|
|
const productsById: Record<number, any> = {}
|
|
products.forEach((p: any) => { productsById[p.id] = p })
|
|
|
|
const cartItems = cartData?.items || []
|
|
const itemCount = cartItems.length
|
|
|
|
useEffect(() => {
|
|
const initial: Record<number, number> = {}
|
|
cartItems.forEach((item) => {
|
|
initial[item.id] = item.quantity
|
|
})
|
|
setQuantities(initial)
|
|
}, [cartData])
|
|
|
|
// Calculate total cart value
|
|
const totalCartValue = cartItems.reduce(
|
|
(sum, item) => {
|
|
const product = productsById[item.skuId]
|
|
const basePrice = product?.price ?? 0
|
|
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice
|
|
return sum + price * item.quantity
|
|
},
|
|
0
|
|
)
|
|
|
|
const freeDeliveryThreshold = isFlashDelivery
|
|
? constsData?.flashFreeDeliveryThreshold
|
|
: constsData?.freeDeliveryThreshold
|
|
const remainingForFreeDelivery = Math.max(0, freeDeliveryThreshold - totalCartValue)
|
|
|
|
const cartBarColor = isFlashDelivery ? '#f81260' : 'var(--brand-600, #2563eb)'
|
|
const cartBarBorderColor = isFlashDelivery ? '#e11d48' : 'var(--brand-500, #3b82f6)'
|
|
|
|
const goToCart = () => {
|
|
setIsExpanded(false)
|
|
navigate({ to: isFlashDelivery ? '/flash/cart' : '/cart' })
|
|
}
|
|
|
|
/* ---------- Desktop: slide-over panel ---------- */
|
|
const slideOver = (
|
|
<div className="pointer-events-auto flex h-full w-full max-w-[85vw] flex-col border-l border-gray-200 bg-white">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
|
|
<div>
|
|
<p className="font-display text-xl font-extrabold tracking-tight text-gray-900">
|
|
Your Cart
|
|
</p>
|
|
<p className="text-[11px] font-bold uppercase tracking-[0.18em] text-gray-400">
|
|
{itemCount} {itemCount === 1 ? 'Item' : 'Items'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setIsExpanded(false)}
|
|
className="flex h-10 w-10 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-gray-50 hover:text-gray-700"
|
|
aria-label="Close cart"
|
|
>
|
|
<X className="h-5 w-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Progress */}
|
|
{remainingForFreeDelivery > 0 && (
|
|
<div className="border-b border-gray-100 bg-brand-25 px-6 py-3">
|
|
<div className="mb-1.5 flex items-center justify-between">
|
|
<p className="text-[10px] font-black uppercase tracking-widest text-brand-700">
|
|
Free Delivery Progress
|
|
</p>
|
|
<p className="text-[10px] font-black text-brand-700">
|
|
{Math.round((totalCartValue / freeDeliveryThreshold) * 100)}%
|
|
</p>
|
|
</div>
|
|
<div className="h-1.5 overflow-hidden rounded-full bg-brand-100">
|
|
<div
|
|
className="h-full rounded-full bg-brand-600 transition-all"
|
|
style={{ width: `${Math.min(100, (totalCartValue / freeDeliveryThreshold) * 100)}%` }}
|
|
/>
|
|
</div>
|
|
<p className="mt-1.5 text-[11px] font-semibold text-gray-500">
|
|
Add ₹{remainingForFreeDelivery} more for free delivery
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Items */}
|
|
<div className="flex-1 overflow-y-auto px-6 py-2">
|
|
{cartItems.length === 0 ? (
|
|
<div className="flex h-full flex-col items-center justify-center py-16 text-center">
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gray-50">
|
|
<Package className="h-7 w-7 text-gray-400" />
|
|
</div>
|
|
<p className="mt-4 font-bold text-gray-800">
|
|
{isFlashDelivery ? 'No flash items yet' : 'Your cart is empty'}
|
|
</p>
|
|
<p className="mt-1 text-sm text-gray-500">
|
|
{isFlashDelivery
|
|
? 'Pick something from 1 hr delivery'
|
|
: 'Add fresh products to get started'}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
cartItems.map((item, index) => (
|
|
<React.Fragment key={item.id}>
|
|
<div className="flex items-center gap-4 py-4">
|
|
<img
|
|
src={productsById[item.skuId]?.images?.[0]}
|
|
alt=""
|
|
className="h-14 w-14 shrink-0 rounded-lg border border-gray-100 bg-gray-50 object-cover"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-bold text-gray-900 break-words">
|
|
{productsById[item.skuId]?.name || ''}
|
|
</p>
|
|
<p className="text-xs font-medium text-gray-500">
|
|
{productsById[item.skuId]?.unitNotation || ''}
|
|
</p>
|
|
{item.slotId && (
|
|
<div className="mt-1 flex items-center gap-1 rounded border border-brand-100 bg-brand-25 px-1.5 py-0.5">
|
|
<Clock className="h-3 w-3 text-brand-600" />
|
|
<p className="text-[10px] font-bold text-brand-700">
|
|
{formatTimeRange(item.deliveryDate || new Date())}
|
|
</p>
|
|
</div>
|
|
)}
|
|
<div className="mt-1 flex items-center justify-between gap-2">
|
|
<MiniQuantifier
|
|
value={quantities[item.id] || item.quantity}
|
|
setValue={(value) => {
|
|
if (value === 0) {
|
|
removeFromCart.mutate(item.skuId)
|
|
} else {
|
|
setQuantities((prev) => ({ ...prev, [item.id]: value }))
|
|
updateCartItem.mutate({ skuId: item.skuId, quantity: value })
|
|
}
|
|
}}
|
|
step={productsById[item.skuId]?.incrementStep || 1}
|
|
/>
|
|
<p className="text-[13px] font-bold text-gray-900 sm:text-sm">
|
|
₹{(() => {
|
|
const product = productsById[item.skuId]
|
|
const basePrice = product?.price ?? 0
|
|
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice
|
|
return price * item.quantity
|
|
})()}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{index < cartItems.length - 1 && <div className="h-px w-full bg-gray-100" />}
|
|
</React.Fragment>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Footer */}
|
|
<div className="border-t border-gray-100 bg-white p-6">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-[10px] font-black uppercase tracking-widest text-gray-400">Subtotal</p>
|
|
<p className="font-display text-2xl font-extrabold text-gray-900">₹{totalCartValue}</p>
|
|
</div>
|
|
{remainingForFreeDelivery === 0 && itemCount > 0 && (
|
|
<div className="flex items-center gap-1.5 rounded-lg bg-brand-25 px-3 py-1.5">
|
|
<Package className="h-4 w-4 text-brand-600" />
|
|
<p className="text-[10px] font-black uppercase text-brand-700">Free Delivery</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
onClick={goToCart}
|
|
disabled={itemCount === 0}
|
|
className="flex w-full items-center justify-center gap-2 rounded-lg bg-brand-600 py-3.5 font-bold text-white transition-colors hover:bg-brand-700 disabled:pointer-events-none disabled:opacity-40"
|
|
>
|
|
Go to Cart
|
|
<ChevronRight className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{/* Mobile: compact bottom bar */}
|
|
<div className={`fixed bottom-18 left-3 right-3 z-[60] md:hidden ${isExpanded || isCartPage ? 'hidden' : ''}`}>
|
|
<button
|
|
onClick={() => itemCount > 0 && setIsExpanded(true)}
|
|
className="flex w-full items-center justify-between rounded-xl px-4 py-3 shadow-lg"
|
|
style={{
|
|
backgroundColor: cartBarColor,
|
|
borderColor: cartBarBorderColor,
|
|
borderWidth: 1,
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<ShoppingCart className="h-5 w-5 text-white" />
|
|
<p className="text-sm font-bold text-white">
|
|
{itemCount === 0
|
|
? isFlashDelivery
|
|
? 'No Flash Items'
|
|
: 'No Items In Cart'
|
|
: `₹${totalCartValue} · ${itemCount} ${itemCount === 1 ? 'Item' : 'Items'}`}
|
|
</p>
|
|
</div>
|
|
<div className="rounded-lg bg-white/20 px-3 py-1.5">
|
|
<p className="text-xs font-bold text-white">View</p>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Desktop: slide-over */}
|
|
<div
|
|
className={`fixed inset-0 z-[70] transition-opacity duration-300 ${
|
|
isExpanded ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
|
|
}`}
|
|
>
|
|
<div
|
|
className="absolute inset-0 bg-gray-900/40"
|
|
onClick={() => setIsExpanded(false)}
|
|
/>
|
|
<div
|
|
className={`absolute inset-y-0 right-0 flex transition-transform duration-300 ease-out ${
|
|
isExpanded ? 'translate-x-0' : 'translate-x-full'
|
|
}`}
|
|
>
|
|
{slideOver}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|