← back to Brand Selector Viewer

outputs/magic-BrandSelector.tsx

190 lines

// Generated via 21st.dev Magic — Brand Selector
// Drop into a React/Tailwind project. Self-contained; props-driven.
// Pairs with Paper editorial spec + Figma design tokens.

import { useMemo, useState } from "react";

export type Brand = {
  name: string;
  country: string;
  founded: number;
  palette: [string, string, string];
};

export type BrandPayload = {
  schema: "figma-build/brand-selection.v1";
  generated_at: string;
  counts: { fashion: number; wallcovering: number; total: number };
  fashion: Brand[];
  wallcovering: Brand[];
};

export const FASHION_20: Brand[] = [
  { name: "Louis Vuitton", country: "FR", founded: 1854, palette: ["#5d3a22", "#d4b886", "#1a1a1a"] },
  { name: "Chanel",        country: "FR", founded: 1910, palette: ["#000000", "#ffffff", "#c9a96e"] },
  { name: "Hermès",        country: "FR", founded: 1837, palette: ["#f37021", "#1a1a1a", "#f4e7d3"] },
  { name: "Gucci",         country: "IT", founded: 1921, palette: ["#006341", "#a01c1f", "#d4af37"] },
  { name: "Prada",         country: "IT", founded: 1913, palette: ["#000000", "#8b8680", "#e8e4dc"] },
  { name: "Dior",          country: "FR", founded: 1946, palette: ["#1a1a1a", "#c8a883", "#ffffff"] },
  { name: "Versace",       country: "IT", founded: 1978, palette: ["#d4af37", "#000000", "#a01c1f"] },
  { name: "Burberry",      country: "UK", founded: 1856, palette: ["#a08661", "#000000", "#c1272d"] },
  { name: "Saint Laurent", country: "FR", founded: 1961, palette: ["#000000", "#d4b886", "#ffffff"] },
  { name: "Balenciaga",    country: "ES", founded: 1919, palette: ["#000000", "#5a5a5a", "#ffffff"] },
  { name: "Bottega Veneta",country: "IT", founded: 1966, palette: ["#3d5a2c", "#c9b27a", "#1a1a1a"] },
  { name: "Fendi",         country: "IT", founded: 1925, palette: ["#5d3a22", "#f0e6d2", "#000000"] },
  { name: "Givenchy",      country: "FR", founded: 1952, palette: ["#000000", "#d4af37", "#a01c1f"] },
  { name: "Valentino",     country: "IT", founded: 1960, palette: ["#a01c1f", "#1a1a1a", "#f0e6d2"] },
  { name: "Armani",        country: "IT", founded: 1975, palette: ["#1a1a2e", "#8b8680", "#000000"] },
  { name: "Ralph Lauren",  country: "US", founded: 1967, palette: ["#1a3a6c", "#a01c1f", "#f0e6d2"] },
  { name: "Tom Ford",      country: "US", founded: 2005, palette: ["#000000", "#5d3a22", "#d4af37"] },
  { name: "Celine",        country: "FR", founded: 1945, palette: ["#000000", "#d4b886", "#ffffff"] },
  { name: "Loewe",         country: "ES", founded: 1846, palette: ["#5d3a22", "#d4b886", "#1a1a1a"] },
  { name: "Alexander McQueen", country: "UK", founded: 1992, palette: ["#000000", "#a01c1f", "#8b8680"] },
];

export const WALL_10: Brand[] = [
  { name: "Phillip Jeffries",  country: "US", founded: 1976, palette: ["#d4b886", "#5d3a22", "#f0e6d2"] },
  { name: "Schumacher",        country: "US", founded: 1889, palette: ["#a01c1f", "#3d5a2c", "#d4af37"] },
  { name: "Cole & Son",        country: "UK", founded: 1875, palette: ["#1a3a6c", "#c9a96e", "#f0e6d2"] },
  { name: "Thibaut",           country: "US", founded: 1886, palette: ["#3d5a2c", "#c9a96e", "#1a1a1a"] },
  { name: "Zoffany",           country: "UK", founded: 1895, palette: ["#5d3a22", "#d4b886", "#3d5a2c"] },
  { name: "Farrow & Ball",     country: "UK", founded: 1946, palette: ["#1a3a3a", "#e8e4dc", "#5d3a22"] },
  { name: "Brunschwig & Fils", country: "US", founded: 1909, palette: ["#a01c1f", "#1a3a6c", "#d4b886"] },
  { name: "de Gournay",        country: "UK", founded: 1986, palette: ["#c9a96e", "#5d3a22", "#3d5a2c"] },
  { name: "Fromental",         country: "UK", founded: 2005, palette: ["#1a3a6c", "#d4af37", "#5d3a22"] },
  { name: "Pierre Frey",       country: "FR", founded: 1935, palette: ["#a01c1f", "#d4b886", "#1a3a6c"] },
];

type Props = {
  fashion?: Brand[];
  wallcovering?: Brand[];
  onSubmit?: (payload: BrandPayload) => void;
};

