"use client"; import { useEffect, useEffectEvent, useRef, useState } from "react"; import Link from "next/link"; import { m, AnimatePresence } from "framer-motion"; import { useCart } from "@/context/CartContext"; import { formatDate } from "@/lib/format-date"; type QuickCartSheetProps = { open: boolean; onClose: () => void; /** Optional override for the product just added (used when re-opening). */ productName?: string; /** Optional message override (e.g. "Added ✓" vs default). */ justAddedLabel?: string; }; const EASE_OUT = [0.22, 0.61, 0.36, 1] as const; /** * Slide-up cart drawer that appears immediately after the buyer adds an item. * * Mobile: bottom sheet that slides up from the bottom of the screen, * full width, ~85vh tall. Drag handle at top. * Desktop: right-side drawer (420px wide) that slides in from the right. * * Contains: * - "✓ Added" confirmation * - The added item card (name, price, qty, fulfillment) * - Express checkout row (Apple Pay / Google Pay / Shop Pay) * - Primary "Checkout" CTA * - Secondary "View full cart" link * - "Continue shopping" dismiss link */ export default function QuickCartSheet({ open, onClose, productName, justAddedLabel, }: QuickCartSheetProps) { const { cart, subtotal, selectedStop, justAdded, dismissToast } = useCart(); // Lock body scroll while open (mobile only — desktop keeps scroll) useEffect(() => { if (!open) return; const prev = document.body.style.overflow; const isDesktop = window.matchMedia("(min-width: 1024px)").matches; if (!isDesktop) { document.body.style.overflow = "hidden"; } return () => { document.body.style.overflow = prev; }; }, [open]); // Close on Escape // useEffectEvent so the latest onClose is always called even though // it's no longer in the effect's dependency array. const onCloseEffect = useEffectEvent(() => { onClose(); }); useEffect(() => { if (!open) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") onCloseEffect(); } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [open]); // Mobile sheet drag state. Replaces framer-motion's `drag` prop with // pointer events so the `m` (lazy) component can be used everywhere. const dragStartYRef = useRef(null); const dragDeltaRef = useRef(0); const dragVelocityRef = useRef(0); const lastDragTimeRef = useRef(0); const lastDragYRef = useRef(0); const [dragOffset, setDragOffset] = useState(0); function handleDragPointerDown(e: React.PointerEvent) { dragStartYRef.current = e.clientY; lastDragYRef.current = e.clientY; lastDragTimeRef.current = performance.now(); (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); } function handleDragPointerMove(e: React.PointerEvent) { if (dragStartYRef.current === null) return; const delta = Math.max(0, e.clientY - dragStartYRef.current); const now = performance.now(); const dt = Math.max(1, now - lastDragTimeRef.current); dragVelocityRef.current = ((e.clientY - lastDragYRef.current) / dt) * 1000; lastDragYRef.current = e.clientY; lastDragTimeRef.current = now; dragDeltaRef.current = delta; setDragOffset(delta); } function handleDragPointerUp(e: React.PointerEvent) { if (dragStartYRef.current === null) return; (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId); if (dragDeltaRef.current > 90 || dragVelocityRef.current > 480) { onClose(); } dragStartYRef.current = null; dragDeltaRef.current = 0; dragVelocityRef.current = 0; setDragOffset(0); } const displayName = productName ?? justAdded?.name ?? cart[0]?.name ?? "Your box"; const addedAt = new Date(); const headerLabel = justAddedLabel ?? "Just added"; function handleViewCart() { dismissToast(); onClose(); } function handleCheckout() { dismissToast(); onClose(); } return ( {open && ( <> {/* Backdrop */} {/* Mobile: bottom sheet */} {/* Drag handle */}
{/* Desktop: right-side drawer */} )} ); } type CartRow = { id: string; name: string; price: string; quantity: number; fulfillment?: "pickup" | "ship"; }; type StopInfo = { id: string; city: string; state: string; date: string; time: string; location: string; brand_id: string; }; type SheetContentProps = { displayName: string; addedAt: Date; headerLabel: string; cart: CartRow[]; subtotal: number; selectedStop: StopInfo | null; onViewCart: () => void; onCheckout: () => void; onContinue: () => void; showCloseButton?: boolean; }; function SheetContent({ displayName, addedAt, headerLabel, cart, subtotal, selectedStop, onViewCart, onCheckout, onContinue, showCloseButton, }: SheetContentProps) { return ( <> {/* Header */}

{headerLabel}

{displayName}

{showCloseButton && ( )}

{addedAt.toLocaleString("en-US", { hour: "numeric", minute: "2-digit", hour12: true, })} · Today

{/* Body — scrollable */}
    {cart.map((item) => (
  • {/* Tiny product swatch */}
    {item.quantity}

    {item.name}

    {item.fulfillment === "ship" ? "Ships to door" : "Pickup at stop"}

    {item.price}

  • ))}
{/* Selected stop banner (if any) */} {selectedStop && (

Pickup at

{selectedStop.city}, {selectedStop.state}

{formatDate(selectedStop.date)} · {selectedStop.time}

)} {/* Subtotal */}
Subtotal ${subtotal.toFixed(2)}

+ tax & shipping at checkout

{/* Footer — sticky CTA + express */}
{/* Express checkout row — the buttons are visual shortcuts. /checkout auto-renders Stripe's for Apple Pay / Google Pay / Link + for card via StripeExpressCheckout. The `?express=` query is decorative. */}

Express checkout

} className="bg-black text-white hover:bg-stone-900" /> } className="bg-white text-stone-900 ring-1 ring-stone-900/10 hover:bg-stone-50" /> } className="bg-[#5A31F4] text-white hover:bg-[#4A22E0]" />
{/* Primary checkout */} Checkout · ${subtotal.toFixed(2)} {/* Secondary actions */}
View full cart
); } function ExpressButton({ href, onClick, label, icon, className, }: { href: string; onClick: () => void; label: string; icon: React.ReactNode; className: string; }) { return ( {icon} {label} ); } /* ── Express brand marks (stylized, not official) ─────────────────── */ function ApplePayMark() { return ( ); } function GooglePayMark() { return ( ); } function ShopPayMark() { return ( ); } function CornGlyph({ className }: { className?: string }) { return ( ); }