diff --git a/src/app/globals.css b/src/app/globals.css
index d268532..96e51b7 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,6 +1,15 @@
@import "tailwindcss";
@theme {
+ /* ─── Font families (Tailwind v4 @theme) ──────────────────────────
+ * Used by the editorial product pages:
+ * font-display → Fraunces (variable serif, set per-page via next/font)
+ * font-mono → JetBrains Mono (variable, set per-page via next/font)
+ * Pages that don't load these fonts fall back to the system serif/mono
+ * stacks, so the rest of the app is unaffected.
+ */
+ --font-display: var(--font-display, ui-serif, Georgia, "Times New Roman", serif);
+ --font-mono: var(--font-mono, ui-monospace, "SF Mono", "JetBrains Mono", monospace);
/* Apple-inspired dark palette — sophisticated depth */
--color-surface-50: #f5f5f7;
--color-surface-100: #e8e8ed;
diff --git a/src/app/tuxedo/products/sweet-corn-box/page.tsx b/src/app/tuxedo/products/sweet-corn-box/page.tsx
new file mode 100644
index 0000000..caa03d2
--- /dev/null
+++ b/src/app/tuxedo/products/sweet-corn-box/page.tsx
@@ -0,0 +1,116 @@
+import type { Metadata } from "next";
+import { Fraunces, JetBrains_Mono } from "next/font/google";
+import { supabase } from "@/lib/supabase";
+import { getBrandSettingsPublic } from "@/actions/brand-settings";
+import SweetCornProductPage from "@/components/storefront/SweetCornProductPage";
+
+const fraunces = Fraunces({
+ subsets: ["latin"],
+ variable: "--font-display",
+ display: "swap",
+ style: ["normal", "italic"],
+});
+
+const jetbrainsMono = JetBrains_Mono({
+ subsets: ["latin"],
+ variable: "--font-mono",
+ display: "swap",
+ weight: ["400", "500", "700"],
+});
+
+export const metadata: Metadata = {
+ // The Tuxedo layout's `template: "%s | Tuxedo Corn"` already appends the
+ // brand — keep the bare title here so the rendered
is correct.
+ title: "Fresh Sweet Corn Box – 12 Ears",
+ description:
+ "Our 12-Ear Corn Box is packed with peak-season, super-sweet Olathe Sweet corn picked that morning and rushed to your door. Hand-selected, non-GMO, and 100% sweetness guaranteed.",
+ keywords: [
+ "sweet corn",
+ "Olathe Sweet",
+ "fresh corn box",
+ "12 ears of corn",
+ "farm fresh corn",
+ "Tuxedo Corn",
+ ],
+ openGraph: {
+ title: "Fresh Sweet Corn Box – 12 Ears",
+ description:
+ "Peak-season, super-sweet Olathe Sweet corn — picked that morning, rushed to your door. 100% sweetness guaranteed.",
+ type: "website",
+ images: [
+ {
+ url: "https://images.unsplash.com/photo-1551754655-cd27e38d2076?auto=format&fit=crop&w=1200&q=80",
+ width: 1200,
+ height: 630,
+ alt: "Fresh sweet corn box — 12 ears of Olathe Sweet corn",
+ },
+ ],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: "Fresh Sweet Corn Box – 12 Ears",
+ description:
+ "Peak-season, super-sweet Olathe Sweet corn. Hand-selected, non-GMO, 100% sweetness guaranteed.",
+ },
+};
+
+const BRAND_SLUG = "tuxedo";
+
+type Brand = {
+ id: string;
+ name: string;
+};
+
+type Settings = {
+ logo_url?: string | null;
+ logo_url_dark?: string | null;
+};
+
+export default async function SweetCornBoxPage() {
+ // Fetch the brand record so we have a real brand_id to thread through
+ // the cart system. Falls back to a placeholder if Supabase is unreachable
+ // (so the page still renders in dev / disconnected previews).
+ let brandId = "00000000-0000-0000-0000-000000000000";
+ let brandName = "Tuxedo Corn";
+ let logoUrl: string | null = null;
+ let logoUrlDark: string | null = null;
+
+ try {
+ const [brandRes, settingsRes] = await Promise.all([
+ supabase.from("brands").select("id, name").eq("slug", BRAND_SLUG).single(),
+ getBrandSettingsPublic(BRAND_SLUG),
+ ]);
+ if (brandRes.data) {
+ const b = brandRes.data as Brand;
+ brandId = b.id;
+ brandName = b.name;
+ }
+ if (settingsRes.success && settingsRes.settings) {
+ const s = settingsRes.settings as Settings;
+ logoUrl = s.logo_url ?? null;
+ logoUrlDark = s.logo_url_dark ?? null;
+ }
+ } catch {
+ // ignore — fall through with defaults
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/storefront/QuickCartSheet.tsx b/src/components/storefront/QuickCartSheet.tsx
new file mode 100644
index 0000000..5ea67e2
--- /dev/null
+++ b/src/components/storefront/QuickCartSheet.tsx
@@ -0,0 +1,491 @@
+"use client";
+
+import { useEffect } from "react";
+import Link from "next/link";
+import { motion, 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
+ useEffect(() => {
+ if (!open) return;
+ function onKey(e: KeyboardEvent) {
+ if (e.key === "Escape") onClose();
+ }
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [open, onClose]);
+
+ 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 */}
+ {
+ if (info.offset.y > 90 || info.velocity.y > 480) onClose();
+ }}
+ className="lg:hidden fixed inset-x-0 bottom-0 z-[61] max-h-[88vh] rounded-t-[28px] bg-[#FAF6EA] shadow-[0_-24px_60px_-12px_rgba(26,77,46,0.35)] flex flex-col"
+ >
+ {/* 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 */}
+
+
+
+ {/* 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 */}
+
+
+ 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
+
+
+ Continue shopping
+
+
+
+ >
+ );
+}
+
+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 (
+
+
+ Pay
+
+
+
+ );
+}
+
+function GooglePayMark() {
+ return (
+
+
+ G
+
+ Pay
+
+
+
+ );
+}
+
+function ShopPayMark() {
+ return (
+
+
+ shop
+ Pay
+
+
+ );
+}
+
+function CornGlyph({ className }: { className?: string }) {
+ return (
+
+ {/* Husk leaves */}
+
+
+ {/* Cob */}
+
+ {/* Kernels (dots) */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/storefront/SweetCornProductPage.tsx b/src/components/storefront/SweetCornProductPage.tsx
new file mode 100644
index 0000000..58afe6e
--- /dev/null
+++ b/src/components/storefront/SweetCornProductPage.tsx
@@ -0,0 +1,873 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import Image from "next/image";
+import { useRouter } from "next/navigation";
+import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion";
+import { useCart } from "@/context/CartContext";
+import QuickCartSheet from "./QuickCartSheet";
+import StorefrontHeader from "./StorefrontHeader";
+import StorefrontFooter from "./StorefrontFooter";
+
+type Props = {
+ brandId: string;
+ brandName: string;
+ logoUrl?: string | null;
+ logoUrlDark?: string | null;
+};
+
+const PRODUCT = {
+ id: "sweet-corn-box-12",
+ slug: "sweet-corn-box",
+ name: "Fresh Sweet Corn Box – 12 Ears",
+ tagline: "Straight-from-the-farm sweetness in every bite.",
+ price: "$29.99",
+ priceNumeric: 29.99,
+ // Used by the existing /cart flow to filter pickup vs ship
+ type: "Pickup & Shipping",
+ imageUrl:
+ "https://images.unsplash.com/photo-1551754655-cd27e38d2076?auto=format&fit=crop&w=1600&q=80",
+ fallbackGradient:
+ "radial-gradient(ellipse at 50% 50%, #FFE08A 0%, #E5A80F 35%, #8A5A06 80%, #1A4D2E 100%)",
+};
+
+const EASE_OUT = [0.22, 0.61, 0.36, 1] as const;
+
+export default function SweetCornProductPage({ brandId, brandName, logoUrl, logoUrlDark }: Props) {
+ const router = useRouter();
+ const { addToCart, buyNow } = useCart();
+ const [sheetOpen, setSheetOpen] = useState(false);
+ const [justAdded, setJustAdded] = useState(false);
+ const [buyNowPulse, setBuyNowPulse] = useState(false);
+ const [imgError, setImgError] = useState(false);
+ const [imgLoaded, setImgLoaded] = useState(false);
+
+ // Parallax for the hero corn ears floating decoration
+ const { scrollY } = useScroll();
+ const floatY1 = useTransform(scrollY, [0, 800], [0, -120]);
+ const floatY2 = useTransform(scrollY, [0, 800], [0, 80]);
+
+ // Auto-dismiss the "Added" pulse after a short delay
+ useEffect(() => {
+ if (!justAdded) return;
+ const t = setTimeout(() => setJustAdded(false), 1800);
+ return () => clearTimeout(t);
+ }, [justAdded]);
+
+ function buildBaseItem() {
+ return {
+ id: PRODUCT.id,
+ name: PRODUCT.name,
+ price: PRODUCT.price,
+ brand_id: brandId,
+ brand_slug: "tuxedo",
+ is_taxable: true,
+ pickup_type: "scheduled_stop" as const,
+ description: PRODUCT.tagline,
+ };
+ }
+
+ function handleAddToCart() {
+ addToCart(buildBaseItem(), "pickup");
+ setJustAdded(true);
+ // Tiny delay so the user sees the "Added" state before the sheet slides up
+ setTimeout(() => setSheetOpen(true), 280);
+ }
+
+ function handleBuyNow() {
+ buyNow(buildBaseItem(), "pickup");
+ setBuyNowPulse(true);
+ setTimeout(() => router.push("/checkout"), 140);
+ }
+
+ return (
+
+
+
+
+ {/* ── HERO ─────────────────────────────────────────────────────── */}
+
+ {/* Subtle paper grain */}
+ \")",
+ }}
+ />
+ {/* Decorative giant "12" in the background */}
+
+ 12
+
+
+ {/* Floating decorative corn ears */}
+
+
+
+
+
+
+
+
+ {/* Breadcrumb */}
+
+
+ Tuxedo Corn
+
+
+
+ Shop
+
+
+ Sweet Corn Box
+
+
+
+ {/* LEFT — Visual */}
+
+
+ {/* Gradient fallback (always behind, visible until image loads or on error) */}
+
+ {/* Decorative corn cobs inside the image frame */}
+
+
+
+ {!imgError && (
+ setImgLoaded(true)}
+ onError={() => setImgError(true)}
+ />
+ )}
+ {/* Bottom scrim for badge legibility */}
+
+ {/* Tilted corner stamp */}
+
+
+
+ Peak
+
+
+ SEASON
+
+
+ 2026
+
+
+
+ {/* Right corner "12 ears" badge */}
+
+
+
+ Box contains
+
+
+ 12 ears
+
+
+
+
+
+ {/* Thumbnails row (decorative — same image) */}
+
+ {[0, 1, 2].map((i) => (
+
+
+
+
+ ))}
+
+
+ Lot 04-26 · In season
+
+
+
+
+ {/* RIGHT — Copy + Buy Box */}
+
+
+
+
+
+ Ships fresh
+
+
+ Same-day harvest when possible
+
+
+
+
+ Sweet Corn
+ Box
+
+
+
+ {PRODUCT.tagline}
+
+
+
+ Our 12-Ear Corn Box is packed with peak-season, super-sweet corn picked
+ that morning and rushed to your door. Perfect for BBQs, family dinners,
+ or freezing for winter.
+
+
+ {/* Why our corn (inline) */}
+
+
+ {/* Price + buy box (desktop sticky) */}
+
+
+
+
+ One box · 12 ears
+
+
+ {PRODUCT.price}
+
+
+ ≈ $2.50 / ear · tax included
+
+
+
+
+ In stock
+
+
+ 240 boxes
+
+
ready this week
+
+
+
+ {/* Quantity */}
+
+
+ Qty
+
+
+ {
+ /* single-product page — qty is always 1 for now */
+ }}
+ >
+ −
+
+ 1
+
+ +
+
+
+
+ Ships in a compostable cooler
+
+
+
+ {/* Add to Cart (primary) */}
+
+
+ {justAdded ? (
+
+
+
+
+ Added to cart
+
+ ) : (
+
+ Add Box to Cart · {PRODUCT.price}
+
+
+
+
+ )}
+
+
+
+ {/* Buy Now (secondary, urgent orange) */}
+
+
+
+
+
+ Quick Buy — Ships Today
+
+ 1-click
+
+
+
+
+ {/* Express row (visual only) */}
+
+
+
+
+
+
+
+ One click. One box. Farm-fresh delivered.
+
+
+
+
+
+
+
+
+ {/* ── GUARANTEE / TRUST STRIP ─────────────────────────────────────── */}
+
+
+
+
+ }
+ eyebrow="Ships Fresh"
+ title="Same-day harvest when possible"
+ body="We pick before the heat steals a single calorie of sweetness. By the time your order arrives, the corn was still in the field that morning."
+ />
+ }
+ eyebrow="Satisfaction"
+ title="100% Sweetness Guarantee"
+ body="If it’s not the sweetest corn you’ve ever had, we’ll replace it. No questions, no forms, no fuss."
+ accent
+ />
+ }
+ eyebrow="Versatile"
+ title="Great for every summer table"
+ body="Grilling, boiling, freezing, or corn-on-the-cob parties — twelve ears is exactly the right amount for a family of four to share."
+ />
+
+
+
+
+ {/* ── FARM STORY ─────────────────────────────────────────────────── */}
+
+
+
+
+
+ Field notes · Vol. IV
+
+
+ Picked by hand, stayed sweet for days.
+
+
+ We hand-select every ear for size and quality. No machine knows when a
+ cob is at peak sugar content. That’s why your box is a little heavier
+ than you expect — and a whole lot sweeter than any corn you’ve bought
+ from a grocery store shelf.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Abstract corn field lines */}
+
+
+
+
+
+
+
+
+ {/* Floating cob clusters */}
+
+
+
+ {/* Sun stamp */}
+
+
+ HAND
+
+ PICKED
+
+
+ {/* Bottom data strip */}
+
+
+ FIELD · 04-N
+
+
+ BRIX · 24.2°
+
+
+ PICK · 06:14
+
+
+
+
+
+
+
+
+ {/* ── FAQ MICRO ──────────────────────────────────────────────────── */}
+
+
+
+ Buyer’s notes
+
+
+ Questions, answered.
+
+
+ {FAQS.map((faq) => (
+
+ ))}
+
+
+
+
+ {/* ── FINAL CTA STRIP (desktop) ─────────────────────────────────── */}
+
+
+
+
+
+ Ready when you are
+
+
+ Twelve ears. One click.
+
+
+ Free shipping on orders over $40. Cancel anytime before harvest.
+
+
+
+
+ Add to cart — {PRODUCT.price}
+
+
+ Quick Buy
+
+
+
+
+
+
+ {/* ── STICKY MOBILE BOTTOM CTA BAR ──────────────────────────────────── */}
+
+
+
+
+ 12-Ear Box
+
+
+ {PRODUCT.price}
+
+
+
+ Quick Buy
+
+
+ Add · {PRODUCT.price}
+
+
+ {/* Safe-area spacer for iOS */}
+
+
+
+ {/* Bottom padding so sticky bar doesn't cover last content on mobile */}
+
+
+
+
+
setSheetOpen(false)}
+ productName={PRODUCT.name}
+ />
+
+ );
+}
+
+/* ── Sub-components ─────────────────────────────────────────────── */
+
+function TrustColumn({
+ icon,
+ eyebrow,
+ title,
+ body,
+ accent,
+}: {
+ icon: React.ReactNode;
+ eyebrow: string;
+ title: string;
+ body: string;
+ accent?: boolean;
+}) {
+ return (
+
+
+ {icon}
+
+
+ {eyebrow}
+
+
+ {title}
+
+
{body}
+
+ );
+}
+
+function Stat({ number, label }: { number: string; label: string }) {
+ return (
+
+
+ {number}
+
+
+ {label}
+
+
+ );
+}
+
+function FAQItem({ q, a }: { q: string; a: string }) {
+ const [open, setOpen] = useState(false);
+ return (
+
+
setOpen((v) => !v)}
+ className="w-full flex items-center justify-between gap-4 text-left group"
+ >
+
+ {q}
+
+
+
+
+
+
+
+
+ {open && (
+
+ {a}
+
+ )}
+
+
+ );
+}
+
+function Chevron() {
+ return (
+
+
+
+ );
+}
+
+function ExpressChip({ label }: { label: string }) {
+ return (
+
+ {label}
+
+ );
+}
+
+function TruckIcon() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+function ShieldIcon() {
+ return (
+
+
+
+
+ );
+}
+
+function SparkleIcon() {
+ return (
+
+
+
+ );
+}
+
+function CornIllustration({ className }: { className?: string }) {
+ return (
+
+ {/* Husk leaves */}
+
+
+
+ {/* Cob */}
+
+ {/* Kernels */}
+
+ {Array.from({ length: 8 }).map((_, row) =>
+ Array.from({ length: 5 }).map((_, col) => {
+ const x = 50 - 14 + col * 7 + (row % 2 ? 3.5 : 0);
+ const y = 55 + row * 6;
+ return ;
+ })
+ )}
+
+ {/* Silk at top */}
+
+
+
+
+
+
+ );
+}
+
+function FloatingCorn({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/* ── Static data ─────────────────────────────────────────────── */
+
+const WHY_CORN = [
+ { title: "Naturally grown & non-GMO", body: "no lab has touched this seed" },
+ { title: "Harvested at peak sugar content", body: "before the heat can steal a calorie" },
+ { title: "Hand-selected for size & quality", body: "the only machine we trust is a cooler" },
+ { title: "Stays sweet for days in the fridge", body: "and freezes beautifully for winter" },
+];
+
+const FAQS = [
+ {
+ q: "How fast will my box ship?",
+ a: "Same-day harvest when the weather cooperates. You’ll get a tracking link the moment the cooler leaves the farm — typically within 24 hours of your order.",
+ },
+ {
+ q: "Can I pick up at a local stop instead?",
+ a: "Yes — at checkout, choose pickup and select a stop near you. We deliver to scheduled stops in Colorado, Utah, New Mexico, and Arizona through the summer.",
+ },
+ {
+ q: "What if my corn isn’t the sweetest I’ve ever had?",
+ a: "We replace it. Send a photo to hello@tuxedocorn.com and we’ll send a fresh box the next morning. No forms, no questions.",
+ },
+ {
+ q: "How long does it stay fresh?",
+ a: "Seven days in the fridge with the husks on. Three months in the freezer if you blanch it for three minutes first.",
+ },
+];
diff --git a/src/context/CartContext.tsx b/src/context/CartContext.tsx
index 5602615..f0552c3 100644
--- a/src/context/CartContext.tsx
+++ b/src/context/CartContext.tsx
@@ -30,6 +30,12 @@ type CartContextType = {
cartRestored: boolean; // true when server cart was loaded (for toast)
setSelectedStop: (stop: StopInfo | null) => void;
addToCart: (item: Omit, fulfillment?: "pickup" | "ship") => void;
+ /**
+ * Replaces the cart with a single item (clears other items + selected stop)
+ * and returns the resulting item so the caller can navigate to /checkout
+ * for a true 1-tap "Buy Now" flow.
+ */
+ buyNow: (item: Omit, fulfillment?: "pickup" | "ship") => CartItem;
increaseQuantity: (id: string) => void;
decreaseQuantity: (id: string) => void;
removeFromCart: (id: string) => void;
@@ -201,6 +207,27 @@ export function CartProvider({ children }: { children: ReactNode }) {
});
}
+ /**
+ * Replaces the entire cart with this single item. Use for "Buy Now" /
+ * "Quick Buy" flows where the buyer wants to skip the cart step and go
+ * straight to checkout. The returned CartItem reflects the final state
+ * (existing items of the same product are merged by quantity).
+ */
+ function buyNow(item: Omit, fulfillment?: "pickup" | "ship"): CartItem {
+ // Always clear selected stop on a buy-now — the buyer is bypassing the
+ // standard stop-picker flow and will pick a stop at checkout.
+ setSelectedStop(null);
+
+ const finalItem: CartItem = {
+ ...item,
+ quantity: 1,
+ fulfillment: fulfillment ?? "pickup",
+ };
+ setCart([finalItem]);
+ setJustAdded(finalItem);
+ return finalItem;
+ }
+
function increaseQuantity(id: string) {
setCart((prev) =>
prev.map((item) => (item.id === id ? { ...item, quantity: item.quantity + 1 } : item))
@@ -291,6 +318,7 @@ export function CartProvider({ children }: { children: ReactNode }) {
cartRestored: showRestoredToast,
setSelectedStop: setSelectedStop,
addToCart,
+ buyNow,
increaseQuantity,
decreaseQuantity,
removeFromCart,