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:
@@ -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 it’s not the sweetest corn you’ve ever had, we’ll 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. 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.
|
||||
</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">
|
||||
Buyer’s 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. 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.",
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user