Fazekit

Code

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

More in E-commerce

View all →