"use client";
import * as React from "react";
import { MotionConfig, useReducedMotion } from "motion/react";
import { Leaf, RotateCcw, Scissors, Truck } from "lucide-react";
import { cn } from "@/lib/utils";
import { defaultStoreData, type Product, type ProductCategory, type StoreData, type Swatch } from "./data";
import { Footer, Newsletter, SocialGrid } from "./sections/closing";
import { Collections } from "./sections/collections";
import { Hero } from "./sections/hero";
import { ShopTheLook } from "./sections/look";
import { Lookbook } from "./sections/lookbook";
import { AnnouncementBar, Navbar } from "./sections/navbar";
import { NewIn } from "./sections/new-in";
import { CartDrawer, SizeGuide, type CartLine } from "./sections/overlays";
import { Reviews } from "./sections/reviews";
import { Story } from "./sections/story";
import { AN_SERIF_STACK, scrollToId, useMoney } from "./sections/ui";
export type { StoreData, Product, CartLine };
export { defaultStoreData };
export interface FashionStoreTemplateProps {
/** Override any top-level content group (brand, hero, products…). Missing groups fall back to the demo content. */
data?: Partial<StoreData>;
/** Called every time a product is added to the bag. */
onAddToCart?: (product: Product, size: string, colour: Swatch) => void;
/** Called when the shopper presses checkout. Return a promise to keep the loading state. */
onCheckout?: (lines: CartLine[]) => void | Promise<void>;
/** Called when someone subscribes to the newsletter. */
onSubscribe?: (email: string) => void;
className?: string;
}
const VALUES = [
{ icon: Truck, title: "Complimentary shipping", body: "On every order over €250" },
{ icon: RotateCcw, title: "30-day returns", body: "Free, with a prepaid label" },
{ icon: Scissors, title: "Ten-year repairs", body: "We mend what we make" },
{ icon: Leaf, title: "Natural fibres", body: "94% of the collection" },
];
/**
* Atelier Noor — a minimal editorial fashion boutique.
* Ken-Burns campaign hero, new-in grid with hover image swap & quick add, shop-the-look hotspots,
* category collage, pinned horizontal lookbook, reviews with fit meter, atelier story, social grid,
* newsletter, footer, size-guide modal and a working mini-cart drawer.
*/
export function FashionStoreTemplate({ data, onAddToCart, onCheckout, onSubscribe, className }: FashionStoreTemplateProps) {
const d: StoreData = React.useMemo(() => ({ ...defaultStoreData, ...data }), [data]);
const reduce = useReducedMotion();
const money = useMoney(d.brand.currency);
const [lines, setLines] = React.useState<CartLine[]>([]);
const [cartOpen, setCartOpen] = React.useState(false);
const [sizeOpen, setSizeOpen] = React.useState(false);
const [wishlist, setWishlist] = React.useState<Set<string>>(() => new Set(["p-dress"]));
const [filter, setFilter] = React.useState<ProductCategory | "all">("all");
const add = React.useCallback(
(product: Product, size: string, swatch: Swatch) => {
const key = `${product.id}|${size}|${swatch.name}`;
setLines((cur) => {
const hit = cur.find((l) => l.key === key);
if (hit) return cur.map((l) => (l.key === key ? { ...l, qty: l.qty + 1 } : l));
return [...cur, { key, product, size, swatch, qty: 1 }];
});
setCartOpen(true);
onAddToCart?.(product, size, swatch);
},
[onAddToCart],
);
const qty = (key: string, delta: number) => setLines((cur) => cur.map((l) => (l.key === key ? { ...l, qty: Math.max(1, l.qty + delta) } : l)));
const remove = (key: string) => setLines((cur) => cur.filter((l) => l.key !== key));
const wish = (id: string) =>
setWishlist((cur) => {
const n = new Set(cur);
if (n.has(id)) n.delete(id);
else n.add(id);
return n;
});
const count = lines.reduce((s, l) => s + l.qty, 0);
const inCart = new Set(lines.map((l) => l.product.id));
const suggestion = d.products.find((p) => !inCart.has(p.id) && p.category === "accessories") ?? d.products.find((p) => !inCart.has(p.id));
const pickCategory = (c: ProductCategory) => {
setFilter(d.newIn.filters.some((f) => f.id === c) ? c : "all");
scrollToId("new-in", reduce);
};
return (
<MotionConfig reducedMotion="user">
<div
className={cn("relative w-full bg-background text-foreground antialiased selection:bg-[#a4553a]/25", className)}
style={{ "--an-serif": AN_SERIF_STACK } as React.CSSProperties}
>
<AnnouncementBar messages={d.announcements} />
<Navbar name={d.brand.name} links={d.nav} cartCount={count} wishCount={wishlist.size} onCart={() => setCartOpen(true)} onSizeGuide={() => setSizeOpen(true)} />
<main>
<Hero hero={d.hero} onShop={() => scrollToId("new-in", reduce)} onCampaign={() => scrollToId("lookbook", reduce)} />
<section aria-label="Our promises" className="border-b px-4 sm:px-8">
<ul className="mx-auto grid max-w-[1400px] grid-cols-2 gap-x-6 lg:grid-cols-4 lg:gap-x-10">
{VALUES.map((v) => (
<li key={v.title} className="flex items-center gap-3 py-5 sm:gap-4 sm:py-8">
<v.icon className="size-5 shrink-0 text-muted-foreground" strokeWidth={1.4} aria-hidden />
<div className="min-w-0">
<p className="text-[12px] font-medium uppercase tracking-[0.14em]">{v.title}</p>
<p className="mt-0.5 text-xs text-muted-foreground">{v.body}</p>
</div>
</li>
))}
</ul>
</section>
<NewIn
newIn={d.newIn}
products={d.products}
money={money}
wishlist={wishlist}
onWish={wish}
onAdd={add}
onSizeGuide={() => setSizeOpen(true)}
filter={filter}
onFilter={setFilter}
/>
<ShopTheLook look={d.look} products={d.products} money={money} onAdd={add} />
<Collections categories={d.categories} onPick={pickCategory} />
<Lookbook lookbook={d.lookbook} />
<Story story={d.story} />
<Reviews reviews={d.reviews} />
<SocialGrid social={d.social} handle={d.brand.handle} />
<Newsletter newsletter={d.newsletter} onSubscribe={onSubscribe} />
</main>
<Footer footer={d.footer} brand={d.brand} onSizeGuide={() => setSizeOpen(true)} />
<SizeGuide open={sizeOpen} onClose={() => setSizeOpen(false)} rows={d.sizeGuide} />
<CartDrawer
open={cartOpen}
onClose={() => setCartOpen(false)}
lines={lines}
money={money}
freeShippingOver={d.brand.freeShippingOver}
onQty={qty}
onRemove={remove}
suggestion={suggestion}
onAddSuggestion={(p) => add(p, p.sizes[0], p.swatches[0])}
onCheckout={onCheckout}
/>
</div>
</MotionConfig>
);
}