@cimplify/cli 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -197,7 +197,7 @@ function Field({
197
197
  font-weight: 600;
198
198
  }
199
199
  }
200
- ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, JetBrains_Mono } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst mono = JetBrains_Mono({\n subsets: ["latin"],\n variable: "--font-mono",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed retail --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed retail",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
200
+ ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, JetBrains_Mono } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst mono = JetBrains_Mono({\n subsets: ["latin"],\n variable: "--font-mono",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed retail --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed retail",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
201
201
  * Brand & content configuration \u2014 the **single source of truth** for every
202
202
  * visible string in this storefront.
203
203
  *
@@ -1011,7 +1011,7 @@ function Field({
1011
1011
  </div>
1012
1012
  );
1013
1013
  }
1014
- ` }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <h1 className="text-3xl mt-0 mb-3 font-bold -tracking-[0.025em]">\n Order confirmed.\n </h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code> \u2014 you&apos;ll\n get an SMS with tracking within 30 minutes.\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/products/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { notFound } from "next/navigation";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Product,\n type ProductWithDetails,\n} from "@cimplify/sdk/server";\nimport { ProductDetail } from "./product-detail";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nfunction productLd(product: ProductWithDetails, SITE_URL: string) {\n const image = product.image_url ?? product.images?.[0];\n const inStock = product.inventory_status?.in_stock !== false;\n return {\n "@context": "https://schema.org",\n "@type": "Product",\n name: product.name,\n description: product.description ?? undefined,\n image: image ? [image] : undefined,\n sku: product.id,\n brand: { "@type": "Brand", name: brand.name },\n offers: {\n "@type": "Offer",\n price: product.default_price,\n priceCurrency: brand.currency,\n availability: inStock\n ? "https://schema.org/InStock"\n : "https://schema.org/OutOfStock",\n url: `${SITE_URL}/products/${product.slug ?? product.id}`,\n },\n };\n}\n\ninterface ProductData {\n product: ProductWithDetails;\n related: Product[];\n}\n\nasync function getProduct(slug: string): Promise<ProductData | null> {\n "use cache";\n cacheTag(tags.product(slug), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const r = await client.catalogue.getProductBySlug(slug);\n if (!r.ok) return null;\n\n const related = r.value.category_id\n ? await client.catalogue\n .getCategoryProducts(r.value.category_id)\n .then((res) =>\n res.ok\n ? (\n ((res.value as { items?: Product[] }).items ??\n (res.value as Product[])) as Product[]\n ).filter((p) => p.id !== r.value.id).slice(0, 4)\n : [],\n )\n : [];\n\n return { product: r.value, related };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const data = await getProduct(slug);\n if (!data) return {};\n const { product } = data;\n const image = product.image_url ?? product.images?.[0];\n return {\n title: `${product.name} \u2014 ${brand.name}`,\n description: product.description ?? undefined,\n openGraph: {\n title: product.name,\n description: product.description ?? undefined,\n images: image ? [{ url: image }] : undefined,\n type: "website",\n },\n };\n}\n\nexport default async function ProductPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<ProductSkeleton />}>\n <ProductContent params={params} />\n </Suspense>\n );\n}\n\nasync function ProductContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const data = await getProduct(slug);\n if (!data) notFound();\n\n return (\n <>\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product, siteUrl)) }}\n />\n <nav\n aria-label="Breadcrumb"\n className="max-w-7xl mx-auto px-6 sm:px-8 pt-6 text-[12px] font-mono text-muted-foreground flex items-center gap-2"\n >\n <Link href="/" className="hover:text-foreground transition-colors">\n Home\n </Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-foreground transition-colors">\n Shop\n </Link>\n {data.product.category?.slug && (\n <>\n <span>/</span>\n <Link\n href={`/categories/${data.product.category.slug}`}\n className="hover:text-foreground transition-colors"\n >\n {data.product.category.name}\n </Link>\n </>\n )}\n <span>/</span>\n <span className="text-foreground/90 truncate">{data.product.name}</span>\n </nav>\n <ProductDetail product={data.product} related={data.related} />\n </>\n );\n}\n\nfunction ProductSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 items-start">\n <div className="aspect-square bg-muted rounded-3xl animate-pulse" />\n <div className="space-y-4">\n <div className="h-3 w-24 bg-muted rounded animate-pulse" />\n <div className="h-10 w-3/4 bg-muted rounded animate-pulse" />\n <div className="h-7 w-32 bg-muted rounded animate-pulse" />\n <div className="h-20 w-full bg-muted rounded animate-pulse mt-6" />\n <div className="h-12 w-full bg-muted rounded animate-pulse mt-4" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "app/products/[slug]/product-detail.tsx", "kind": "text", "content": '"use client";\n\nimport Image from "next/image";\nimport { ProductPage as SdkProductPage, useCart } from "@cimplify/sdk/react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the product detail page.\n *\n * - Receives a server-fetched `ProductWithDetails` (no client refetch).\n * - Renders the SDK `<ProductPage>` which picks the right layout\n * (Default / Wholesale / Service / Bundle / Composite) automatically.\n * - On add-to-cart success, routes to `/cart`.\n * - Custom Next.js Image renderer for optimised, lazy-loaded gallery shots.\n * - Renders a "You may also like" rail of in-category products below.\n */\nexport function ProductDetail({\n product,\n related,\n}: {\n product: ProductWithDetails;\n related: Product[];\n}) {\n const { addItem } = useCart();\n\n return (\n <>\n <SdkProductPage\n product={product}\n showRelated={false}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={1200}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-8 sm:py-10"\n />\n\n {related.length > 0 && (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-16 border-t border-border mt-8">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n You may also like\n </p>\n <h2 className="text-[clamp(1.5rem,2.5vw,2rem)] font-bold m-0 -tracking-[0.025em]">\n More from this category.\n </h2>\n </div>\n </div>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {related.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </section>\n )}\n </>\n );\n}\n' }, { "path": "app/login/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sign in \u2014 ${brand.name}`,\n description: brand.account.loginSubtitle,\n};\n\n/**\n * Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in\n * automatically when no session exists. We just bounce /login to /account\n * so consumers landing here get the right UI without a duplicate form.\n */\nexport default function LoginPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Collection, type Product } from "@cimplify/sdk/server";\nimport { FeatureHero } from "@/components/feature-hero";\nimport { CategoryTiles } from "@/components/category-tiles";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { PromoBanner } from "@/components/promo-banner";\nimport { TrustBar } from "@/components/trust-bar";\nimport { BrandMarquee } from "@/components/brand-marquee";\nimport { TradeInCta } from "@/components/trade-in-cta";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\ninterface CollectionWithProducts {\n collection: Collection;\n products: Product[];\n}\n\nasync function getHomeData() {\n "use cache";\n cacheTag(tags.collections(), tags.categories(), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const [colRes, catRes, productsRes] = await Promise.all([\n client.catalogue.getCollections(),\n client.catalogue.getCategories(),\n client.catalogue.getProducts({ limit: 12 }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n const allProducts = productsRes.ok ? productsRes.value.items : [];\n\n const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(col.id);\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n featured: allProducts.slice(0, 4),\n newArrivals: allProducts.slice(4, 12),\n };\n}\n\n// Verified-reachable Unsplash fashion editorial \u2014 replace with your own asset.\nconst HERO_FALLBACK_IMAGE =\n "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=2000&h=1400&fit=crop&auto=format&q=85";\n\nexport default async function HomePage() {\n const { collections, categories, featured, newArrivals } = await getHomeData();\n const heroProduct = featured[0];\n\n return (\n <>\n <FeatureHero\n eyebrow="Featured \xB7 in stock"\n badge="Just landed"\n title={brand.hero.title}\n description={heroProduct?.description ?? brand.hero.subtitle}\n primaryCta={{\n label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,\n href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",\n }}\n secondaryCta={\n brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref\n ? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }\n : undefined\n }\n imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}\n imageAlt={heroProduct?.name ?? "Featured product"}\n />\n\n <TrustBar />\n\n <Suspense fallback={<CategoryTilesSkeleton />}>\n <CategoryTiles categories={categories} />\n </Suspense>\n\n <PromoBanner />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Just dropped"\n title="New this week."\n description="Hand-picked launches and freshly restocked staples."\n link={{ label: "Browse all", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(0, 4).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <BrandMarquee />\n\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n\n <TradeInCta />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Best sellers"\n title="What everyone&apos;s buying."\n link={{ label: "See more", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(4, 8).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <Newsletter />\n </>\n );\n}\n\nfunction CategoryTilesSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">\n <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\nasync function getCategory(slug: string): Promise<CategoryData | null> {\n "use cache";\n cacheTag(tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug);\n if (!catRes.ok) return null;\n\n cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));\n const r = await client.catalogue.getCategoryProducts(catRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { category: catRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) return {};\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) notFound();\n\n const { category, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/error.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Link from "next/link";\n\n/**\n * Root error boundary. Next.js calls this whenever a thrown error escapes\n * a Server Component without being caught by a closer `error.tsx`.\n *\n * Wire `reportError` (Sentry, Datadog, etc.) here so production failures\n * surface \u2014 silently recovering hides real bugs.\n */\nexport default function GlobalError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n // TODO: replace with your error reporter\n // eslint-disable-next-line no-console\n console.error("Storefront error:", error);\n }, [error]);\n\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n Something broke\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold mb-4 -tracking-[0.025em]">\n That wasn&apos;t supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We&apos;ve been notified. Refresh the page, or head back home \u2014 most\n of the time the second try just works.\n </p>\n {error.digest && (\n <p className="text-xs font-mono text-muted-foreground mb-6">\n Reference:{" "}\n <code className="text-foreground">{error.digest}</code>\n </p>\n )}\n <div className="flex flex-wrap items-center justify-center gap-3">\n <button\n type="button"\n onClick={reset}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Try again\n </button>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Studio FRX \u2014 Nike/Aritzia-style streetwear palette. High-contrast\n neutrals with one electric accent. Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.99 0 0);\n --color-foreground: oklch(0.12 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.12 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.12 0 0);\n --color-primary: oklch(0.7 0.24 30);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.96 0 0);\n --color-secondary-foreground: oklch(0.18 0 0);\n --color-muted: oklch(0.96 0 0);\n --color-muted-foreground: oklch(0.45 0 0);\n --color-accent: oklch(0.92 0 0);\n --color-accent-foreground: oklch(0.18 0 0);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0 0);\n --color-input: oklch(0.93 0 0);\n --color-ring: oklch(0.7 0.24 30);\n --radius: 0.125rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-display);\n letter-spacing: -0.04em;\n font-weight: 800;\n line-height: 0.95;\n }\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Anton } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst anton = Anton({\n subsets: ["latin"],\n variable: "--font-display",\n weight: "400",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${anton.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/size-guide/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Size Guide \u2014 ${brand.name}`,\n description: "Chest, length, and shoulder measurements for every Studio FRX piece.",\n};\n\ninterface SizeRow {\n size: string;\n chest: string;\n length: string;\n shoulder: string;\n}\n\nconst TOPS_CHART: SizeRow[] = [\n { size: "XS", chest: "104 cm / 41 in", length: "68 cm / 26.8 in", shoulder: "50 cm / 19.7 in" },\n { size: "S", chest: "110 cm / 43.3 in", length: "70 cm / 27.5 in", shoulder: "52 cm / 20.5 in" },\n { size: "M", chest: "116 cm / 45.7 in", length: "72 cm / 28.3 in", shoulder: "54 cm / 21.3 in" },\n { size: "L", chest: "122 cm / 48 in", length: "74 cm / 29.1 in", shoulder: "56 cm / 22 in" },\n { size: "XL", chest: "128 cm / 50.4 in", length: "76 cm / 29.9 in", shoulder: "58 cm / 22.8 in" },\n { size: "2XL", chest: "134 cm / 52.7 in", length: "78 cm / 30.7 in", shoulder: "60 cm / 23.6 in" },\n { size: "3XL", chest: "140 cm / 55.1 in", length: "80 cm / 31.5 in", shoulder: "62 cm / 24.4 in" },\n];\n\nconst BOTTOMS_CHART: { size: string; waist: string; hip: string; inseam: string }[] = [\n { size: "XS", waist: "72 cm / 28 in", hip: "92 cm / 36 in", inseam: "78 cm / 30.7 in" },\n { size: "S", waist: "76 cm / 30 in", hip: "96 cm / 38 in", inseam: "78 cm / 30.7 in" },\n { size: "M", waist: "82 cm / 32 in", hip: "102 cm / 40 in", inseam: "80 cm / 31.5 in" },\n { size: "L", waist: "88 cm / 35 in", hip: "108 cm / 42.5 in", inseam: "80 cm / 31.5 in" },\n { size: "XL", waist: "94 cm / 37 in", hip: "114 cm / 45 in", inseam: "82 cm / 32.3 in" },\n { size: "2XL", waist: "102 cm / 40 in", hip: "122 cm / 48 in", inseam: "82 cm / 32.3 in" },\n];\n\nexport default function SizeGuidePage() {\n return (\n <article className="max-w-4xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-2">\n Size guide\n </p>\n <h1 className="font-display text-[clamp(2.5rem,7vw,5rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.95]">\n Find your fit.\n </h1>\n <p className="text-base text-muted-foreground leading-relaxed max-w-2xl mb-12">\n Our pieces are cut for an oversized, dropped-shoulder fit. Order true to size for\n the intended fit; size down for a regular cut. All measurements are flat-laid\n garment measurements.\n </p>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Tops & outerwear</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Hoodies, tees, sweats, jackets. Chest measured pit-to-pit, doubled. Length\n measured from highest shoulder seam to hem.\n </p>\n <SizeTable\n headings={["Size", "Chest", "Length", "Shoulder"]}\n rows={TOPS_CHART.map((r) => [r.size, r.chest, r.length, r.shoulder])}\n />\n </section>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Bottoms</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Trousers, shorts, sweats. Waist measured flat across the top of the\n waistband, doubled.\n </p>\n <SizeTable\n headings={["Size", "Waist", "Hip", "Inseam"]}\n rows={BOTTOMS_CHART.map((r) => [r.size, r.waist, r.hip, r.inseam])}\n />\n </section>\n\n <section className="rounded-2xl bg-foreground text-background p-8">\n <h2 className="text-xl font-bold mb-3 -tracking-[0.025em]">How to measure yourself</h2>\n <ul className="space-y-3 text-background/85 leading-relaxed">\n <li>\n <strong className="text-background">Chest</strong> \u2014 Wrap a soft tape around\n the fullest part of your chest, under the arms, keeping the tape level.\n </li>\n <li>\n <strong className="text-background">Waist</strong> \u2014 Around the narrowest part\n of your waist, just above the navel.\n </li>\n <li>\n <strong className="text-background">Hip</strong> \u2014 Around the fullest part of\n your hips, feet together.\n </li>\n <li>\n <strong className="text-background">Inseam</strong> \u2014 From the very top of the\n inside of your leg down to the floor (no shoes).\n </li>\n </ul>\n </section>\n\n <p className="mt-12 text-sm text-muted-foreground">\n Still unsure?{" "}\n <a\n href={`mailto:${brand.faq.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n Email us your usual size in another brand\n </a>{" "}\n and we\'ll recommend a fit.\n </p>\n </article>\n );\n}\n\nfunction SizeTable({\n headings,\n rows,\n}: {\n headings: string[];\n rows: string[][];\n}) {\n return (\n <div className="overflow-x-auto rounded-2xl border border-border">\n <table className="w-full text-sm">\n <thead className="bg-muted">\n <tr>\n {headings.map((h) => (\n <th\n key={h}\n className="text-left font-semibold uppercase tracking-wider text-[11px] text-muted-foreground px-4 py-3"\n >\n {h}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((r, i) => (\n <tr\n key={r[0]}\n className={i % 2 === 0 ? "bg-background" : "bg-muted/30"}\n >\n {r.map((cell, j) => (\n <td\n key={j}\n className={[\n "px-4 py-3 tabular-nums",\n j === 0 ? "font-semibold text-foreground" : "text-foreground/80",\n ].join(" ")}\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed fashion --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed fashion",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check:visual": "playwright test",\n "check:visual:update": "playwright test --update-snapshots",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@playwright/test": "^1.50.0",\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
1014
+ ` }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s no longer in stock. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Browse the shop\n </Link>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <h1 className="text-3xl mt-0 mb-3 font-bold -tracking-[0.025em]">\n Order confirmed.\n </h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code> \u2014 you&apos;ll\n get an SMS with tracking within 30 minutes.\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/products/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { notFound } from "next/navigation";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Product,\n type ProductWithDetails,\n} from "@cimplify/sdk/server";\nimport { ProductDetail } from "./product-detail";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nfunction productLd(product: ProductWithDetails, SITE_URL: string) {\n const image = product.image_url ?? product.images?.[0];\n const inStock = product.inventory_status?.in_stock !== false;\n return {\n "@context": "https://schema.org",\n "@type": "Product",\n name: product.name,\n description: product.description ?? undefined,\n image: image ? [image] : undefined,\n sku: product.id,\n brand: { "@type": "Brand", name: brand.name },\n offers: {\n "@type": "Offer",\n price: product.default_price,\n priceCurrency: brand.currency,\n availability: inStock\n ? "https://schema.org/InStock"\n : "https://schema.org/OutOfStock",\n url: `${SITE_URL}/products/${product.slug ?? product.id}`,\n },\n };\n}\n\ninterface ProductData {\n product: ProductWithDetails;\n related: Product[];\n}\n\nasync function getProduct(slug: string): Promise<ProductData | null> {\n "use cache";\n cacheTag(tags.product(slug), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const r = await client.catalogue.getProductBySlug(slug);\n if (!r.ok) return null;\n\n const related = r.value.category_id\n ? await client.catalogue\n .getCategoryProducts(r.value.category_id)\n .then((res) =>\n res.ok\n ? (\n ((res.value as { items?: Product[] }).items ??\n (res.value as Product[])) as Product[]\n ).filter((p) => p.id !== r.value.id).slice(0, 4)\n : [],\n )\n : [];\n\n return { product: r.value, related };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const data = await getProduct(slug);\n if (!data) return {};\n const { product } = data;\n const image = product.image_url ?? product.images?.[0];\n return {\n title: `${product.name} \u2014 ${brand.name}`,\n description: product.description ?? undefined,\n openGraph: {\n title: product.name,\n description: product.description ?? undefined,\n images: image ? [{ url: image }] : undefined,\n type: "website",\n },\n };\n}\n\nexport default async function ProductPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<ProductSkeleton />}>\n <ProductContent params={params} />\n </Suspense>\n );\n}\n\nasync function ProductContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const siteUrl = await getSiteUrl();\n const data = await getProduct(slug);\n if (!data) notFound();\n\n return (\n <>\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product, siteUrl)) }}\n />\n <nav\n aria-label="Breadcrumb"\n className="max-w-7xl mx-auto px-6 sm:px-8 pt-6 text-[12px] font-mono text-muted-foreground flex items-center gap-2"\n >\n <Link href="/" className="hover:text-foreground transition-colors">\n Home\n </Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-foreground transition-colors">\n Shop\n </Link>\n {data.product.category?.slug && (\n <>\n <span>/</span>\n <Link\n href={`/categories/${data.product.category.slug}`}\n className="hover:text-foreground transition-colors"\n >\n {data.product.category.name}\n </Link>\n </>\n )}\n <span>/</span>\n <span className="text-foreground/90 truncate">{data.product.name}</span>\n </nav>\n <ProductDetail product={data.product} related={data.related} />\n </>\n );\n}\n\nfunction ProductSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 items-start">\n <div className="aspect-square bg-muted rounded-3xl animate-pulse" />\n <div className="space-y-4">\n <div className="h-3 w-24 bg-muted rounded animate-pulse" />\n <div className="h-10 w-3/4 bg-muted rounded animate-pulse" />\n <div className="h-7 w-32 bg-muted rounded animate-pulse" />\n <div className="h-20 w-full bg-muted rounded animate-pulse mt-6" />\n <div className="h-12 w-full bg-muted rounded animate-pulse mt-4" />\n </div>\n </div>\n </section>\n );\n}\n' }, { "path": "app/products/[slug]/product-detail.tsx", "kind": "text", "content": '"use client";\n\nimport Image from "next/image";\nimport { ProductPage as SdkProductPage, useCart } from "@cimplify/sdk/react";\nimport type { Product, ProductWithDetails } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the product detail page.\n *\n * - Receives a server-fetched `ProductWithDetails` (no client refetch).\n * - Renders the SDK `<ProductPage>` which picks the right layout\n * (Default / Wholesale / Service / Bundle / Composite) automatically.\n * - On add-to-cart success, routes to `/cart`.\n * - Custom Next.js Image renderer for optimised, lazy-loaded gallery shots.\n * - Renders a "You may also like" rail of in-category products below.\n */\nexport function ProductDetail({\n product,\n related,\n}: {\n product: ProductWithDetails;\n related: Product[];\n}) {\n const { addItem } = useCart();\n\n return (\n <>\n <SdkProductPage\n product={product}\n showRelated={false}\n onAddToCart={async (p, qty, options) => {\n await addItem(p, qty, options);\n }}\n renderImage={({ src, alt, className }) => (\n <Image\n src={src}\n alt={alt}\n width={1200}\n height={1200}\n className={className}\n style={{ width: "100%", height: "auto", objectFit: "cover" }}\n priority\n />\n )}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-8 sm:py-10"\n />\n\n {related.length > 0 && (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-16 border-t border-border mt-8">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n You may also like\n </p>\n <h2 className="text-[clamp(1.5rem,2.5vw,2rem)] font-bold m-0 -tracking-[0.025em]">\n More from this category.\n </h2>\n </div>\n </div>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {related.map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </section>\n )}\n </>\n );\n}\n' }, { "path": "app/login/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sign in \u2014 ${brand.name}`,\n description: brand.account.loginSubtitle,\n};\n\n/**\n * Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in\n * automatically when no session exists. We just bounce /login to /account\n * so consumers landing here get the right UI without a duplicate form.\n */\nexport default function LoginPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Collection, type Product } from "@cimplify/sdk/server";\nimport { FeatureHero } from "@/components/feature-hero";\nimport { CategoryTiles } from "@/components/category-tiles";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { PromoBanner } from "@/components/promo-banner";\nimport { TrustBar } from "@/components/trust-bar";\nimport { BrandMarquee } from "@/components/brand-marquee";\nimport { TradeInCta } from "@/components/trade-in-cta";\nimport { Newsletter } from "@/components/newsletter";\nimport { SectionHeading } from "@/components/section-heading";\nimport { StoreProductCard } from "@/components/store-product-card";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: brand.hero.title,\n description: brand.description,\n};\n\ninterface CollectionWithProducts {\n collection: Collection;\n products: Product[];\n}\n\nasync function getHomeData() {\n "use cache";\n cacheTag(tags.collections(), tags.categories(), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const [colRes, catRes, productsRes] = await Promise.all([\n client.catalogue.getCollections(),\n client.catalogue.getCategories(),\n client.catalogue.getProducts({ limit: 12 }),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n const allProducts = productsRes.ok ? productsRes.value.items : [];\n\n const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(col.id);\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n featured: allProducts.slice(0, 4),\n newArrivals: allProducts.slice(4, 12),\n };\n}\n\n// Verified-reachable Unsplash fashion editorial \u2014 replace with your own asset.\nconst HERO_FALLBACK_IMAGE =\n "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=2000&h=1400&fit=crop&auto=format&q=85";\n\nexport default async function HomePage() {\n const { collections, categories, featured, newArrivals } = await getHomeData();\n const heroProduct = featured[0];\n\n return (\n <>\n <FeatureHero\n eyebrow="Featured \xB7 in stock"\n badge="Just landed"\n title={brand.hero.title}\n description={heroProduct?.description ?? brand.hero.subtitle}\n primaryCta={{\n label: heroProduct ? `Shop ${heroProduct.name}` : brand.hero.primaryCtaLabel,\n href: heroProduct ? `/products/${heroProduct.slug ?? heroProduct.id}` : "/shop",\n }}\n secondaryCta={\n brand.hero.secondaryCtaLabel && brand.hero.secondaryCtaHref\n ? { label: brand.hero.secondaryCtaLabel, href: brand.hero.secondaryCtaHref }\n : undefined\n }\n imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_FALLBACK_IMAGE}\n imageAlt={heroProduct?.name ?? "Featured product"}\n />\n\n <TrustBar />\n\n <Suspense fallback={<CategoryTilesSkeleton />}>\n <CategoryTiles categories={categories} />\n </Suspense>\n\n <PromoBanner />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Just dropped"\n title="New this week."\n description="Hand-picked launches and freshly restocked staples."\n link={{ label: "Browse all", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(0, 4).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <BrandMarquee />\n\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n\n <TradeInCta />\n\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <SectionHeading\n eyebrow="Best sellers"\n title="What everyone&apos;s buying."\n link={{ label: "See more", href: "/shop" }}\n />\n <Suspense fallback={<GridSkeleton count={4} />}>\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {newArrivals.slice(4, 8).map((p) => (\n <StoreProductCard key={p.id} product={p} />\n ))}\n </div>\n </Suspense>\n </section>\n\n <Newsletter />\n </>\n );\n}\n\nfunction CategoryTilesSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">\n <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />\n <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction GridSkeleton({ count }: { count: number }) {\n return (\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">\n <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\nasync function getCategory(slug: string): Promise<CategoryData | null> {\n "use cache";\n cacheTag(tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug);\n if (!catRes.ok) return null;\n\n cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));\n const r = await client.catalogue.getCategoryProducts(catRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { category: catRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) return {};\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) notFound();\n\n const { category, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>\n <span>/</span>\n <span className="text-background/90">{category.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {category.name}\n </h1>\n {category.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {category.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "app/error.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Link from "next/link";\n\n/**\n * Root error boundary. Next.js calls this whenever a thrown error escapes\n * a Server Component without being caught by a closer `error.tsx`.\n *\n * Wire `reportError` (Sentry, Datadog, etc.) here so production failures\n * surface \u2014 silently recovering hides real bugs.\n */\nexport default function GlobalError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n // TODO: replace with your error reporter\n // eslint-disable-next-line no-console\n console.error("Storefront error:", error);\n }, [error]);\n\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n Something broke\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold mb-4 -tracking-[0.025em]">\n That wasn&apos;t supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We&apos;ve been notified. Refresh the page, or head back home \u2014 most\n of the time the second try just works.\n </p>\n {error.digest && (\n <p className="text-xs font-mono text-muted-foreground mb-6">\n Reference:{" "}\n <code className="text-foreground">{error.digest}</code>\n </p>\n )}\n <div className="flex flex-wrap items-center justify-center gap-3">\n <button\n type="button"\n onClick={reset}\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Try again\n </button>\n <Link\n href="/"\n className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Studio FRX \u2014 Nike/Aritzia-style streetwear palette. High-contrast\n neutrals with one electric accent. Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.99 0 0);\n --color-foreground: oklch(0.12 0 0);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.12 0 0);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.12 0 0);\n --color-primary: oklch(0.7 0.24 30);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.96 0 0);\n --color-secondary-foreground: oklch(0.18 0 0);\n --color-muted: oklch(0.96 0 0);\n --color-muted-foreground: oklch(0.45 0 0);\n --color-accent: oklch(0.92 0 0);\n --color-accent-foreground: oklch(0.18 0 0);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0 0);\n --color-input: oklch(0.93 0 0);\n --color-ring: oklch(0.7 0.24 30);\n --radius: 0.125rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-display);\n letter-spacing: -0.04em;\n font-weight: 800;\n line-height: 0.95;\n }\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Anton } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst anton = Anton({\n subsets: ["latin"],\n variable: "--font-display",\n weight: "400",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${anton.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/size-guide/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Size Guide \u2014 ${brand.name}`,\n description: "Chest, length, and shoulder measurements for every Studio FRX piece.",\n};\n\ninterface SizeRow {\n size: string;\n chest: string;\n length: string;\n shoulder: string;\n}\n\nconst TOPS_CHART: SizeRow[] = [\n { size: "XS", chest: "104 cm / 41 in", length: "68 cm / 26.8 in", shoulder: "50 cm / 19.7 in" },\n { size: "S", chest: "110 cm / 43.3 in", length: "70 cm / 27.5 in", shoulder: "52 cm / 20.5 in" },\n { size: "M", chest: "116 cm / 45.7 in", length: "72 cm / 28.3 in", shoulder: "54 cm / 21.3 in" },\n { size: "L", chest: "122 cm / 48 in", length: "74 cm / 29.1 in", shoulder: "56 cm / 22 in" },\n { size: "XL", chest: "128 cm / 50.4 in", length: "76 cm / 29.9 in", shoulder: "58 cm / 22.8 in" },\n { size: "2XL", chest: "134 cm / 52.7 in", length: "78 cm / 30.7 in", shoulder: "60 cm / 23.6 in" },\n { size: "3XL", chest: "140 cm / 55.1 in", length: "80 cm / 31.5 in", shoulder: "62 cm / 24.4 in" },\n];\n\nconst BOTTOMS_CHART: { size: string; waist: string; hip: string; inseam: string }[] = [\n { size: "XS", waist: "72 cm / 28 in", hip: "92 cm / 36 in", inseam: "78 cm / 30.7 in" },\n { size: "S", waist: "76 cm / 30 in", hip: "96 cm / 38 in", inseam: "78 cm / 30.7 in" },\n { size: "M", waist: "82 cm / 32 in", hip: "102 cm / 40 in", inseam: "80 cm / 31.5 in" },\n { size: "L", waist: "88 cm / 35 in", hip: "108 cm / 42.5 in", inseam: "80 cm / 31.5 in" },\n { size: "XL", waist: "94 cm / 37 in", hip: "114 cm / 45 in", inseam: "82 cm / 32.3 in" },\n { size: "2XL", waist: "102 cm / 40 in", hip: "122 cm / 48 in", inseam: "82 cm / 32.3 in" },\n];\n\nexport default function SizeGuidePage() {\n return (\n <article className="max-w-4xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-2">\n Size guide\n </p>\n <h1 className="font-display text-[clamp(2.5rem,7vw,5rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.95]">\n Find your fit.\n </h1>\n <p className="text-base text-muted-foreground leading-relaxed max-w-2xl mb-12">\n Our pieces are cut for an oversized, dropped-shoulder fit. Order true to size for\n the intended fit; size down for a regular cut. All measurements are flat-laid\n garment measurements.\n </p>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Tops & outerwear</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Hoodies, tees, sweats, jackets. Chest measured pit-to-pit, doubled. Length\n measured from highest shoulder seam to hem.\n </p>\n <SizeTable\n headings={["Size", "Chest", "Length", "Shoulder"]}\n rows={TOPS_CHART.map((r) => [r.size, r.chest, r.length, r.shoulder])}\n />\n </section>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Bottoms</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Trousers, shorts, sweats. Waist measured flat across the top of the\n waistband, doubled.\n </p>\n <SizeTable\n headings={["Size", "Waist", "Hip", "Inseam"]}\n rows={BOTTOMS_CHART.map((r) => [r.size, r.waist, r.hip, r.inseam])}\n />\n </section>\n\n <section className="rounded-2xl bg-foreground text-background p-8">\n <h2 className="text-xl font-bold mb-3 -tracking-[0.025em]">How to measure yourself</h2>\n <ul className="space-y-3 text-background/85 leading-relaxed">\n <li>\n <strong className="text-background">Chest</strong> \u2014 Wrap a soft tape around\n the fullest part of your chest, under the arms, keeping the tape level.\n </li>\n <li>\n <strong className="text-background">Waist</strong> \u2014 Around the narrowest part\n of your waist, just above the navel.\n </li>\n <li>\n <strong className="text-background">Hip</strong> \u2014 Around the fullest part of\n your hips, feet together.\n </li>\n <li>\n <strong className="text-background">Inseam</strong> \u2014 From the very top of the\n inside of your leg down to the floor (no shoes).\n </li>\n </ul>\n </section>\n\n <p className="mt-12 text-sm text-muted-foreground">\n Still unsure?{" "}\n <a\n href={`mailto:${brand.faq.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n Email us your usual size in another brand\n </a>{" "}\n and we\'ll recommend a fit.\n </p>\n </article>\n );\n}\n\nfunction SizeTable({\n headings,\n rows,\n}: {\n headings: string[];\n rows: string[][];\n}) {\n return (\n <div className="overflow-x-auto rounded-2xl border border-border">\n <table className="w-full text-sm">\n <thead className="bg-muted">\n <tr>\n {headings.map((h) => (\n <th\n key={h}\n className="text-left font-semibold uppercase tracking-wider text-[11px] text-muted-foreground px-4 py-3"\n >\n {h}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((r, i) => (\n <tr\n key={r[0]}\n className={i % 2 === 0 ? "bg-background" : "bg-muted/30"}\n >\n {r.map((cell, j) => (\n <td\n key={j}\n className={[\n "px-4 py-3 tabular-nums",\n j === 0 ? "font-semibold text-foreground" : "text-foreground/80",\n ].join(" ")}\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed fashion --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed fashion",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check:visual": "playwright test",\n "check:visual:update": "playwright test --update-snapshots",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@playwright/test": "^1.50.0",\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
1015
1015
  * Brand & content configuration \u2014 single source of truth for every visible
1016
1016
  * string. Edit this file to rebrand. See ../AGENTS.md.
1017
1017
  */
@@ -1683,7 +1683,7 @@ function Field({
1683
1683
  font-weight: 700;
1684
1684
  }
1685
1685
  }
1686
- ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={inter.variable}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed grocery --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed grocery",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
1686
+ ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={inter.variable}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed grocery --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed grocery",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
1687
1687
  * Brand & content configuration \u2014 single source of truth for every visible
1688
1688
  * string. Edit this file to rebrand. See ../AGENTS.md.
1689
1689
  */
@@ -2255,7 +2255,7 @@ function Field({
2255
2255
  font-weight: 600;
2256
2256
  }
2257
2257
  }
2258
- ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, JetBrains_Mono } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst mono = JetBrains_Mono({\n subsets: ["latin"],\n variable: "--font-mono",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed auto --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed auto",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
2258
+ ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, JetBrains_Mono } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst mono = JetBrains_Mono({\n subsets: ["latin"],\n variable: "--font-mono",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed auto --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed auto",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
2259
2259
  * Brand & content configuration \u2014 the **single source of truth** for every
2260
2260
  * visible string in this storefront.
2261
2261
  *
@@ -3152,7 +3152,7 @@ function Field({
3152
3152
  letter-spacing: -0.012em;
3153
3153
  }
3154
3154
  }
3155
- ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Lora } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst lora = Lora({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${lora.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed restaurant --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed restaurant",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
3155
+ ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Lora } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst lora = Lora({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${lora.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed restaurant --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed restaurant",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
3156
3156
  * Brand & content configuration \u2014 single source of truth for every visible
3157
3157
  * string. Edit this file to rebrand. See ../AGENTS.md.
3158
3158
  */
@@ -3638,7 +3638,7 @@ function Field({
3638
3638
  </div>
3639
3639
  );
3640
3640
  }
3641
- ` }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/book/book-client.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, useState } from "react";\nimport { useRouter } from "next/navigation";\nimport type { Product } from "@cimplify/sdk";\nimport { useCart } from "@cimplify/sdk/react";\n\ninterface Slot {\n date: Date;\n label: string;\n}\n\n/**\n * Real booking flow:\n * 1. Pick a treatment (left rail)\n * 2. Pick a date (chips, today + next 13 days)\n * 3. Pick a slot (15-minute grid, 10am\u20137pm)\n * 4. Add to cart with the chosen slot as a cart-item note;\n * Cimplify Checkout finalises the booking.\n *\n * The slot grid is generated client-side as a placeholder. In production,\n * call `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react`\n * to fetch real availability from the Cimplify scheduling API.\n */\nexport function BookClient({ treatments }: { treatments: Product[] }) {\n const router = useRouter();\n const { addItem } = useCart();\n const [selectedTreatment, setSelectedTreatment] = useState<Product | undefined>(\n treatments[0],\n );\n const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());\n const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const dates = useMemo(() => {\n const now = new Date();\n return Array.from({ length: 14 }).map((_, i) => {\n const d = new Date(now);\n d.setDate(now.getDate() + i);\n d.setHours(0, 0, 0, 0);\n return d;\n });\n }, []);\n\n const slots = useMemo<Slot[]>(() => {\n const out: Slot[] = [];\n for (let h = 10; h <= 19; h++) {\n for (const m of [0, 30]) {\n const d = new Date(selectedDate);\n d.setHours(h, m, 0, 0);\n const label = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;\n out.push({ date: d, label });\n }\n }\n return out;\n }, [selectedDate]);\n\n const slotKey = (s: Slot) => s.date.toISOString();\n\n async function confirm() {\n if (!selectedTreatment || !selectedSlotKey) return;\n setSubmitting(true);\n try {\n await addItem(selectedTreatment, 1, {\n notes: `Booked for ${new Date(selectedSlotKey).toLocaleString()}`,\n });\n router.push("/checkout");\n } catch {\n setSubmitting(false);\n }\n }\n\n if (treatments.length === 0) {\n return (\n <p className="text-muted-foreground">\n No bookable treatments yet. Add a Service-type product to your catalogue first.\n </p>\n );\n }\n\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n {/* Treatments */}\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Treatment\n </p>\n <div className="space-y-2">\n {treatments.map((t) => {\n const active = selectedTreatment?.id === t.id;\n return (\n <button\n key={t.id}\n type="button"\n onClick={() => setSelectedTreatment(t)}\n className={[\n "w-full text-left rounded-2xl border p-4 transition-colors",\n active\n ? "border-primary bg-primary/5"\n : "border-border bg-card hover:border-foreground/30",\n ].join(" ")}\n >\n <div className="flex items-center justify-between gap-3">\n <div className="min-w-0">\n <p className="font-semibold text-sm m-0 truncate">{t.name}</p>\n <p className="text-xs text-muted-foreground m-0">\n {t.duration_minutes ? `${t.duration_minutes} min \xB7 ` : ""}\n {t.currency ?? "GHS"} {t.default_price}\n </p>\n </div>\n {active && (\n <span className="grid place-items-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs">\n \u2713\n </span>\n )}\n </div>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Date + slots */}\n <div className="rounded-2xl border border-border bg-card p-6">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Date\n </p>\n <div className="grid grid-cols-7 gap-1.5 mb-6">\n {dates.map((d) => {\n const active = d.toDateString() === selectedDate.toDateString();\n return (\n <button\n key={d.toISOString()}\n type="button"\n onClick={() => {\n setSelectedDate(d);\n setSelectedSlotKey(null);\n }}\n className={[\n "flex flex-col items-center justify-center py-2 rounded-md transition-colors",\n active\n ? "bg-foreground text-background"\n : "bg-background hover:bg-muted text-foreground",\n ].join(" ")}\n >\n <span className="text-[10px] uppercase tracking-wider opacity-60">\n {d.toLocaleString(undefined, { weekday: "short" })}\n </span>\n <span className="text-base font-semibold tabular-nums">{d.getDate()}</span>\n </button>\n );\n })}\n </div>\n\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Slot\n </p>\n <div className="grid grid-cols-3 sm:grid-cols-4 gap-2">\n {slots.map((s) => {\n const key = slotKey(s);\n const active = selectedSlotKey === key;\n return (\n <button\n key={key}\n type="button"\n onClick={() => setSelectedSlotKey(key)}\n className={[\n "py-2 rounded-md text-sm tabular-nums transition-colors",\n active\n ? "bg-primary text-primary-foreground"\n : "bg-background border border-border hover:border-primary",\n ].join(" ")}\n >\n {s.label}\n </button>\n );\n })}\n </div>\n\n <button\n type="button"\n onClick={confirm}\n disabled={!selectedTreatment || !selectedSlotKey || submitting}\n className="w-full mt-6 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"\n >\n {submitting\n ? "Confirming\u2026"\n : selectedSlotKey\n ? `Book ${selectedTreatment?.name} at ${new Date(selectedSlotKey).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`\n : "Pick a slot to book"}\n </button>\n </div>\n </div>\n );\n}\n' }, { "path": "app/book/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { BookClient } from "./book-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Book a treatment \u2014 ${brand.name}`,\n description: "Pick a treatment, pick a slot, you\'re booked.",\n};\n\nasync function getTreatments(): Promise<Product[]> {\n "use cache";\n cacheTag(tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const r = await client.catalogue.getProducts({ limit: 50 });\n if (!r.ok) return [];\n // Booking flow only handles service-typed products.\n return r.value.items.filter((p) => p.type === "service");\n}\n\nexport default async function BookPage() {\n const treatments = await getTreatments();\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Book a treatment\n </p>\n <h1 className="text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">\n Pick a treatment.<br />Pick a slot.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">\n Available windows for the next two weeks. Confirmation by SMS within a minute of\n booking. Free cancellation up to 24 hours before.\n </p>\n\n <Suspense fallback={<BookSkeleton />}>\n <BookClient treatments={treatments} />\n </Suspense>\n </article>\n );\n}\n\nfunction BookSkeleton() {\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-2">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n <div className="rounded-2xl border border-border bg-card p-8">\n <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />\n <div className="grid grid-cols-3 gap-2">\n {Array.from({ length: 9 }).map((_, i) => (\n <div key={i} className="h-10 bg-muted rounded animate-pulse" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n' }, { "path": "app/login/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sign in \u2014 ${brand.name}`,\n description: brand.account.loginSubtitle,\n};\n\n/**\n * Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in\n * automatically when no session exists. We just bounce /login to /account\n * so consumers landing here get the right UI without a duplicate form.\n */\nexport default function LoginPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nasync function getHomeData() {\n "use cache";\n cacheTag(tags.collections(), tags.categories(), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections(),\n client.catalogue.getCategories(),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(col.id);\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\nasync function getCategory(slug: string): Promise<CategoryData | null> {\n "use cache";\n cacheTag(tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug);\n if (!catRes.ok) return null;\n\n cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));\n const r = await client.catalogue.getCategoryProducts(catRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { category: catRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) return {};\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) notFound();\n\n const { category, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/error.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Link from "next/link";\n\n/**\n * Root error boundary. Next.js calls this whenever a thrown error escapes\n * a Server Component without being caught by a closer `error.tsx`.\n *\n * Wire `reportError` (Sentry, Datadog, etc.) here so production failures\n * surface \u2014 silently recovering hides real bugs.\n */\nexport default function GlobalError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n // TODO: replace with your error reporter\n // eslint-disable-next-line no-console\n console.error("Storefront error:", error);\n }, [error]);\n\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n Something broke\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-4">\n That wasn&apos;t supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We&apos;ve been notified. Refresh the page, or head back home \u2014\n most of the time the second try just works.\n </p>\n {error.digest && (\n <p className="text-xs font-mono text-muted-foreground mb-6">\n Reference: <code className="text-foreground">{error.digest}</code>\n </p>\n )}\n <div className="flex flex-wrap items-center justify-center gap-3">\n <button\n type="button"\n onClick={reset}\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Try again\n </button>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Serene Spa \u2014 calm sand + sage palette, soft contrast.\n Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.98 0.012 90);\n --color-foreground: oklch(0.22 0.02 140);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.22 0.02 140);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.22 0.02 140);\n --color-primary: oklch(0.46 0.06 150);\n --color-primary-foreground: oklch(0.99 0.01 90);\n --color-secondary: oklch(0.93 0.025 90);\n --color-secondary-foreground: oklch(0.3 0.04 140);\n --color-muted: oklch(0.95 0.018 90);\n --color-muted-foreground: oklch(0.5 0.02 140);\n --color-accent: oklch(0.9 0.035 90);\n --color-accent-foreground: oklch(0.3 0.04 140);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.91 0.018 90);\n --color-input: oklch(0.93 0.018 90);\n --color-ring: oklch(0.46 0.06 150);\n --radius: 1rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.015em;\n font-weight: 500;\n }\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Fraunces } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst fraunces = Fraunces({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${fraunces.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed services --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed services",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
3641
+ ` }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/book/book-client.tsx", "kind": "text", "content": '"use client";\n\nimport { useMemo, useState } from "react";\nimport { useRouter } from "next/navigation";\nimport type { Product } from "@cimplify/sdk";\nimport { useCart } from "@cimplify/sdk/react";\n\ninterface Slot {\n date: Date;\n label: string;\n}\n\n/**\n * Real booking flow:\n * 1. Pick a treatment (left rail)\n * 2. Pick a date (chips, today + next 13 days)\n * 3. Pick a slot (15-minute grid, 10am\u20137pm)\n * 4. Add to cart with the chosen slot as a cart-item note;\n * Cimplify Checkout finalises the booking.\n *\n * The slot grid is generated client-side as a placeholder. In production,\n * call `useAvailableSlots({ serviceId, date })` from `@cimplify/sdk/react`\n * to fetch real availability from the Cimplify scheduling API.\n */\nexport function BookClient({ treatments }: { treatments: Product[] }) {\n const router = useRouter();\n const { addItem } = useCart();\n const [selectedTreatment, setSelectedTreatment] = useState<Product | undefined>(\n treatments[0],\n );\n const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());\n const [selectedSlotKey, setSelectedSlotKey] = useState<string | null>(null);\n const [submitting, setSubmitting] = useState(false);\n\n const dates = useMemo(() => {\n const now = new Date();\n return Array.from({ length: 14 }).map((_, i) => {\n const d = new Date(now);\n d.setDate(now.getDate() + i);\n d.setHours(0, 0, 0, 0);\n return d;\n });\n }, []);\n\n const slots = useMemo<Slot[]>(() => {\n const out: Slot[] = [];\n for (let h = 10; h <= 19; h++) {\n for (const m of [0, 30]) {\n const d = new Date(selectedDate);\n d.setHours(h, m, 0, 0);\n const label = `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;\n out.push({ date: d, label });\n }\n }\n return out;\n }, [selectedDate]);\n\n const slotKey = (s: Slot) => s.date.toISOString();\n\n async function confirm() {\n if (!selectedTreatment || !selectedSlotKey) return;\n setSubmitting(true);\n try {\n await addItem(selectedTreatment, 1, {\n notes: `Booked for ${new Date(selectedSlotKey).toLocaleString()}`,\n });\n router.push("/checkout");\n } catch {\n setSubmitting(false);\n }\n }\n\n if (treatments.length === 0) {\n return (\n <p className="text-muted-foreground">\n No bookable treatments yet. Add a Service-type product to your catalogue first.\n </p>\n );\n }\n\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n {/* Treatments */}\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Treatment\n </p>\n <div className="space-y-2">\n {treatments.map((t) => {\n const active = selectedTreatment?.id === t.id;\n return (\n <button\n key={t.id}\n type="button"\n onClick={() => setSelectedTreatment(t)}\n className={[\n "w-full text-left rounded-2xl border p-4 transition-colors",\n active\n ? "border-primary bg-primary/5"\n : "border-border bg-card hover:border-foreground/30",\n ].join(" ")}\n >\n <div className="flex items-center justify-between gap-3">\n <div className="min-w-0">\n <p className="font-semibold text-sm m-0 truncate">{t.name}</p>\n <p className="text-xs text-muted-foreground m-0">\n {t.duration_minutes ? `${t.duration_minutes} min \xB7 ` : ""}\n {t.currency ?? "GHS"} {t.default_price}\n </p>\n </div>\n {active && (\n <span className="grid place-items-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs">\n \u2713\n </span>\n )}\n </div>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* Date + slots */}\n <div className="rounded-2xl border border-border bg-card p-6">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Date\n </p>\n <div className="grid grid-cols-7 gap-1.5 mb-6">\n {dates.map((d) => {\n const active = d.toDateString() === selectedDate.toDateString();\n return (\n <button\n key={d.toISOString()}\n type="button"\n onClick={() => {\n setSelectedDate(d);\n setSelectedSlotKey(null);\n }}\n className={[\n "flex flex-col items-center justify-center py-2 rounded-md transition-colors",\n active\n ? "bg-foreground text-background"\n : "bg-background hover:bg-muted text-foreground",\n ].join(" ")}\n >\n <span className="text-[10px] uppercase tracking-wider opacity-60">\n {d.toLocaleString(undefined, { weekday: "short" })}\n </span>\n <span className="text-base font-semibold tabular-nums">{d.getDate()}</span>\n </button>\n );\n })}\n </div>\n\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Slot\n </p>\n <div className="grid grid-cols-3 sm:grid-cols-4 gap-2">\n {slots.map((s) => {\n const key = slotKey(s);\n const active = selectedSlotKey === key;\n return (\n <button\n key={key}\n type="button"\n onClick={() => setSelectedSlotKey(key)}\n className={[\n "py-2 rounded-md text-sm tabular-nums transition-colors",\n active\n ? "bg-primary text-primary-foreground"\n : "bg-background border border-border hover:border-primary",\n ].join(" ")}\n >\n {s.label}\n </button>\n );\n })}\n </div>\n\n <button\n type="button"\n onClick={confirm}\n disabled={!selectedTreatment || !selectedSlotKey || submitting}\n className="w-full mt-6 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"\n >\n {submitting\n ? "Confirming\u2026"\n : selectedSlotKey\n ? `Book ${selectedTreatment?.name} at ${new Date(selectedSlotKey).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}`\n : "Pick a slot to book"}\n </button>\n </div>\n </div>\n );\n}\n' }, { "path": "app/book/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { BookClient } from "./book-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Book a treatment \u2014 ${brand.name}`,\n description: "Pick a treatment, pick a slot, you\'re booked.",\n};\n\nasync function getTreatments(): Promise<Product[]> {\n "use cache";\n cacheTag(tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const r = await client.catalogue.getProducts({ limit: 50 });\n if (!r.ok) return [];\n // Booking flow only handles service-typed products.\n return r.value.items.filter((p) => p.type === "service");\n}\n\nexport default async function BookPage() {\n const treatments = await getTreatments();\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n Book a treatment\n </p>\n <h1 className="text-[clamp(2rem,5vw,3rem)] font-semibold mb-3 -tracking-[0.02em]">\n Pick a treatment.<br />Pick a slot.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-10 max-w-xl">\n Available windows for the next two weeks. Confirmation by SMS within a minute of\n booking. Free cancellation up to 24 hours before.\n </p>\n\n <Suspense fallback={<BookSkeleton />}>\n <BookClient treatments={treatments} />\n </Suspense>\n </article>\n );\n}\n\nfunction BookSkeleton() {\n return (\n <div className="grid grid-cols-1 lg:grid-cols-[1fr_1.4fr] gap-8">\n <div className="space-y-2">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-16 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n <div className="rounded-2xl border border-border bg-card p-8">\n <div className="h-5 w-40 bg-muted rounded mb-4 animate-pulse" />\n <div className="grid grid-cols-3 gap-2">\n {Array.from({ length: 9 }).map((_, i) => (\n <div key={i} className="h-10 bg-muted rounded animate-pulse" />\n ))}\n </div>\n </div>\n </div>\n );\n}\n' }, { "path": "app/login/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sign in \u2014 ${brand.name}`,\n description: brand.account.loginSubtitle,\n};\n\n/**\n * Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in\n * automatically when no session exists. We just bounce /login to /account\n * so consumers landing here get the right UI without a duplicate form.\n */\nexport default function LoginPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nasync function getHomeData() {\n "use cache";\n cacheTag(tags.collections(), tags.categories(), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections(),\n client.catalogue.getCategories(),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(col.id);\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\nasync function getCategory(slug: string): Promise<CategoryData | null> {\n "use cache";\n cacheTag(tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug);\n if (!catRes.ok) return null;\n\n cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));\n const r = await client.catalogue.getCategoryProducts(catRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { category: catRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) return {};\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) notFound();\n\n const { category, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/error.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Link from "next/link";\n\n/**\n * Root error boundary. Next.js calls this whenever a thrown error escapes\n * a Server Component without being caught by a closer `error.tsx`.\n *\n * Wire `reportError` (Sentry, Datadog, etc.) here so production failures\n * surface \u2014 silently recovering hides real bugs.\n */\nexport default function GlobalError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n // TODO: replace with your error reporter\n // eslint-disable-next-line no-console\n console.error("Storefront error:", error);\n }, [error]);\n\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n Something broke\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-4">\n That wasn&apos;t supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We&apos;ve been notified. Refresh the page, or head back home \u2014\n most of the time the second try just works.\n </p>\n {error.digest && (\n <p className="text-xs font-mono text-muted-foreground mb-6">\n Reference: <code className="text-foreground">{error.digest}</code>\n </p>\n )}\n <div className="flex flex-wrap items-center justify-center gap-3">\n <button\n type="button"\n onClick={reset}\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Try again\n </button>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* Serene Spa \u2014 calm sand + sage palette, soft contrast.\n Tweak primary to retheme everything. */\n@theme {\n --color-background: oklch(0.98 0.012 90);\n --color-foreground: oklch(0.22 0.02 140);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.22 0.02 140);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.22 0.02 140);\n --color-primary: oklch(0.46 0.06 150);\n --color-primary-foreground: oklch(0.99 0.01 90);\n --color-secondary: oklch(0.93 0.025 90);\n --color-secondary-foreground: oklch(0.3 0.04 140);\n --color-muted: oklch(0.95 0.018 90);\n --color-muted-foreground: oklch(0.5 0.02 140);\n --color-accent: oklch(0.9 0.035 90);\n --color-accent-foreground: oklch(0.3 0.04 140);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.91 0.018 90);\n --color-input: oklch(0.93 0.018 90);\n --color-ring: oklch(0.46 0.06 150);\n --radius: 1rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.015em;\n font-weight: 500;\n }\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Fraunces } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst fraunces = Fraunces({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${fraunces.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed services --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed services",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
3642
3642
  * Brand & content configuration \u2014 single source of truth for every visible
3643
3643
  * string. Edit this file to rebrand. See ../AGENTS.md.
3644
3644
  */
@@ -4143,7 +4143,7 @@ function Field({
4143
4143
  </div>
4144
4144
  );
4145
4145
  }
4146
- ` }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/login/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sign in \u2014 ${brand.name}`,\n description: brand.account.loginSubtitle,\n};\n\n/**\n * Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in\n * automatically when no session exists. We just bounce /login to /account\n * so consumers landing here get the right UI without a duplicate form.\n */\nexport default function LoginPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nasync function getHomeData() {\n "use cache";\n cacheTag(tags.collections(), tags.categories(), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections(),\n client.catalogue.getCategories(),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(col.id);\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\nasync function getCategory(slug: string): Promise<CategoryData | null> {\n "use cache";\n cacheTag(tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug);\n if (!catRes.ok) return null;\n\n cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));\n const r = await client.catalogue.getCategoryProducts(catRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { category: catRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) return {};\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) notFound();\n\n const { category, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/error.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Link from "next/link";\n\n/**\n * Root error boundary. Next.js calls this whenever a thrown error escapes\n * a Server Component without being caught by a closer `error.tsx`.\n *\n * Wire `reportError` (Sentry, Datadog, etc.) here so production failures\n * surface \u2014 silently recovering hides real bugs.\n */\nexport default function GlobalError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n // TODO: replace with your error reporter\n // eslint-disable-next-line no-console\n console.error("Storefront error:", error);\n }, [error]);\n\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n Something broke\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-4">\n That wasn&apos;t supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We&apos;ve been notified. Refresh the page, or head back home \u2014\n most of the time the second try just works.\n </p>\n {error.digest && (\n <p className="text-xs font-mono text-muted-foreground mb-6">\n Reference: <code className="text-foreground">{error.digest}</code>\n </p>\n )}\n <div className="flex flex-wrap items-center justify-center gap-3">\n <button\n type="button"\n onClick={reset}\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Try again\n </button>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n/* Scan the SDK\'s bundled JS so utility classes used inside its components\n land in the compiled stylesheet alongside our own. */\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* The SDK declares its design tokens with `@theme reference`, meaning the\n consumer must supply the actual values. Warm bakery palette below \u2014 swap\n these to retheme the entire SDK + storefront in one place. */\n@theme {\n --color-background: oklch(0.985 0.007 80);\n --color-foreground: oklch(0.18 0.01 60);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.18 0.01 60);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.18 0.01 60);\n --color-primary: oklch(0.45 0.16 40);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.95 0.02 80);\n --color-secondary-foreground: oklch(0.25 0.04 50);\n --color-muted: oklch(0.96 0.01 80);\n --color-muted-foreground: oklch(0.5 0.02 60);\n --color-accent: oklch(0.92 0.05 70);\n --color-accent-foreground: oklch(0.3 0.06 40);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0.02 80);\n --color-input: oklch(0.92 0.02 80);\n --color-ring: oklch(0.6 0.1 40);\n --radius: 0.75rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.01em;\n }\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Playfair_Display } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst playfair = Playfair_Display({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${playfair.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed default --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed default",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
4146
+ ` }, { "path": "app/terms/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Terms of Service \u2014 ${brand.name}`,\n description: `The rules of the road for ordering from ${brand.name}.`,\n};\n\nexport default function TermsPage() {\n const t = brand.terms;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="font-serif text-[clamp(2.5rem,5vw,3.5rem)] font-semibold mb-4">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that&apos;s sold out for the day. Try the menu or head back home.\n </p>\n <div className="flex flex-wrap items-center justify-center gap-3">\n <Link\n href="/shop"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Browse the menu\n </Link>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/orders/[id]/page.tsx", "kind": "text", "content": 'import Link from "next/link";\n\nexport default async function OrderPage({ params }: { params: Promise<{ id: string }> }) {\n const { id } = await params;\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <h1 className="font-serif text-3xl mt-0 mb-3">Thanks \u2014 your order is confirmed</h1>\n <p className="text-muted-foreground">\n Order <code className="font-mono text-foreground">{id}</code>\n </p>\n <p className="mt-6">\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Continue shopping\n </Link>\n </p>\n </section>\n );\n}\n' }, { "path": "app/login/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { redirect } from "next/navigation";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sign in \u2014 ${brand.name}`,\n description: brand.account.loginSubtitle,\n};\n\n/**\n * Cimplify Link (the iframe in `<CimplifyAccount>`) handles sign-in\n * automatically when no session exists. We just bounce /login to /account\n * so consumers landing here get the right UI without a duplicate form.\n */\nexport default function LoginPage(): never {\n redirect("/account");\n}\n' }, { "path": "app/accessibility/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.accessibility.title} \u2014 ${brand.name}`,\n};\n\nexport default function AccessibilityPage() {\n return <PolicyPage policy={brand.accessibility} />;\n}\n' }, { "path": "app/shipping/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.shipping.title} \u2014 ${brand.name}`,\n description: brand.shipping.sections[0]?.body\n ? typeof brand.shipping.sections[0].body === "string"\n ? brand.shipping.sections[0].body\n : brand.shipping.sections[0].body.intro\n : undefined,\n};\n\nexport default function ShippingPage() {\n return <PolicyPage policy={brand.shipping} />;\n}\n' }, { "path": "app/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { Hero } from "@/components/hero";\nimport { CollectionStrip } from "@/components/collection-strip";\nimport { CategoryGrid } from "@/components/category-grid";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `${brand.name} \u2014 ${brand.hero.title}`,\n description: brand.description,\n};\n\nasync function getHomeData() {\n "use cache";\n cacheTag(tags.collections(), tags.categories(), tags.products());\n cacheLife("hours");\n\n const client = getServerClient();\n const [colRes, catRes] = await Promise.all([\n client.catalogue.getCollections(),\n client.catalogue.getCategories(),\n ]);\n const collections = colRes.ok ? colRes.value : [];\n const categories = catRes.ok ? catRes.value : [];\n\n const collectionsWithProducts = await Promise.all(\n collections.map(async (col) => {\n const r = await client.catalogue.getCollectionProducts(col.id);\n const items = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: col, products: items };\n }),\n );\n\n return {\n collections: collectionsWithProducts.filter((x) => x.products.length > 0),\n categories,\n };\n}\n\nexport default async function HomePage() {\n const { collections, categories } = await getHomeData();\n\n return (\n <>\n <Hero\n badge={brand.hero.badge}\n title={brand.hero.title}\n subtitle={brand.hero.subtitle}\n />\n {collections.map(({ collection, products }) => (\n <Suspense\n key={collection.id}\n fallback={<StripSkeleton title={collection.name} />}\n >\n <CollectionStrip\n collection={collection}\n products={products}\n collectionHref={`/collections/${collection.slug}`}\n />\n </Suspense>\n ))}\n <Suspense fallback={<CategoryGridSkeleton />}>\n <CategoryGrid categories={categories} />\n </Suspense>\n </>\n );\n}\n\nfunction StripSkeleton({ title }: { title: string }) {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <h2 className="font-serif text-[28px] font-semibold m-0 mb-5">{title}</h2>\n <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">\n {Array.from({ length: 5 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n\nfunction CategoryGridSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-14">\n <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/categories/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the category listing. Receives server-fetched products\n * as props (serializable) and owns the `renderCard` function.\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this category yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/categories/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Category,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\nasync function getCategory(slug: string): Promise<CategoryData | null> {\n "use cache";\n cacheTag(tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug);\n if (!catRes.ok) return null;\n\n cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));\n const r = await client.catalogue.getCategoryProducts(catRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { category: catRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) return {};\n return {\n title: `${data.category.name} \u2014 ${brand.name}`,\n description: data.category.description ?? undefined,\n };\n}\n\nexport default async function CategoryPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CategorySkeleton />}>\n <CategoryContent params={params} />\n </Suspense>\n );\n}\n\nasync function CategoryContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCategory(slug);\n if (!data) notFound();\n\n const { category, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Category\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {category.name}\n </h1>\n {category.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {category.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CategorySkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "app/error.tsx", "kind": "text", "content": '"use client";\n\nimport { useEffect } from "react";\nimport Link from "next/link";\n\n/**\n * Root error boundary. Next.js calls this whenever a thrown error escapes\n * a Server Component without being caught by a closer `error.tsx`.\n *\n * Wire `reportError` (Sentry, Datadog, etc.) here so production failures\n * surface \u2014 silently recovering hides real bugs.\n */\nexport default function GlobalError({\n error,\n reset,\n}: {\n error: Error & { digest?: string };\n reset: () => void;\n}) {\n useEffect(() => {\n // TODO: replace with your error reporter\n // eslint-disable-next-line no-console\n console.error("Storefront error:", error);\n }, [error]);\n\n return (\n <section className="max-w-2xl mx-auto px-8 py-20 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-3">\n Something broke\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-4">\n That wasn&apos;t supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We&apos;ve been notified. Refresh the page, or head back home \u2014\n most of the time the second try just works.\n </p>\n {error.digest && (\n <p className="text-xs font-mono text-muted-foreground mb-6">\n Reference: <code className="text-foreground">{error.digest}</code>\n </p>\n )}\n <div className="flex flex-wrap items-center justify-center gap-3">\n <button\n type="button"\n onClick={reset}\n className="inline-block px-6 py-3 rounded-full bg-primary text-primary-foreground font-semibold text-sm transition-colors hover:bg-primary/90"\n >\n Try again\n </button>\n <Link\n href="/"\n className="inline-block px-6 py-3 rounded-full border border-border hover:bg-muted transition-colors text-sm font-medium"\n >\n Back home\n </Link>\n </div>\n </section>\n );\n}\n' }, { "path": "app/globals.css", "kind": "text", "content": '@import "tailwindcss";\n\n/* Scan the SDK\'s bundled JS so utility classes used inside its components\n land in the compiled stylesheet alongside our own. */\n@source "../node_modules/@cimplify/sdk/dist";\n\n/* The SDK declares its design tokens with `@theme reference`, meaning the\n consumer must supply the actual values. Warm bakery palette below \u2014 swap\n these to retheme the entire SDK + storefront in one place. */\n@theme {\n --color-background: oklch(0.985 0.007 80);\n --color-foreground: oklch(0.18 0.01 60);\n --color-card: oklch(1 0 0);\n --color-card-foreground: oklch(0.18 0.01 60);\n --color-popover: oklch(1 0 0);\n --color-popover-foreground: oklch(0.18 0.01 60);\n --color-primary: oklch(0.45 0.16 40);\n --color-primary-foreground: oklch(0.99 0 0);\n --color-secondary: oklch(0.95 0.02 80);\n --color-secondary-foreground: oklch(0.25 0.04 50);\n --color-muted: oklch(0.96 0.01 80);\n --color-muted-foreground: oklch(0.5 0.02 60);\n --color-accent: oklch(0.92 0.05 70);\n --color-accent-foreground: oklch(0.3 0.06 40);\n --color-destructive: oklch(0.58 0.24 27);\n --color-destructive-foreground: oklch(0.99 0 0);\n --color-border: oklch(0.9 0.02 80);\n --color-input: oklch(0.92 0.02 80);\n --color-ring: oklch(0.6 0.1 40);\n --radius: 0.75rem;\n}\n\n@layer base {\n html, body {\n margin: 0;\n padding: 0;\n background: var(--color-background);\n color: var(--color-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n a { color: inherit; text-decoration: none; }\n h1, h2, h3, h4 {\n font-family: var(--font-serif);\n letter-spacing: -0.01em;\n }\n}\n' }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="The Menu"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return <ShopClient products={products} categories={categories} />;\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, Playfair_Display } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst playfair = Playfair_Display({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${playfair.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Collection\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mx-auto mb-2 max-w-xl text-muted-foreground">\n {collection.description}\n </p>\n )}\n <p className="text-sm text-muted-foreground">\n {products.length} item{products.length === 1 ? "" : "s"}\n </p>\n </header>\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/" className="text-primary font-semibold">\n \u2190 Back home\n </Link>\n </p>\n )}\n </section>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <section className="max-w-7xl mx-auto px-8 pt-12">\n <header className="mb-8 text-center">\n <div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />\n <div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />\n </header>\n <div className="grid grid-cols-2 md:grid-cols-3 gap-4">\n {Array.from({ length: 6 }).map((_, i) => (\n <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed default --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed default",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
4147
4147
  * Brand & content configuration \u2014 the **single source of truth** for every
4148
4148
  * visible string in this storefront.
4149
4149
  *
@@ -4910,7 +4910,7 @@ function Field({
4910
4910
  font-weight: 600;
4911
4911
  }
4912
4912
  }
4913
- ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, JetBrains_Mono } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst mono = JetBrains_Mono({\n subsets: ["latin"],\n variable: "--font-mono",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed pharmacy --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed pharmacy",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.46.1",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
4913
+ ` }, { "path": "app/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Sitemap \u2014 ${brand.name}`,\n description: "A human-readable index of every page on this site.",\n};\n\ninterface SitemapData {\n products: { slug: string; name: string }[];\n categories: { slug: string; name: string }[];\n collections: { slug: string; name: string }[];\n}\n\nasync function getSitemap(): Promise<SitemapData> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n return {\n products: (pRes.ok ? pRes.value.items : []).map((p: Product) => ({\n slug: p.slug ?? p.id,\n name: p.name,\n })),\n categories: (cRes.ok ? cRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n collections: (colRes.ok ? colRes.value : []).map((c) => ({ slug: c.slug, name: c.name })),\n };\n}\n\nconst STATIC_LINKS: { title: string; links: { href: string; label: string }[] }[] = [\n {\n title: "Browse",\n links: [\n { href: "/", label: "Home" },\n { href: "/shop", label: "Shop" },\n { href: "/search", label: "Search" },\n ],\n },\n {\n title: "Account",\n links: [\n { href: "/account", label: "Account" },\n { href: "/account/orders", label: "Orders" },\n { href: "/account/addresses", label: "Addresses" },\n { href: "/account/settings", label: "Settings" },\n { href: "/login", label: "Sign in" },\n { href: "/signup", label: "Create account" },\n { href: "/track-order", label: "Track an order" },\n { href: "/cart", label: "Cart" },\n { href: "/checkout", label: "Checkout" },\n ],\n },\n {\n title: "About",\n links: [\n { href: "/about", label: "About" },\n { href: "/faq", label: "FAQ" },\n { href: "/contact", label: "Contact" },\n ],\n },\n {\n title: "Policies",\n links: [\n { href: "/shipping", label: "Shipping" },\n { href: "/returns", label: "Returns" },\n { href: "/accessibility", label: "Accessibility" },\n { href: "/terms", label: "Terms of Service" },\n { href: "/privacy", label: "Privacy Policy" },\n ],\n },\n {\n title: "Machine-readable",\n links: [\n { href: "/sitemap.xml", label: "sitemap.xml (search engines)" },\n { href: "/llms.txt", label: "llms.txt (LLM agents)" },\n { href: "/robots.txt", label: "robots.txt" },\n { href: "/opensearch.xml", label: "opensearch.xml (browser search)" },\n ],\n },\n];\n\nexport default async function SitemapHtmlPage() {\n const { products, categories, collections } = await getSitemap();\n\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Sitemap\n </p>\n <h1 className="text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2 -tracking-[0.02em]">\n Every page, in one place.\n </h1>\n <p className="text-muted-foreground mb-12">\n For search engines, see <Link href="/sitemap.xml" className="text-primary hover:underline">/sitemap.xml</Link>.\n For LLM agents, see <Link href="/llms.txt" className="text-primary hover:underline">/llms.txt</Link>.\n </p>\n <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-10">\n {STATIC_LINKS.map((s) => (\n <Section key={s.title} title={s.title}>\n {s.links.map((l) => (\n <li key={l.href}>\n <Link href={l.href} className="hover:text-primary transition-colors">\n {l.label}\n </Link>\n </li>\n ))}\n </Section>\n ))}\n {categories.length > 0 && (\n <Section title="Categories">\n {categories.map((c) => (\n <li key={c.slug}>\n <Link href={`/categories/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {collections.length > 0 && (\n <Section title="Collections">\n {collections.map((c) => (\n <li key={c.slug}>\n <Link href={`/collections/${c.slug}`} className="hover:text-primary transition-colors">\n {c.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n {products.length > 0 && (\n <Section title={`Products (${products.length})`}>\n {products.map((p) => (\n <li key={p.slug}>\n <Link href={`/shop?product=${encodeURIComponent(p.slug)}`} className="hover:text-primary transition-colors">\n {p.name}\n </Link>\n </li>\n ))}\n </Section>\n )}\n </div>\n </article>\n );\n}\n\nfunction Section({ title, children }: { title: string; children: React.ReactNode }) {\n return (\n <div>\n <p className="font-semibold text-[12px] uppercase tracking-[0.12em] text-foreground mb-3">\n {title}\n </p>\n <ul className="space-y-2 m-0 p-0 list-none text-sm text-muted-foreground">\n {children}\n </ul>\n </div>\n );\n}\n' }, { "path": "app/shop/shop-client.tsx", "kind": "text", "content": '"use client";\n\nimport { CataloguePage } from "@cimplify/sdk/react";\nimport type { Category, Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the shop page. Server-side fetches all products and\n * categories (cached via `\'use cache\'` in `app/shop/page.tsx`), then hands\n * them to `<CataloguePage>` which owns the interactive filter / sort state.\n *\n * The page hero (in `app/shop/page.tsx`) already provides the title; we\n * pass an empty `title` and override the SDK heading via className so the\n * page reads as one continuous design.\n */\nexport function ShopClient({\n products,\n categories,\n}: {\n products: Product[];\n categories: Category[];\n}) {\n return (\n <CataloguePage\n title="All products"\n products={products}\n categories={categories}\n renderCard={(p) => <StoreProductCard product={p} />}\n className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"\n />\n );\n}\n' }, { "path": "app/shop/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags } from "@cimplify/sdk/server";\nimport { ShopClient } from "./shop-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Shop \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nasync function getShopData() {\n "use cache";\n cacheTag(tags.products(), tags.categories());\n cacheLife("hours");\n\n const client = getServerClient();\n const [p, c] = await Promise.all([\n client.catalogue.getProducts({ limit: 50 }),\n client.catalogue.getCategories(),\n ]);\n return {\n products: p.ok ? p.value.items : [],\n categories: c.ok ? c.value : [],\n };\n}\n\nexport default async function ShopPage() {\n const { products, categories } = await getShopData();\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">\n Catalogue \xB7 {products.length} products\n </p>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">\n Every product we stock.\n </h1>\n <p className="mt-3 max-w-xl text-base text-background/75">\n Filter, sort, search. Authorised dealer pricing, two-year warranty,\n same-day Accra delivery on every order.\n </p>\n </div>\n </section>\n <Suspense\n fallback={\n <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </div>\n }\n >\n <ShopClient products={products} categories={categories} />\n </Suspense>\n </>\n );\n}\n' }, { "path": "app/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/layout.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Inter, JetBrains_Mono } from "next/font/google";\nimport "./globals.css";\nimport { Providers } from "@/components/providers";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst mono = JetBrains_Mono({\n subsets: ["latin"],\n variable: "--font-mono",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nasync function organizationLd() {\n const siteUrl = await getSiteUrl();\n return {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: siteUrl,\n description: brand.description,\n email: brand.contact.email,\n telephone: brand.contact.phoneTel,\n address: {\n "@type": "PostalAddress",\n streetAddress: brand.contact.streetAddress,\n addressLocality: brand.contact.city,\n addressCountry: brand.contact.countryCode,\n },\n sameAs: brand.socials.map((s) => s.href),\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const ld = await organizationLd();\n return (\n <html lang="en" suppressHydrationWarning className={`${inter.variable} ${mono.variable}`}>\n <body\n suppressHydrationWarning\n className="min-h-screen flex flex-col bg-background text-foreground font-sans"\n >\n <script\n type="application/ld+json"\n dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }}\n />\n <Providers>\n <Header />\n <main className="flex-1 pb-12 w-full">{children}</main>\n <Footer />\n <CartDrawer />\n </Providers>\n </body>\n </html>\n );\n}\n' }, { "path": "app/checkout/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";\n\nexport default function CheckoutPage() {\n const router = useRouter();\n return (\n <SdkCheckoutPage\n onComplete={(result) => {\n if (result.success && result.order) {\n router.push(`/orders/${result.order.id}`);\n }\n }}\n />\n );\n}\n' }, { "path": "app/privacy/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Privacy Policy \u2014 ${brand.name}`,\n description: `How ${brand.name} collects, uses, and protects your personal data.`,\n};\n\nexport default function PrivacyPage() {\n const p = brand.privacy;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {p.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {p.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {p.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {p.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/collections/[slug]/listing-client.tsx", "kind": "text", "content": '"use client";\n\nimport { ProductGrid } from "@cimplify/sdk/react";\nimport type { Product } from "@cimplify/sdk";\nimport { StoreProductCard } from "@/components/store-product-card";\n\n/**\n * Client island for the collection listing. Receives server-fetched\n * products as props (serializable) and owns the `renderCard` function\n * (which can\'t cross the server/client boundary).\n */\nexport function ListingClient({ products }: { products: Product[] }) {\n return (\n <ProductGrid\n products={products}\n emptyMessage="No products in this collection yet."\n renderCard={(p) => <StoreProductCard product={p} />}\n />\n );\n}\n' }, { "path": "app/collections/[slug]/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport Link from "next/link";\nimport { notFound } from "next/navigation";\nimport { cacheTag, cacheLife } from "next/cache";\nimport {\n getServerClient,\n tags,\n type Collection,\n type Product,\n} from "@cimplify/sdk/server";\nimport { ListingClient } from "./listing-client";\nimport { brand } from "@/lib/brand";\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\nasync function getCollection(slug: string): Promise<CollectionData | null> {\n "use cache";\n cacheTag(tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug);\n if (!colRes.ok) return null;\n\n cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));\n const r = await client.catalogue.getCollectionProducts(colRes.value.id);\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { collection: colRes.value, products };\n}\n\nexport async function generateMetadata({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}): Promise<Metadata> {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) return {};\n return {\n title: `${data.collection.name} \u2014 ${brand.name}`,\n description: data.collection.description ?? undefined,\n };\n}\n\nexport default async function CollectionPage({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n return (\n <Suspense fallback={<CollectionSkeleton />}>\n <CollectionContent params={params} />\n </Suspense>\n );\n}\n\nasync function CollectionContent({\n params,\n}: {\n params: Promise<{ slug: string }>;\n}) {\n const { slug } = await params;\n const data = await getCollection(slug);\n if (!data) notFound();\n\n const { collection, products } = data;\n return (\n <>\n <section className="bg-foreground text-background relative overflow-hidden">\n <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />\n <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">\n <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">\n <Link href="/" className="hover:text-background transition-colors">Home</Link>\n <span>/</span>\n <span className="text-background/90">Collections</span>\n <span>/</span>\n <span className="text-background/90">{collection.name}</span>\n </nav>\n <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">\n {collection.name}\n </h1>\n {collection.description && (\n <p className="mt-3 max-w-2xl text-base text-background/75">\n {collection.description}\n </p>\n )}\n <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">\n {products.length} {products.length === 1 ? "product" : "products"}\n </p>\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <ListingClient products={products} />\n {products.length === 0 && (\n <p className="text-center mt-8">\n <Link href="/shop" className="text-primary font-semibold hover:underline">\n \u2190 Browse all products\n </Link>\n </p>\n )}\n </section>\n </>\n );\n}\n\nfunction CollectionSkeleton() {\n return (\n <>\n <section className="bg-foreground py-12 sm:py-14">\n <div className="max-w-7xl mx-auto px-6 sm:px-8">\n <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />\n <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />\n </div>\n </section>\n <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">\n <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">\n {Array.from({ length: 8 }).map((_, i) => (\n <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />\n ))}\n </div>\n </section>\n </>\n );\n}\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed pharmacy --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed pharmacy",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.47.0",\n "next": "^16.2.4",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "lib/brand.ts", "kind": "text", "content": `/**
4914
4914
  * Brand & content configuration \u2014 the **single source of truth** for every
4915
4915
  * visible string in this storefront.
4916
4916
  *