export default function BrandSelector({
  fashion = FASHION_20,
  wallcovering = WALL_10,
  onSubmit,
}: Props) {
  const [picked, setPicked] = useState<Record<string, boolean>>({});

  const toggle = (name: string) =>
    setPicked((p) => ({ ...p, [name]: !p[name] }));

  const buildPayload = (): BrandPayload => {
    const f = fashion.filter((b) => picked[b.name]);
    const w = wallcovering.filter((b) => picked[b.name]);
    return {
      schema: "figma-build/brand-selection.v1",
      generated_at: new Date().toISOString(),
      counts: { fashion: f.length, wallcovering: w.length, total: f.length + w.length },
      fashion: f,
      wallcovering: w,
    };
  };

  const summary = useMemo(() => {
    const all = [...fashion, ...wallcovering].filter((b) => picked[b.name]);
    return { count: all.length, names: all.map((b) => b.name).join(" · ") || "none" };
  }, [picked, fashion, wallcovering]);

  return (
    <div className="min-h-screen bg-[#0b0b0d] text-zinc-100 pb-32 font-sans">
      <header className="px-10 pt-12 pb-6 border-b border-zinc-800 flex flex-wrap items-end justify-between gap-6">
        <div>
          <div className="text-[10px] tracking-[3px] uppercase text-amber-400/80">Figma Build Source · v1</div>
          <h1 className="font-serif text-5xl tracking-tight mt-2">Brand Selector</h1>
          <p className="italic text-zinc-400 mt-1">Pick the labels that will source the next Figma build · Top 20 fashion · Top 10 wallcovering</p>
        </div>
        <div className="flex gap-12 text-right">
          <Stat n={fashion.filter((b) => picked[b.name]).length} label="Fashion" />
          <Stat n={wallcovering.filter((b) => picked[b.name]).length} label="Wallcovering" />
        </div>
      </header>

      <Section index="I." title="Fashion" sub="Top 20 — Luxury & Iconic Houses" brands={fashion} picked={picked} onToggle={toggle} />
      <Section index="II." title="Wallcovering" sub="Top 10 — Heritage & Designer Houses" brands={wallcovering} picked={picked} onToggle={toggle} />

      <div className="fixed bottom-0 inset-x-0 bg-[#0b0b0d]/90 backdrop-blur border-t border-zinc-800 px-10 py-3 flex items-center justify-between gap-4">
        <div className="flex gap-3 items-center text-sm">
          <span className="bg-zinc-900 px-3 py-1.5 rounded-full"><b className="text-amber-400 font-serif mr-1">{summary.count}</b> selected</span>
          <span className="text-zinc-500 truncate max-w-[600px]">{summary.names}</span>
        </div>
        <button
          onClick={() => onSubmit?.(buildPayload())}
          className="bg-amber-400 text-zinc-900 px-5 py-2 rounded-full text-sm font-medium hover:bg-amber-300 transition"
        >
          Send to Figma Build →
        </button>
      </div>
    </div>
  );
}

function Stat({ n, label }: { n: number; label: string }) {
  return (
    <div>
      <div className="font-serif text-3xl text-amber-400 leading-none">{n}</div>
      <div className="text-[10px] tracking-[2px] uppercase text-zinc-500 mt-1">{label}</div>
    </div>
  );
}

function Section({
  index, title, sub, brands, picked, onToggle,
}: { index: string; title: string; sub: string; brands: Brand[]; picked: Record<string, boolean>; onToggle: (n: string) => void; }) {
  return (
    <section className="px-10 pt-12">
      <div className="mb-5">
        <div className="text-[10px] tracking-[3px] uppercase text-amber-400">{index}</div>
        <h2 className="font-serif text-3xl mt-1">{title}</h2>
        <div className="italic text-zinc-500 text-sm tracking-wide">{sub}</div>
      </div>
      <div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-3">
        {brands.map((b) => (
          <BrandCard key={b.name} brand={b} selected={!!picked[b.name]} onClick={() => onToggle(b.name)} />
        ))}
      </div>
    </section>
  );
}

function BrandCard({ brand, selected, onClick }: { brand: Brand; selected: boolean; onClick: () => void }) {
  return (
    <button
      onClick={onClick}
      className={[
        "relative text-left rounded-2xl p-5 min-h-[140px] flex flex-col justify-between transition border",
        selected
          ? "bg-gradient-to-br from-amber-400/10 to-zinc-900 border-amber-400 shadow-[inset_0_0_0_1px_rgba(212,175,55,1)]"
          : "bg-zinc-900 border-zinc-800 hover:border-zinc-600 hover:-translate-y-0.5",
      ].join(" ")}
    >
      <span className={[
        "absolute top-3 right-3 w-5 h-5 rounded-full border flex items-center justify-center text-[11px]",
        selected ? "bg-amber-400 border-amber-400 text-zinc-900" : "border-zinc-700 text-transparent",
      ].join(" ")}>✓</span>
      <div>
        <div className="font-serif text-lg leading-tight pr-7">{brand.name}</div>
        <div className="flex gap-1.5 mt-2.5">
          <Tag>{brand.country}</Tag>
          <Tag>est. {brand.founded}</Tag>
        </div>
      </div>
      <div className="flex gap-1 mt-3">
        {brand.palette.map((c) => (
          <span key={c} className="w-4 h-4 rounded border border-white/10" style={{ background: c }} />
        ))}
      </div>
    </button>
  );
}

function Tag({ children }: { children: React.ReactNode }) {
  return (
    <span className="text-[9.5px] tracking-[1.2px] uppercase text-zinc-500 border border-zinc-800 px-2 py-0.5 rounded-full">
      {children}
    </span>
  );
}