feat(storefront): dedicated Sweet Corn Box product page with 2-3 click order flow

- New route /tuxedo/products/sweet-corn-box with editorial Fraunces/JetBrains
  Mono typography, paper-grain hero, tilted 'Peak Season 2026' stamp, and
  sticky buy-box (desktop) + sticky mobile bottom bar with 48px+ tap targets
- QuickCartSheet slide-up drawer: mobile bottom sheet (spring + drag-to-close)
  vs desktop right drawer, with Apple Pay / Google Pay / Shop Pay express row
  and one-page 'Checkout' CTA — enables the user's specified 2-3 click flow
- CartContext.buyNow() — single-item cart replace + clear stop for 1-tap
  'Quick Buy' that pushes directly to /checkout
- 4-feature check list, 100% Sweetness Guarantee, FAQ accordion, dark final
  CTA strip, and proper SEO/Open Graph meta
This commit is contained in:
2026-06-04 17:59:36 +00:00
parent 66c6f45efc
commit b2aa53f274
5 changed files with 1517 additions and 0 deletions
+9
View File
@@ -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;
@@ -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 <title> 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 (
<div
className={`${fraunces.variable} ${jetbrainsMono.variable} font-sans`}
style={{
// The product page uses editorial typography: a display serif
// (Fraunces) for headlines and a mono (JetBrains Mono) for data
// stamps. Body copy falls back to the global SF Pro stack.
["--font-display" as never]: fraunces.style.fontFamily,
["--font-mono" as never]: jetbrainsMono.style.fontFamily,
}}
>
<SweetCornProductPage
brandId={brandId}
brandName={brandName}
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
/>
</div>
);
}
@@ -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 (
<AnimatePresence>
{open && (
<>
{/* Backdrop */}
<motion.button
type="button"
aria-label="Close cart"
onClick={onClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25, ease: EASE_OUT }}
className="fixed inset-0 z-[60] bg-stone-950/55 backdrop-blur-[3px] cursor-default"
/>
{/* Mobile: bottom sheet */}
<motion.div
role="dialog"
aria-modal="true"
aria-label="Quick cart"
initial={{ y: "100%" }}
animate={{ y: 0 }}
exit={{ y: "100%" }}
transition={{ type: "spring", stiffness: 380, damping: 38, mass: 0.9 }}
drag="y"
dragConstraints={{ top: 0, bottom: 0 }}
dragElastic={{ top: 0, bottom: 0.5 }}
onDragEnd={(_, info) => {
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 */}
<div className="flex justify-center pt-3 pb-1 cursor-grab active:cursor-grabbing">
<div className="h-1.5 w-12 rounded-full bg-stone-300/70" />
</div>
<SheetContent
displayName={displayName}
addedAt={addedAt}
headerLabel={headerLabel}
cart={cart}
subtotal={subtotal}
selectedStop={selectedStop}
onViewCart={handleViewCart}
onCheckout={handleCheckout}
onContinue={onClose}
/>
</motion.div>
{/* Desktop: right-side drawer */}
<motion.aside
role="dialog"
aria-modal="true"
aria-label="Quick cart"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "spring", stiffness: 320, damping: 36, mass: 0.9 }}
className="hidden lg:flex fixed inset-y-0 right-0 z-[61] w-full sm:w-[420px] xl:w-[460px] bg-[#FAF6EA] shadow-[-24px_0_60px_-12px_rgba(26,77,46,0.35)] flex-col"
>
<SheetContent
displayName={displayName}
addedAt={addedAt}
headerLabel={headerLabel}
cart={cart}
subtotal={subtotal}
selectedStop={selectedStop}
onViewCart={handleViewCart}
onCheckout={handleCheckout}
onContinue={onClose}
showCloseButton
/>
</motion.aside>
</>
)}
</AnimatePresence>
);
}
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 */}
<div className="relative px-6 sm:px-8 pt-2 pb-5 border-b border-stone-900/10">
<div className="flex items-center gap-3">
<motion.div
initial={{ scale: 0, rotate: -45 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: "spring", stiffness: 500, damping: 18, delay: 0.05 }}
className="flex h-10 w-10 items-center justify-center rounded-full bg-[#1A4D2E] text-white shadow-[0_4px_14px_rgba(26,77,46,0.4)]"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</motion.div>
<div className="flex-1 min-w-0">
<p className="text-[10px] font-bold uppercase tracking-[0.22em] text-[#1A4D2E]/70">
{headerLabel}
</p>
<h2 className="font-display text-[22px] sm:text-[24px] font-bold leading-[1.1] text-stone-950 truncate">
{displayName}
</h2>
</div>
{showCloseButton && (
<button
onClick={onContinue}
aria-label="Close"
className="flex h-9 w-9 items-center justify-center rounded-full text-stone-500 hover:bg-stone-900/5 hover:text-stone-900 transition-colors"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
<p className="mt-3 font-mono text-[10px] uppercase tracking-widest text-stone-500">
{addedAt.toLocaleString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
})} · Today
</p>
</div>
{/* Body — scrollable */}
<div className="flex-1 overflow-y-auto px-6 sm:px-8 py-5">
<ul className="space-y-3">
{cart.map((item) => (
<li
key={item.id}
className="flex items-start gap-4 rounded-2xl bg-white/70 ring-1 ring-stone-900/8 p-4"
>
{/* Tiny product swatch */}
<div className="relative h-14 w-14 shrink-0 overflow-hidden rounded-xl bg-gradient-to-br from-[#FFD86A] via-[#F0B81C] to-[#A47A0B] ring-1 ring-stone-900/10">
<CornGlyph className="absolute inset-0 m-auto h-9 w-9 text-[#1A4D2E]/85" />
<span className="absolute -bottom-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-[#1A4D2E] text-[10px] font-bold text-white shadow-sm">
{item.quantity}
</span>
</div>
<div className="min-w-0 flex-1">
<p className="font-semibold text-stone-950 text-[15px] leading-tight truncate">
{item.name}
</p>
<p className="mt-0.5 text-[11px] font-mono uppercase tracking-widest text-stone-500">
{item.fulfillment === "ship" ? "Ships to door" : "Pickup at stop"}
</p>
<p className="mt-1.5 text-[15px] font-bold text-stone-950">
{item.price}
</p>
</div>
</li>
))}
</ul>
{/* Selected stop banner (if any) */}
{selectedStop && (
<div className="mt-4 flex items-center gap-3 rounded-2xl border border-dashed border-[#1A4D2E]/40 bg-[#1A4D2E]/5 p-3.5">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-[#1A4D2E] text-white">
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M17.657 16.657L13.414 20.9a2 2 0 01-2.828 0l-4.243-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div className="min-w-0 flex-1">
<p className="text-[10px] font-mono uppercase tracking-widest text-[#1A4D2E]/70">Pickup at</p>
<p className="text-sm font-semibold text-stone-950 truncate">
{selectedStop.city}, {selectedStop.state}
</p>
<p className="text-[11px] text-stone-600">
{formatDate(selectedStop.date)} · {selectedStop.time}
</p>
</div>
</div>
)}
{/* Subtotal */}
<div className="mt-5 flex items-baseline justify-between border-t border-stone-900/10 pt-4">
<span className="font-mono text-[10px] uppercase tracking-widest text-stone-500">
Subtotal
</span>
<span className="font-display text-2xl font-bold text-stone-950">
${subtotal.toFixed(2)}
</span>
</div>
<p className="mt-1.5 text-[11px] text-stone-500 text-right">
+ tax & shipping at checkout
</p>
</div>
{/* Footer — sticky CTA + express */}
<div className="border-t border-stone-900/10 bg-[#F5EFD9] px-6 sm:px-8 pt-5 pb-7 space-y-3">
{/* Express checkout row */}
<div>
<p className="font-mono text-[9px] uppercase tracking-[0.22em] text-stone-500 mb-2 text-center">
Express checkout
</p>
<div className="grid grid-cols-3 gap-2">
<ExpressButton
href="/checkout?express=apple_pay"
onClick={onCheckout}
label="Pay"
icon={<ApplePayMark />}
className="bg-black text-white hover:bg-stone-900"
/>
<ExpressButton
href="/checkout?express=google_pay"
onClick={onCheckout}
label="Pay"
icon={<GooglePayMark />}
className="bg-white text-stone-900 ring-1 ring-stone-900/10 hover:bg-stone-50"
/>
<ExpressButton
href="/checkout?express=shop_pay"
onClick={onCheckout}
label="Shop"
icon={<ShopPayMark />}
className="bg-[#5A31F4] text-white hover:bg-[#4A22E0]"
/>
</div>
</div>
{/* Primary checkout */}
<Link
href="/checkout"
onClick={onCheckout}
className="group flex h-14 w-full items-center justify-center gap-2 rounded-2xl bg-[#1A4D2E] text-white font-bold tracking-wide shadow-[0_10px_24px_-8px_rgba(26,77,46,0.5)] hover:bg-[#143C24] active:scale-[0.99] transition-all"
>
Checkout · ${subtotal.toFixed(2)}
<svg className="h-5 w-5 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</Link>
{/* Secondary actions */}
<div className="flex items-center justify-between pt-1 text-[13px]">
<Link
href="/cart"
onClick={onViewCart}
className="font-semibold text-stone-600 hover:text-stone-950 underline-offset-4 hover:underline transition-colors"
>
View full cart
</Link>
<button
onClick={onContinue}
className="font-semibold text-stone-600 hover:text-stone-950 underline-offset-4 hover:underline transition-colors"
>
Continue shopping
</button>
</div>
</div>
</>
);
}
function ExpressButton({
href,
onClick,
label,
icon,
className,
}: {
href: string;
onClick: () => void;
label: string;
icon: React.ReactNode;
className: string;
}) {
return (
<Link
href={href}
onClick={onClick}
className={`group flex h-12 items-center justify-center gap-1.5 rounded-xl font-bold text-[13px] tracking-tight transition-all active:scale-[0.97] ${className}`}
>
{icon}
<span>{label}</span>
</Link>
);
}
/* ── Express brand marks (stylized, not official) ─────────────────── */
function ApplePayMark() {
return (
<svg viewBox="0 0 28 14" className="h-4 w-auto" aria-hidden="true">
<text x="0" y="11" fontFamily="system-ui, -apple-system, sans-serif" fontWeight="700" fontSize="12" fill="currentColor" letterSpacing="-0.5">
Pay
</text>
<path
d="M5.5 3.2c.5-.6.8-1.4.7-2.2-.7 0-1.5.5-2 1.1-.4.5-.8 1.3-.7 2.1.8 0 1.6-.4 2-1zm.7.9c-1.1 0-2 .6-2.6.6-.6 0-1.4-.6-2.3-.6-1.2 0-2.3.7-2.9 1.8C-2.9 8.1-2 11 1 11c.8 0 1.4-.5 2.3-.5s1.4.5 2.3.5c1 0 1.6-.8 2.2-1.6.7-1 .9-1.9.9-2-.1 0-1.7-.6-1.7-2.4 0-1.5 1.2-2.2 1.3-2.2-.7-1.1-1.9-1.2-2.3-1.2z"
transform="translate(0, -3) scale(0.5)"
fill="currentColor"
/>
</svg>
);
}
function GooglePayMark() {
return (
<svg viewBox="0 0 36 16" className="h-3.5 w-auto" aria-hidden="true">
<text
x="0"
y="12"
fontFamily="system-ui, -apple-system, sans-serif"
fontWeight="600"
fontSize="11"
fill="currentColor"
letterSpacing="-0.3"
>
G
<tspan dx="0.5" fontWeight="400" fontStyle="italic">
Pay
</tspan>
</text>
</svg>
);
}
function ShopPayMark() {
return (
<svg viewBox="0 0 38 14" className="h-3.5 w-auto" aria-hidden="true">
<text
x="0"
y="11"
fontFamily="system-ui, -apple-system, sans-serif"
fontWeight="700"
fontSize="11"
fill="currentColor"
letterSpacing="-0.4"
>
shop
<tspan fontWeight="400">Pay</tspan>
</text>
</svg>
);
}
function CornGlyph({ className }: { className?: string }) {
return (
<svg viewBox="0 0 32 32" className={className} aria-hidden="true">
{/* Husk leaves */}
<path
d="M9 16c-2-4-2-9 0-12 1 3 3 5 5 6"
fill="currentColor"
opacity="0.4"
/>
<path
d="M23 16c2-4 2-9 0-12-1 3-3 5-5 6"
fill="currentColor"
opacity="0.4"
/>
{/* Cob */}
<ellipse cx="16" cy="17" rx="6" ry="9" fill="currentColor" />
{/* Kernels (dots) */}
<g fill="#FAF6EA" opacity="0.7">
<circle cx="14" cy="12" r="0.8" />
<circle cx="18" cy="12" r="0.8" />
<circle cx="13" cy="15" r="0.8" />
<circle cx="16" cy="14" r="0.8" />
<circle cx="19" cy="15" r="0.8" />
<circle cx="14" cy="18" r="0.8" />
<circle cx="18" cy="18" r="0.8" />
<circle cx="16" cy="20" r="0.8" />
<circle cx="14" cy="22" r="0.8" />
<circle cx="18" cy="22" r="0.8" />
</g>
</svg>
);
}
@@ -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 (
<div className="min-h-screen bg-[#FAF6EA] text-stone-950">
<StorefrontHeader
brandName={brandName}
brandSlug="tuxedo"
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
showWholesaleLink
brandAccent="green"
/>
<main>
{/* ── HERO ─────────────────────────────────────────────────────── */}
<section className="relative overflow-hidden bg-[#FAF6EA]">
{/* Subtle paper grain */}
<div
aria-hidden
className="absolute inset-0 pointer-events-none opacity-[0.035] mix-blend-multiply"
style={{
backgroundImage:
"url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='220' height='220'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%' height='100%' filter='url(%23n)' opacity='0.7'/></svg>\")",
}}
/>
{/* Decorative giant "12" in the background */}
<div
aria-hidden
className="absolute -top-20 -right-10 lg:-right-24 font-display text-[260px] lg:text-[420px] font-black leading-none text-[#E5A80F]/8 select-none pointer-events-none"
style={{ color: "rgba(229, 168, 15, 0.08)" }}
>
12
</div>
{/* Floating decorative corn ears */}
<motion.div
aria-hidden
style={{ y: floatY1 }}
className="absolute top-32 right-[6%] hidden lg:block"
>
<FloatingCorn className="w-16 text-[#1A4D2E]/15 rotate-12" />
</motion.div>
<motion.div
aria-hidden
style={{ y: floatY2 }}
className="absolute bottom-20 left-[4%] hidden lg:block"
>
<FloatingCorn className="w-12 text-[#E5A80F]/25 -rotate-12" />
</motion.div>
<div className="relative mx-auto max-w-6xl px-5 sm:px-6 lg:px-8 pt-10 sm:pt-14 lg:pt-20 pb-8 lg:pb-12">
{/* Breadcrumb */}
<nav className="flex items-center gap-1.5 text-[11px] font-mono uppercase tracking-widest text-stone-500">
<Link href="/tuxedo" className="hover:text-stone-900 transition-colors">
Tuxedo Corn
</Link>
<Chevron />
<Link href="/tuxedo#products" className="hover:text-stone-900 transition-colors">
Shop
</Link>
<Chevron />
<span className="text-stone-900">Sweet Corn Box</span>
</nav>
<div className="mt-8 lg:mt-12 grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-14 items-start">
{/* LEFT — Visual */}
<div className="lg:col-span-7 relative">
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, ease: EASE_OUT }}
className="relative aspect-[4/3] sm:aspect-[5/4] lg:aspect-[6/5] w-full overflow-hidden rounded-[28px] ring-1 ring-stone-900/10 shadow-[0_30px_60px_-20px_rgba(26,77,46,0.35)]"
>
{/* Gradient fallback (always behind, visible until image loads or on error) */}
<div
className="absolute inset-0"
style={{ background: PRODUCT.fallbackGradient }}
/>
{/* Decorative corn cobs inside the image frame */}
<div className="absolute inset-0 flex items-center justify-center">
<CornIllustration className="w-3/5 max-w-[420px] text-[#1A4D2E]/55" />
</div>
{!imgError && (
<Image
src={PRODUCT.imageUrl}
alt="Freshly harvested Olathe Sweet sweet corn"
fill
priority
sizes="(min-width: 1024px) 58vw, 100vw"
className={`object-cover transition-opacity duration-700 ${
imgLoaded ? "opacity-100" : "opacity-0"
}`}
onLoad={() => setImgLoaded(true)}
onError={() => setImgError(true)}
/>
)}
{/* Bottom scrim for badge legibility */}
<div className="absolute inset-x-0 bottom-0 h-32 bg-gradient-to-t from-black/35 to-transparent pointer-events-none" />
{/* Tilted corner stamp */}
<motion.div
initial={{ opacity: 0, rotate: 0, scale: 0.6 }}
animate={{ opacity: 1, rotate: -8, scale: 1 }}
transition={{ delay: 0.55, type: "spring", stiffness: 200, damping: 14 }}
className="absolute top-5 left-5 lg:top-7 lg:left-7 origin-top-left"
>
<div className="flex h-[88px] w-[88px] lg:h-[100px] lg:w-[100px] flex-col items-center justify-center rounded-full border-2 border-dashed border-white/85 bg-[#1A4D2E]/90 text-white text-center shadow-lg backdrop-blur-sm">
<span className="font-mono text-[8px] uppercase tracking-[0.18em] text-white/70">
Peak
</span>
<span className="font-display text-[20px] lg:text-[22px] font-black leading-none">
SEASON
</span>
<span className="font-mono text-[8px] uppercase tracking-[0.18em] text-white/70">
2026
</span>
</div>
</motion.div>
{/* Right corner "12 ears" badge */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4, duration: 0.5 }}
className="absolute bottom-5 right-5 lg:bottom-7 lg:right-7"
>
<div className="rounded-2xl bg-white/95 px-3.5 py-2 ring-1 ring-stone-900/10 shadow-lg backdrop-blur-sm">
<p className="font-mono text-[9px] uppercase tracking-widest text-stone-500 leading-none">
Box contains
</p>
<p className="font-display text-[22px] font-black text-stone-950 leading-none mt-0.5">
12 ears
</p>
</div>
</motion.div>
</motion.div>
{/* Thumbnails row (decorative — same image) */}
<div className="mt-4 flex gap-3">
{[0, 1, 2].map((i) => (
<button
key={i}
aria-label={`View image ${i + 1}`}
className={`relative aspect-square w-16 sm:w-20 overflow-hidden rounded-xl ring-2 transition-all ${
i === 0
? "ring-[#1A4D2E]"
: "ring-stone-900/10 hover:ring-stone-900/30"
}`}
>
<div
className="absolute inset-0"
style={{ background: PRODUCT.fallbackGradient }}
/>
<CornIllustration className="absolute inset-0 m-auto w-3/5 text-[#1A4D2E]/55" />
</button>
))}
<div className="ml-auto hidden sm:flex items-center gap-1.5 rounded-xl bg-stone-900/5 px-3 py-2 text-[10px] font-mono uppercase tracking-widest text-stone-600">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" />
Lot 04-26 · In season
</div>
</div>
</div>
{/* RIGHT — Copy + Buy Box */}
<div className="lg:col-span-5">
<motion.div
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.7, delay: 0.1, ease: EASE_OUT }}
>
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1.5 rounded-full bg-[#1A4D2E] px-3 py-1 text-[10px] font-bold uppercase tracking-widest text-white">
<span className="h-1.5 w-1.5 rounded-full bg-[#FCD34D] animate-pulse" />
Ships fresh
</span>
<span className="font-mono text-[10px] uppercase tracking-widest text-stone-500">
Same-day harvest when possible
</span>
</div>
<h1 className="mt-5 font-display text-[44px] sm:text-[52px] lg:text-[60px] font-black leading-[0.95] tracking-[-0.02em] text-stone-950">
<span className="block">Sweet Corn</span>
<span className="block italic font-normal text-[#1A4D2E]">Box</span>
</h1>
<p className="mt-4 font-display text-[19px] sm:text-[21px] leading-[1.35] text-stone-700 italic">
{PRODUCT.tagline}
</p>
<p className="mt-5 text-[15px] leading-relaxed text-stone-700 max-w-prose">
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.
</p>
{/* Why our corn (inline) */}
<ul className="mt-6 space-y-2.5">
{WHY_CORN.map((item) => (
<li key={item.title} className="flex items-start gap-3">
<span className="mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[#1A4D2E] text-white">
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
</span>
<div>
<span className="font-bold text-stone-950 text-[14px]">
{item.title}
</span>
<span className="text-stone-600 text-[14px]">
{item.body && <> {item.body}</>}
</span>
</div>
</li>
))}
</ul>
{/* Price + buy box (desktop sticky) */}
<div
id="buy-box"
className="mt-8 lg:mt-10 rounded-3xl bg-white ring-1 ring-stone-900/10 p-6 sm:p-7 shadow-[0_20px_40px_-12px_rgba(26,77,46,0.18)]"
>
<div className="flex items-end justify-between gap-4">
<div>
<p className="font-mono text-[10px] uppercase tracking-widest text-stone-500">
One box · 12 ears
</p>
<p className="mt-1 font-display text-[44px] font-black leading-none tracking-tight text-stone-950">
{PRODUCT.price}
</p>
<p className="mt-1 text-[11px] text-stone-500">
$2.50 / ear · tax included
</p>
</div>
<div className="text-right">
<p className="font-mono text-[10px] uppercase tracking-widest text-stone-500">
In stock
</p>
<p className="mt-1 font-display text-[20px] font-bold text-[#1A4D2E]">
240 boxes
</p>
<p className="text-[10px] text-stone-500">ready this week</p>
</div>
</div>
{/* Quantity */}
<div className="mt-5 flex items-center gap-3">
<span className="font-mono text-[10px] uppercase tracking-widest text-stone-500">
Qty
</span>
<div className="inline-flex items-center rounded-full ring-1 ring-stone-900/15 bg-stone-50">
<button
aria-label="Decrease quantity"
className="h-10 w-10 flex items-center justify-center text-stone-700 hover:text-stone-950 hover:bg-stone-100 rounded-l-full transition-colors"
onClick={() => {
/* single-product page — qty is always 1 for now */
}}
>
</button>
<span className="w-10 text-center font-bold text-stone-950">1</span>
<button
aria-label="Increase quantity"
className="h-10 w-10 flex items-center justify-center text-stone-700 hover:text-stone-950 hover:bg-stone-100 rounded-r-full transition-colors"
>
+
</button>
</div>
<span className="ml-auto text-[12px] text-stone-500 hidden sm:inline">
Ships in a compostable cooler
</span>
</div>
{/* Add to Cart (primary) */}
<motion.button
onClick={handleAddToCart}
whileTap={{ scale: 0.98 }}
className={`mt-5 relative w-full overflow-hidden rounded-2xl h-14 sm:h-16 font-bold text-[15px] sm:text-[16px] tracking-wide transition-all ${
justAdded
? "bg-[#1A4D2E] text-white"
: "bg-stone-950 text-white hover:bg-stone-800"
}`}
>
<AnimatePresence mode="wait">
{justAdded ? (
<motion.span
key="added"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.18 }}
className="flex items-center justify-center gap-2"
>
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={3}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
Added to cart
</motion.span>
) : (
<motion.span
key="add"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.18 }}
className="flex items-center justify-center gap-2"
>
Add Box to Cart · {PRODUCT.price}
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
</motion.span>
)}
</AnimatePresence>
</motion.button>
{/* Buy Now (secondary, urgent orange) */}
<motion.button
onClick={handleBuyNow}
whileTap={{ scale: 0.98 }}
className={`mt-3 group relative w-full overflow-hidden rounded-2xl h-12 sm:h-[52px] font-bold text-[14px] tracking-wide transition-all ${
buyNowPulse
? "bg-[#D97706] text-white"
: "bg-[#FFF1D6] text-[#9A4A06] ring-1 ring-[#E5A80F]/40 hover:bg-[#FFE7B0]"
}`}
>
<span className="flex items-center justify-center gap-2">
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" />
</svg>
Quick Buy Ships Today
<span className="ml-1 font-mono text-[10px] font-medium uppercase tracking-widest opacity-70 hidden sm:inline">
1-click
</span>
</span>
</motion.button>
{/* Express row (visual only) */}
<div className="mt-4 grid grid-cols-3 gap-2">
<ExpressChip label="Apple Pay" />
<ExpressChip label="Google Pay" />
<ExpressChip label="Shop Pay" />
</div>
<p className="mt-4 text-center text-[11px] text-stone-500">
One click. One box. Farm-fresh delivered.
</p>
</div>
</motion.div>
</div>
</div>
</div>
</section>
{/* ── GUARANTEE / TRUST STRIP ─────────────────────────────────────── */}
<section className="relative bg-[#1A4D2E] text-white overflow-hidden">
<div
aria-hidden
className="absolute inset-0 opacity-[0.08]"
style={{
background:
"radial-gradient(ellipse at 20% 50%, rgba(252, 211, 77, 0.4) 0%, transparent 50%), radial-gradient(ellipse at 80% 50%, rgba(252, 211, 77, 0.3) 0%, transparent 50%)",
}}
/>
<div className="relative mx-auto max-w-6xl px-5 sm:px-6 lg:px-8 py-12 sm:py-16">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10">
<TrustColumn
icon={<TruckIcon />}
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."
/>
<TrustColumn
icon={<ShieldIcon />}
eyebrow="Satisfaction"
title="100% Sweetness Guarantee"
body="If its not the sweetest corn youve ever had, well replace it. No questions, no forms, no fuss."
accent
/>
<TrustColumn
icon={<SparkleIcon />}
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."
/>
</div>
</div>
</section>
{/* ── FARM STORY ─────────────────────────────────────────────────── */}
<section className="relative bg-[#F3EDD8]">
<div className="mx-auto max-w-6xl px-5 sm:px-6 lg:px-8 py-20 sm:py-28">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-16 items-center">
<div className="lg:col-span-5">
<p className="font-mono text-[10px] uppercase tracking-[0.25em] text-[#9A4A06]">
Field notes · Vol. IV
</p>
<h2 className="mt-4 font-display text-[36px] sm:text-[44px] lg:text-[52px] font-black leading-[1.02] tracking-tight text-stone-950">
Picked by hand, <span className="italic font-normal text-[#1A4D2E]">stayed</span> sweet for days.
</h2>
<p className="mt-5 text-stone-700 text-[16px] leading-relaxed max-w-prose">
We hand-select every ear for size and quality. No machine knows when a
cob is at peak sugar content. Thats why your box is a little heavier
than you expect and a whole lot sweeter than any corn youve bought
from a grocery store shelf.
</p>
<div className="mt-8 flex flex-wrap items-center gap-6">
<Stat number="40+" label="Years growing" />
<span className="h-10 w-px bg-stone-900/15" />
<Stat number="3" label="Generations" />
<span className="h-10 w-px bg-stone-900/15" />
<Stat number="100%" label="Hand-picked" />
</div>
</div>
<div className="lg:col-span-7 relative">
<div className="relative aspect-[5/4] overflow-hidden rounded-[28px] ring-1 ring-stone-900/10 shadow-[0_30px_60px_-20px_rgba(26,77,46,0.35)]">
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, #FCD34D 0%, #E5A80F 45%, #8A5A06 100%)",
}}
/>
{/* Abstract corn field lines */}
<svg
viewBox="0 0 400 320"
className="absolute inset-0 w-full h-full"
aria-hidden
>
<defs>
<pattern id="rows" width="50" height="20" patternUnits="userSpaceOnUse" patternTransform="rotate(-8)">
<line x1="0" y1="10" x2="50" y2="10" stroke="#1A4D2E" strokeWidth="2" strokeLinecap="round" />
</pattern>
</defs>
<rect width="400" height="320" fill="url(#rows)" opacity="0.45" />
</svg>
{/* Floating cob clusters */}
<CornIllustration className="absolute -left-6 top-1/2 -translate-y-1/2 w-32 text-[#1A4D2E]/85 rotate-[-8deg]" />
<CornIllustration className="absolute right-6 top-8 w-24 text-[#1A4D2E]/75 rotate-[12deg]" />
<CornIllustration className="absolute right-16 bottom-8 w-20 text-[#1A4D2E]/70 -rotate-6" />
{/* Sun stamp */}
<div className="absolute top-5 right-5 h-20 w-20 rounded-full bg-[#FFE08A] ring-4 ring-[#1A4D2E]/15 flex items-center justify-center">
<span className="font-display text-[10px] font-bold text-[#1A4D2E] text-center leading-tight">
HAND
<br />
PICKED
</span>
</div>
{/* Bottom data strip */}
<div className="absolute inset-x-4 bottom-4 rounded-2xl bg-stone-950/90 backdrop-blur-sm px-4 py-3 flex items-center justify-between text-white text-[11px] font-mono">
<div>
<span className="text-white/50">FIELD</span> · 04-N
</div>
<div>
<span className="text-white/50">BRIX</span> · 24.2°
</div>
<div>
<span className="text-white/50">PICK</span> · 06:14
</div>
</div>
</div>
</div>
</div>
</div>
</section>
{/* ── FAQ MICRO ──────────────────────────────────────────────────── */}
<section className="relative bg-[#FAF6EA]">
<div className="mx-auto max-w-4xl px-5 sm:px-6 lg:px-8 py-20 sm:py-24">
<p className="font-mono text-[10px] uppercase tracking-[0.25em] text-[#9A4A06] text-center">
Buyers notes
</p>
<h2 className="mt-4 font-display text-[36px] sm:text-[44px] font-black leading-tight text-center text-stone-950">
Questions, answered.
</h2>
<div className="mt-12 divide-y divide-stone-900/10 border-t border-b border-stone-900/10">
{FAQS.map((faq) => (
<FAQItem key={faq.q} q={faq.q} a={faq.a} />
))}
</div>
</div>
</section>
{/* ── FINAL CTA STRIP (desktop) ─────────────────────────────────── */}
<section className="hidden lg:block relative bg-stone-950 text-white overflow-hidden">
<div
aria-hidden
className="absolute inset-0 opacity-30"
style={{
background:
"radial-gradient(ellipse at 30% 50%, rgba(229, 168, 15, 0.5) 0%, transparent 60%), radial-gradient(ellipse at 80% 50%, rgba(26, 77, 46, 0.6) 0%, transparent 60%)",
}}
/>
<div className="relative mx-auto max-w-6xl px-8 py-16 flex items-center justify-between gap-10">
<div>
<p className="font-mono text-[10px] uppercase tracking-[0.25em] text-[#FCD34D]">
Ready when you are
</p>
<h3 className="mt-3 font-display text-[40px] font-black leading-[1.05]">
Twelve ears. <span className="italic font-normal">One click.</span>
</h3>
<p className="mt-2 text-stone-400 max-w-md">
Free shipping on orders over $40. Cancel anytime before harvest.
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<button
onClick={handleAddToCart}
className="rounded-2xl h-14 px-7 bg-white text-stone-950 font-bold hover:bg-stone-100 active:scale-[0.98] transition-all"
>
Add to cart {PRODUCT.price}
</button>
<button
onClick={handleBuyNow}
className="rounded-2xl h-14 px-7 bg-[#FCD34D] text-stone-950 font-bold hover:bg-[#FBBF24] active:scale-[0.98] transition-all"
>
Quick Buy
</button>
</div>
</div>
</section>
</main>
{/* ── STICKY MOBILE BOTTOM CTA BAR ──────────────────────────────────── */}
<div className="lg:hidden fixed inset-x-0 bottom-0 z-40 bg-[#FAF6EA]/95 backdrop-blur-xl border-t border-stone-900/10 shadow-[0_-12px_30px_-12px_rgba(26,77,46,0.3)]">
<div className="flex items-center gap-3 px-4 py-3">
<div className="min-w-0 flex-1">
<p className="font-mono text-[9px] uppercase tracking-widest text-stone-500 leading-none">
12-Ear Box
</p>
<p className="mt-1 font-display text-[26px] font-black leading-none text-stone-950">
{PRODUCT.price}
</p>
</div>
<button
onClick={handleBuyNow}
className="h-14 min-w-[120px] rounded-2xl bg-[#FFF1D6] text-[#9A4A06] font-bold text-[14px] ring-1 ring-[#E5A80F]/40 active:scale-[0.98] transition-all"
>
Quick Buy
</button>
<button
onClick={handleAddToCart}
className="h-14 min-w-[160px] rounded-2xl bg-stone-950 text-white font-bold text-[15px] active:scale-[0.98] transition-all"
>
Add · {PRODUCT.price}
</button>
</div>
{/* Safe-area spacer for iOS */}
<div className="h-[env(safe-area-inset-bottom)]" />
</div>
{/* Bottom padding so sticky bar doesn't cover last content on mobile */}
<div className="lg:hidden h-28" aria-hidden />
<StorefrontFooter
brandName={brandName}
brandSlug="tuxedo"
logoUrl={logoUrl}
logoUrlDark={logoUrlDark}
brandAccent="green"
/>
<QuickCartSheet
open={sheetOpen}
onClose={() => setSheetOpen(false)}
productName={PRODUCT.name}
/>
</div>
);
}
/* ── Sub-components ─────────────────────────────────────────────── */
function TrustColumn({
icon,
eyebrow,
title,
body,
accent,
}: {
icon: React.ReactNode;
eyebrow: string;
title: string;
body: string;
accent?: boolean;
}) {
return (
<div className="relative">
<div
className={`flex h-12 w-12 items-center justify-center rounded-2xl ${
accent ? "bg-[#FCD34D] text-stone-950" : "bg-white/10 text-[#FCD34D]"
} ring-1 ring-white/15`}
>
{icon}
</div>
<p
className={`mt-5 font-mono text-[10px] uppercase tracking-[0.25em] ${
accent ? "text-[#FCD34D]" : "text-white/60"
}`}
>
{eyebrow}
</p>
<h3 className="mt-2 font-display text-[24px] font-bold leading-[1.15] text-white">
{title}
</h3>
<p className="mt-3 text-[14px] leading-relaxed text-white/75 max-w-sm">{body}</p>
</div>
);
}
function Stat({ number, label }: { number: string; label: string }) {
return (
<div>
<p className="font-display text-[40px] font-black leading-none text-stone-950">
{number}
</p>
<p className="mt-1 font-mono text-[10px] uppercase tracking-widest text-stone-500">
{label}
</p>
</div>
);
}
function FAQItem({ q, a }: { q: string; a: string }) {
const [open, setOpen] = useState(false);
return (
<div className="py-5">
<button
onClick={() => setOpen((v) => !v)}
className="w-full flex items-center justify-between gap-4 text-left group"
>
<span className="font-display text-[18px] sm:text-[20px] font-bold text-stone-950 group-hover:text-[#1A4D2E] transition-colors">
{q}
</span>
<span
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-full ring-1 ring-stone-900/15 transition-transform duration-300 ${
open ? "rotate-45 bg-stone-950 text-white" : "text-stone-700"
}`}
>
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg>
</span>
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: EASE_OUT }}
className="overflow-hidden"
>
<p className="pt-3 text-[15px] leading-relaxed text-stone-700 max-w-prose">{a}</p>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function Chevron() {
return (
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg>
);
}
function ExpressChip({ label }: { label: string }) {
return (
<div className="flex h-9 items-center justify-center rounded-lg bg-stone-100 ring-1 ring-stone-900/5 text-[11px] font-semibold text-stone-700">
{label}
</div>
);
}
function TruckIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M3 7h11v10H3z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M14 10h4l3 3v4h-7z" />
<circle cx="7" cy="18" r="2" />
<circle cx="17" cy="18" r="2" />
</svg>
);
}
function ShieldIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 2l8 4v6c0 5-3.5 9-8 10-4.5-1-8-5-8-10V6l8-4z"
/>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4" />
</svg>
);
}
function SparkleIcon() {
return (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3l2 5 5 2-5 2-2 5-2-5-5-2 5-2 2-5z" />
</svg>
);
}
function CornIllustration({ className }: { className?: string }) {
return (
<svg viewBox="0 0 100 140" className={className} aria-hidden="true">
{/* Husk leaves */}
<path
d="M30 70c-12-15-15-40-5-60 5 18 15 30 25 38"
fill="currentColor"
opacity="0.55"
/>
<path
d="M70 70c12-15 15-40 5-60-5 18-15 30-25 38"
fill="currentColor"
opacity="0.55"
/>
<path
d="M50 75c-3-20 0-50 5-65 1 20-1 45-3 60"
fill="currentColor"
opacity="0.45"
/>
{/* Cob */}
<ellipse cx="50" cy="78" rx="20" ry="34" fill="currentColor" />
{/* Kernels */}
<g fill="#FAF6EA" opacity="0.75">
{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 <circle key={`${row}-${col}`} cx={x} cy={y} r="1.6" />;
})
)}
</g>
{/* Silk at top */}
<g stroke="currentColor" strokeWidth="1.4" fill="none" opacity="0.7" strokeLinecap="round">
<path d="M50 45c-6-8-12-12-18-12" />
<path d="M50 45c0-10 4-15 8-20" />
<path d="M50 45c6-6 12-8 18-10" />
</g>
</svg>
);
}
function FloatingCorn({ className }: { className?: string }) {
return (
<svg viewBox="0 0 60 80" className={className} aria-hidden="true">
<ellipse cx="30" cy="40" rx="14" ry="24" fill="currentColor" />
<g fill="white" opacity="0.3">
<circle cx="26" cy="28" r="1.2" />
<circle cx="32" cy="32" r="1.2" />
<circle cx="28" cy="38" r="1.2" />
<circle cx="34" cy="42" r="1.2" />
<circle cx="26" cy="48" r="1.2" />
<circle cx="32" cy="52" r="1.2" />
<circle cx="28" cy="58" r="1.2" />
</g>
</svg>
);
}
/* ── 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. Youll 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 isnt the sweetest Ive ever had?",
a: "We replace it. Send a photo to hello@tuxedocorn.com and well 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.",
},
];
+28
View File
@@ -30,6 +30,12 @@ type CartContextType = {
cartRestored: boolean; // true when server cart was loaded (for toast)
setSelectedStop: (stop: StopInfo | null) => void;
addToCart: (item: Omit<CartItem, "quantity">, 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<CartItem, "quantity">, 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<CartItem, "quantity">, 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,