"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";
import { cellarContent, formatPrice, type CellarContent, type Product } from "./data";
import { AgeGate } from "./sections/age-gate";
import { Header, type CartLine } from "./sections/header";
import { Hero } from "./sections/hero";
import { Bestsellers, Categories } from "./sections/shop";
import { Journal } from "./sections/journal";
import { GiftFinder } from "./sections/gift-finder";
import { Stores } from "./sections/stores";
import { Footer, Newsletter, Reviews, Services } from "./sections/closing";
import { PALETTE } from "./sections/ui";
import { Bottle } from "./sections/bottle";
export interface LiquorStoreTemplateProps {
/** Override any top-level content block. Missing keys fall back to the demo copy. */
content?: Partial<CellarContent>;
/** Show the 18+ age confirmation on first render. Defaults to true — keep it on in production. */
ageGate?: boolean;
/** Called whenever a product is added to the basket. */
onAddToCart?: (product: Product) => void;
/** Products shown in the hero art (left, centre, right). Defaults to the first wine, whisky and gin. */
heroProductIds?: [string, string, string];
className?: string;
}
/**
* Northwind Cellar — a premium spirits & wine shop homepage.
* Age gate, seasonal hero, categories, bestsellers with a working basket, tasting-notes editorial,
* gift-finder quiz, store locator, newsletter with a discount code, reviews, services and a legal footer.
*/
export function LiquorStoreTemplate({ content, ageGate = true, onAddToCart, heroProductIds, className }: LiquorStoreTemplateProps) {
const c: CellarContent = { ...cellarContent, ...content };
const [verified, setVerified] = React.useState(!ageGate);
const [cart, setCart] = React.useState<Record<string, number>>({});
const [toast, setToast] = React.useState<{ product: Product; n: number } | null>(null);
React.useEffect(() => {
if (!toast) return;
const t = window.setTimeout(() => setToast(null), 2600);
return () => window.clearTimeout(t);
}, [toast]);
const byId = React.useMemo(() => new Map(c.products.map((p) => [p.id, p])), [c.products]);
const lines: CartLine[] = Object.entries(cart)
.filter(([, q]) => q > 0)
.map(([id, qty]) => ({ product: byId.get(id), qty }))
.filter((l): l is CartLine => !!l.product);
const add = (p: Product) => {
setCart((cur) => ({ ...cur, [p.id]: (cur[p.id] ?? 0) + 1 }));
setToast((t) => ({ product: p, n: (t?.n ?? 0) + 1 }));
onAddToCart?.(p);
};
const changeQty = (id: string, delta: number) => setCart((cur) => ({ ...cur, [id]: Math.max(0, (cur[id] ?? 0) + delta) }));
const pick = (fallback: Product["category"], id?: string) => (id ? byId.get(id) : undefined) ?? c.products.find((p) => p.category === fallback) ?? c.products[0];
const heroProducts = [pick("wine", heroProductIds?.[0]), pick("whisky", heroProductIds?.[1]), pick("gin", heroProductIds?.[2])].filter((p): p is Product => !!p);
return (
<div className={cn(PALETTE, "relative w-full bg-(--nw-bg) font-sans text-(--nw-ink) antialiased selection:bg-(--nw-wine) selection:text-(--nw-on-wine)", className)}>
<AgeGate open={!verified} brand={c.brand} onConfirm={() => setVerified(true)} />
<Header content={c} lines={lines} onQty={changeQty} />
<main>
<Hero content={c.hero} brand={c.brand} featured={heroProducts} currency={c.currency} />
<Categories content={c.categories} />
<Bestsellers products={c.products} currency={c.currency} onAdd={add} />
<Journal content={c.tasting} />
<GiftFinder content={c.quiz} products={c.products} currency={c.currency} onAdd={add} />
<Stores content={c.stores} />
<Newsletter content={c.newsletter} />
<Reviews content={c.reviews} />
<Services items={c.services} responsible={c.responsible} />
</main>
<Footer brand={c.brand} nav={c.nav} content={c.footer} />
<div aria-live="polite" className="pointer-events-none fixed inset-x-0 bottom-4 z-[60] flex justify-center px-4">
<AnimatePresence>
{toast && (
<motion.div
key={toast.n}
initial={{ opacity: 0, y: 24, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 12, scale: 0.98 }}
transition={{ type: "spring", stiffness: 380, damping: 30 }}
className="pointer-events-auto flex items-center gap-3 rounded-2xl border border-(--nw-line) bg-(--nw-paper) py-2.5 pl-2.5 pr-5 text-(--nw-ink) shadow-2xl"
>
<span className="flex h-12 w-9 items-end justify-center rounded-lg bg-(--nw-soft) pb-1">
<Bottle art={toast.product.art} className="h-10" />
</span>
<span className="text-sm">
<span className="flex items-center gap-1.5 font-medium">
<Check className="size-3.5 text-emerald-600" /> Added to basket
</span>
<span className="block max-w-[220px] truncate text-xs text-(--nw-muted)">
{toast.product.name} · {formatPrice(toast.product.price, c.currency)}
</span>
</span>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}
export default LiquorStoreTemplate;