Fazekit

Code

"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;