freshyo/apps/web-ui/src/components/FloatingCartBar.tsx
2026-09-12 09:06:33 +05:30

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>
</>
)
}