@cimplify/cli 0.2.4 → 0.2.5

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.
@@ -530,7 +530,7 @@ function Field({
530
530
  letter-spacing: -0.012em;
531
531
  }
532
532
  }
533
- ` }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\nexport const revalidate = 3600;\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
533
+ ` }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
534
534
  # Dev: leave empty so the SDK uses the current origin (localhost:3000),
535
535
  # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
536
536
  # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
@@ -1081,7 +1081,7 @@ function Field({
1081
1081
  font-weight: 700;
1082
1082
  }
1083
1083
  }
1084
- ` }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\nexport const revalidate = 3600;\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
1084
+ ` }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
1085
1085
  # Dev: leave empty so the SDK uses the current origin (localhost:3000),
1086
1086
  # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
1087
1087
  # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
@@ -1782,7 +1782,7 @@ function Field({
1782
1782
  </div>
1783
1783
  );
1784
1784
  }
1785
- ` }, { "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/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\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/signup/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: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\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";\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\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport const metadata: Metadata = {\n metadataBase: new URL(SITE_URL),\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\nconst ORGANIZATION_LD = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: SITE_URL,\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\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\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(ORGANIZATION_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/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/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/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\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/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/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\nexport const revalidate = 3600;\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
1785
+ ` }, { "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/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\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/signup/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: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\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";\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\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport const metadata: Metadata = {\n metadataBase: new URL(SITE_URL),\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\nconst ORGANIZATION_LD = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: SITE_URL,\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\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\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(ORGANIZATION_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/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/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/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\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/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/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
1786
1786
  # Dev: leave empty so the SDK uses the current origin (localhost:3000),
1787
1787
  # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
1788
1788
  # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
@@ -2309,7 +2309,7 @@ function Field({
2309
2309
  </div>
2310
2310
  );
2311
2311
  }
2312
- ` }, { "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/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\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/signup/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: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\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";\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\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport const metadata: Metadata = {\n metadataBase: new URL(SITE_URL),\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\nconst ORGANIZATION_LD = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: SITE_URL,\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\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\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(ORGANIZATION_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/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/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/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/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/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\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/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/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\nexport const revalidate = 3600;\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
2312
+ ` }, { "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/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `FAQ \u2014 ${brand.name}`,\n description: "Delivery, pickup, custom cakes, allergens, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-10 -tracking-[0.02em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="font-serif text-2xl font-semibold mb-5">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-medium text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n // Bakery uses a URL-driven product modal (`?product=<slug>` on the home or\n // shop pages). Emit those as deep-linkable URLs so search engines and LLMs\n // can index each product canonically.\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/shop?product=${encodeURIComponent(p.slug ?? p.id)}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\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/signup/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: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\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";\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\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport const metadata: Metadata = {\n metadataBase: new URL(SITE_URL),\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\nconst ORGANIZATION_LD = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: SITE_URL,\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\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\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(ORGANIZATION_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/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/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/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/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/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\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/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/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-6 -tracking-[0.02em]">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-3xl font-semibold mt-10 mb-3">{s.heading}</h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full menu with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/shop?product=${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
2313
2313
  # Dev: leave empty so the SDK uses the current origin (localhost:3000),
2314
2314
  # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
2315
2315
  # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
@@ -3192,7 +3192,7 @@ function Field({
3192
3192
  font-weight: 600;
3193
3193
  }
3194
3194
  }
3195
- ` }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nfunction productLd(product: ProductWithDetails) {\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 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 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)) }}\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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\nexport const revalidate = 3600;\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
3195
+ ` }, { "path": "app/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/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";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nfunction productLd(product: ProductWithDetails) {\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 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 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)) }}\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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
3196
3196
  # Dev: leave empty so the SDK uses the current origin (localhost:3000),
3197
3197
  # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
3198
3198
  # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
@@ -3882,7 +3882,7 @@ function Field({
3882
3882
  </div>
3883
3883
  );
3884
3884
  }
3885
- ` }, { "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/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\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/signup/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: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\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";\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\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport const metadata: Metadata = {\n metadataBase: new URL(SITE_URL),\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\nconst ORGANIZATION_LD = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: SITE_URL,\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\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\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(ORGANIZATION_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/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/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/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/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\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/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/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/lookbook/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Image from "next/image";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Lookbook \u2014 ${brand.name}`,\n description: "Editorial photography from each Studio FRX drop.",\n};\n\ninterface LookbookEntry {\n drop: string;\n title: string;\n hero: string;\n tiles: string[];\n byline: string;\n date: string;\n}\n\n// Each entry is a drop; tiles are the editorial shots. Replace the URLs\n// with your own asset bucket / CDN before going live.\nconst ENTRIES: LookbookEntry[] = [\n {\n drop: "Drop 04",\n title: "Built for now.",\n date: "Spring 2026",\n byline: "Photographed by Selasi Adjei in Jamestown, Accra.",\n hero: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1485518882345-15568b007407?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 03",\n title: "Heavyweight, hand-screened.",\n date: "Winter 2025",\n byline: "Shot at the Tema dye works.",\n hero: "https://images.unsplash.com/photo-1467043153537-a4fba2cd39ef?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1542838686-37da4a9fd1b3?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 02",\n title: "From the studio floor.",\n date: "Autumn 2025",\n byline: "Studio days, Osu.",\n hero: "https://images.unsplash.com/photo-1525507119028-ed4c629a60a3?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1512436991641-6745cdb1723f?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1490114538077-0a7f8cb49891?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n];\n\nexport default function LookbookPage() {\n return (\n <article>\n <section className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-3">\n Lookbook\n </p>\n <h1 className="font-display text-[clamp(3rem,9vw,7rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.92]">\n The drops, in full.\n </h1>\n <p className="text-base text-muted-foreground max-w-2xl leading-relaxed">\n Each drop is photographed in the room it\'s built in. Editorials below;\n full collection in the shop.\n </p>\n </div>\n </section>\n\n {ENTRIES.map((entry, i) => (\n <section key={entry.drop} className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-16">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {entry.drop} \xB7 {entry.date}\n </p>\n <h2 className="font-display text-[clamp(2rem,5vw,3.5rem)] uppercase m-0 -tracking-[0.04em] leading-[0.95]">\n {entry.title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2">{entry.byline}</p>\n </div>\n <Link\n href="/shop"\n className="hidden sm:inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-foreground hover:text-primary transition-colors whitespace-nowrap"\n >\n Shop {entry.drop} \u2192\n </Link>\n </div>\n\n <div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden bg-muted mb-3">\n <Image\n src={entry.hero}\n alt={`${entry.drop} hero \u2014 ${entry.title}`}\n fill\n sizes="(min-width: 1280px) 1280px, 100vw"\n className="object-cover"\n priority={i === 0}\n />\n </div>\n\n <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">\n {entry.tiles.map((src, ti) => (\n <div\n key={src}\n className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-muted"\n >\n <Image\n src={src}\n alt={`${entry.drop} editorial ${ti + 1}`}\n fill\n sizes="(min-width: 768px) 33vw, 100vw"\n className="object-cover"\n />\n </div>\n ))}\n </div>\n </div>\n </section>\n ))}\n </article>\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";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nfunction productLd(product: ProductWithDetails) {\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 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 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)) }}\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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\nexport const revalidate = 3600;\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
3885
+ ` }, { "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/faq/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Support \u2014 ${brand.name}`,\n description: "Shipping, warranty, repairs, returns, payment \u2014 answers to the questions we hear most often.",\n};\n\nexport default function FaqPage() {\n const f = brand.faq;\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {f.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">\n {f.title}\n </h1>\n <div className="space-y-12">\n {f.sections.map((section) => (\n <section key={section.title}>\n <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>\n <dl className="space-y-6">\n {section.items.map((item) => (\n <div key={item.q}>\n <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>\n <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>\n </div>\n ))}\n </dl>\n </section>\n ))}\n </div>\n <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">\n {f.contactPrompt}{" "}\n <a\n href={`mailto:${f.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n {f.contactEmail}\n </a>{" "}\n and a real human will reply within 24 hours.\n </p>\n </article>\n );\n}\n' }, { "path": "app/opensearch.xml/route.ts", "kind": "text", "content": 'import { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\n/**\n * OpenSearch description document \u2014 lets browsers add this site to the\n * address bar\'s search engine list. When users press Tab after typing the\n * domain, they get an inline search box that hits /search?q=...\n */\nexport async function GET(): Promise<Response> {\n const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">\n <ShortName>${escapeXml(brand.shortName)}</ShortName>\n <Description>Search ${escapeXml(brand.name)}</Description>\n <InputEncoding>UTF-8</InputEncoding>\n <Url type="text/html" method="get" template="${SITE_URL}/search?q={searchTerms}" />\n <Url type="application/opensearchdescription+xml" rel="self" template="${SITE_URL}/opensearch.xml" />\n <moz:SearchForm>${SITE_URL}/search</moz:SearchForm>\n</OpenSearchDescription>\n`;\n return new Response(xml, {\n headers: {\n "Content-Type": "application/opensearchdescription+xml; charset=utf-8",\n "Cache-Control": "public, max-age=86400",\n },\n });\n}\n\nfunction escapeXml(s: string): string {\n return s\n .replace(/&/g, "&amp;")\n .replace(/</g, "&lt;")\n .replace(/>/g, "&gt;")\n .replace(/"/g, "&quot;")\n .replace(/\'/g, "&apos;");\n}\n' }, { "path": "app/sitemap.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getServerClient, type Product } from "@cimplify/sdk/server";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nconst STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [\n { path: "/", priority: 1.0, changeFrequency: "daily" },\n { path: "/shop", priority: 0.9, changeFrequency: "daily" },\n { path: "/about", priority: 0.5, changeFrequency: "monthly" },\n { path: "/faq", priority: 0.4, changeFrequency: "monthly" },\n { path: "/terms", priority: 0.2, changeFrequency: "monthly" },\n { path: "/privacy", priority: 0.2, changeFrequency: "monthly" },\n];\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n const now = new Date();\n const client = getServerClient();\n\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({\n url: `${SITE_URL}${r.path}`,\n lastModified: now,\n changeFrequency: r.changeFrequency,\n priority: r.priority,\n }));\n\n const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({\n url: `${SITE_URL}/products/${p.slug ?? p.id}`,\n lastModified: p.updated_at ? new Date(p.updated_at) : now,\n changeFrequency: "weekly",\n priority: 0.7,\n }));\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({\n url: `${SITE_URL}/categories/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({\n url: `${SITE_URL}/collections/${c.slug}`,\n lastModified: now,\n changeFrequency: "weekly",\n priority: 0.6,\n }));\n\n return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];\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/signup/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: `Create an account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\n/**\n * Cimplify Link handles sign-up inside the `<CimplifyAccount>` iframe.\n * We bounce /signup to /account; the iframe shows the create-account UI\n * for visitors with no session.\n */\nexport default function SignupPage(): never {\n redirect("/account");\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";\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\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport const metadata: Metadata = {\n metadataBase: new URL(SITE_URL),\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\nconst ORGANIZATION_LD = {\n "@context": "https://schema.org",\n "@type": brand.schemaType,\n name: brand.name,\n url: SITE_URL,\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\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\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(ORGANIZATION_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/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/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/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/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${SITE_URL}/sitemap.xml`,\n host: SITE_URL,\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/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/cart/page.tsx", "kind": "text", "content": '"use client";\n\nimport { useRouter } from "next/navigation";\nimport { CartPage as SdkCartPage } from "@cimplify/sdk/react";\n\nexport default function CartPage() {\n const router = useRouter();\n return <SdkCartPage onCheckout={() => router.push("/checkout")} />;\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/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/account/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Account \u2014 ${brand.name}`,\n description: brand.account.signupSubtitle,\n};\n\nexport default function AccountPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n {brand.account.accountEyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n {brand.account.accountTitle}\n </h1>\n <AccountIframe />\n </article>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Orders \u2014 ${brand.name}`,\n};\n\nexport default function OrdersPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your orders\n </h1>\n <AccountIframe section="orders" />\n </article>\n );\n}\n' }, { "path": "app/account/settings/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Settings \u2014 ${brand.name}`,\n};\n\nexport default function SettingsPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Settings\n </h1>\n <AccountIframe section="settings" />\n </article>\n );\n}\n' }, { "path": "app/account/addresses/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { AccountIframe } from "@/components/account-iframe";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Addresses \u2014 ${brand.name}`,\n};\n\nexport default function AddressesPage() {\n return (\n <article className="max-w-5xl mx-auto px-6 sm:px-8 py-12">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Account\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Your addresses\n </h1>\n <AccountIframe section="addresses" />\n </article>\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/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/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/about/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `About \u2014 ${brand.name}`,\n description: brand.description,\n};\n\nexport default function AboutPage() {\n const a = brand.about;\n // Title supports a single \\n for a hard line break.\n const titleParts = a.title.split("\\n");\n return (\n <article className="max-w-3xl mx-auto px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">\n {a.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">\n {titleParts.map((line, i) => (\n <span key={i}>\n {line}\n {i < titleParts.length - 1 && <br />}\n </span>\n ))}\n </h1>\n <div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">\n {a.paragraphs.map((p, i) => (\n <p key={i}>{p}</p>\n ))}\n {a.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">\n {s.heading}\n </h2>\n <p>{s.body}</p>\n </div>\n ))}\n </div>\n </article>\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/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/lookbook/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Image from "next/image";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Lookbook \u2014 ${brand.name}`,\n description: "Editorial photography from each Studio FRX drop.",\n};\n\ninterface LookbookEntry {\n drop: string;\n title: string;\n hero: string;\n tiles: string[];\n byline: string;\n date: string;\n}\n\n// Each entry is a drop; tiles are the editorial shots. Replace the URLs\n// with your own asset bucket / CDN before going live.\nconst ENTRIES: LookbookEntry[] = [\n {\n drop: "Drop 04",\n title: "Built for now.",\n date: "Spring 2026",\n byline: "Photographed by Selasi Adjei in Jamestown, Accra.",\n hero: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1483985988355-763728e1935b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1485518882345-15568b007407?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 03",\n title: "Heavyweight, hand-screened.",\n date: "Winter 2025",\n byline: "Shot at the Tema dye works.",\n hero: "https://images.unsplash.com/photo-1467043153537-a4fba2cd39ef?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1576566588028-4147f3842f27?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1542838686-37da4a9fd1b3?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n {\n drop: "Drop 02",\n title: "From the studio floor.",\n date: "Autumn 2025",\n byline: "Studio days, Osu.",\n hero: "https://images.unsplash.com/photo-1525507119028-ed4c629a60a3?w=1800&h=1100&fit=crop&auto=format&q=85",\n tiles: [\n "https://images.unsplash.com/photo-1512436991641-6745cdb1723f?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=900&h=1200&fit=crop&auto=format&q=85",\n "https://images.unsplash.com/photo-1490114538077-0a7f8cb49891?w=900&h=1200&fit=crop&auto=format&q=85",\n ],\n },\n];\n\nexport default function LookbookPage() {\n return (\n <article>\n <section className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-14">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-3">\n Lookbook\n </p>\n <h1 className="font-display text-[clamp(3rem,9vw,7rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.92]">\n The drops, in full.\n </h1>\n <p className="text-base text-muted-foreground max-w-2xl leading-relaxed">\n Each drop is photographed in the room it\'s built in. Editorials below;\n full collection in the shop.\n </p>\n </div>\n </section>\n\n {ENTRIES.map((entry, i) => (\n <section key={entry.drop} className="border-b border-border">\n <div className="max-w-7xl mx-auto px-6 sm:px-10 py-16">\n <div className="flex items-end justify-between gap-6 mb-8">\n <div>\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-2">\n {entry.drop} \xB7 {entry.date}\n </p>\n <h2 className="font-display text-[clamp(2rem,5vw,3.5rem)] uppercase m-0 -tracking-[0.04em] leading-[0.95]">\n {entry.title}\n </h2>\n <p className="text-sm text-muted-foreground mt-2">{entry.byline}</p>\n </div>\n <Link\n href="/shop"\n className="hidden sm:inline-flex items-center gap-2 text-sm font-semibold uppercase tracking-wider text-foreground hover:text-primary transition-colors whitespace-nowrap"\n >\n Shop {entry.drop} \u2192\n </Link>\n </div>\n\n <div className="relative w-full aspect-[16/9] rounded-2xl overflow-hidden bg-muted mb-3">\n <Image\n src={entry.hero}\n alt={`${entry.drop} hero \u2014 ${entry.title}`}\n fill\n sizes="(min-width: 1280px) 1280px, 100vw"\n className="object-cover"\n priority={i === 0}\n />\n </div>\n\n <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">\n {entry.tiles.map((src, ti) => (\n <div\n key={src}\n className="relative aspect-[3/4] rounded-2xl overflow-hidden bg-muted"\n >\n <Image\n src={src}\n alt={`${entry.drop} editorial ${ti + 1}`}\n fill\n sizes="(min-width: 768px) 33vw, 100vw"\n className="object-cover"\n />\n </div>\n ))}\n </div>\n </div>\n </section>\n ))}\n </article>\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";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nfunction productLd(product: ProductWithDetails) {\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 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 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)) }}\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/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": "app/track-order/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { TrackOrderForm } from "./track-order-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Track an order \u2014 ${brand.name}`,\n description: brand.trackOrder.body,\n};\n\nexport default function TrackOrderPage() {\n const t = brand.trackOrder;\n return (\n <article className="max-w-2xl 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 {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2rem,5vw,3rem)] font-semibold mb-4 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">{t.body}</p>\n <TrackOrderForm />\n </article>\n );\n}\n' }, { "path": "app/track-order/track-order-form.tsx", "kind": "text", "content": '"use client";\n\nimport { useState } from "react";\nimport { useRouter } from "next/navigation";\n\n/**\n * Guest order tracker. Routes /orders/<id> with the entered id; the\n * post-checkout confirmation page already lives at /orders/[id] so this\n * just sends the visitor there. Replace the redirect with a real lookup\n * (e.g. Cimplify orders API + email-match guard) for production.\n */\nexport function TrackOrderForm() {\n const router = useRouter();\n const [orderId, setOrderId] = useState("");\n const [email, setEmail] = useState("");\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n if (!orderId.trim()) return;\n router.push(`/orders/${encodeURIComponent(orderId.trim())}`);\n }}\n className="space-y-4 rounded-2xl border border-border bg-card p-6"\n >\n <div>\n <label\n htmlFor="orderId"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order number\n </label>\n <input\n id="orderId"\n name="orderId"\n required\n value={orderId}\n onChange={(e) => setOrderId(e.target.value)}\n placeholder="ord_abc123\u2026"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <div>\n <label\n htmlFor="email"\n className="text-xs font-semibold uppercase tracking-wider text-muted-foreground block mb-1.5"\n >\n Order email\n </label>\n <input\n id="email"\n name="email"\n type="email"\n required\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n placeholder="you@email.com"\n className="w-full px-4 py-3 rounded-md bg-background border border-border focus:border-primary focus:ring-2 focus:ring-primary/20 outline-none text-sm"\n />\n </div>\n <button\n type="submit"\n className="w-full inline-flex items-center justify-center px-5 py-3 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"\n >\n Track order\n </button>\n </form>\n );\n}\n' }, { "path": "app/.well-known/ucp/route.ts", "kind": "text", "content": 'import { NextResponse } from "next/server";\n\n/**\n * UCP (Universal Commerce Protocol) manifest discovery endpoint.\n *\n * Agents \u2014 Claude, ChatGPT, Gemini, MCP clients \u2014 probe\n * `https://<your-domain>/.well-known/ucp` to learn what commerce\n * capabilities your storefront supports. We forward the request to\n * Cimplify, which returns the canonical manifest; the response body\n * tells agents to make subsequent UCP calls directly to\n * `api.cimplify.io/ucp/v1/<handle>/*` (no per-request proxy here).\n *\n * Edge-cached for an hour because capabilities change rarely.\n */\nconst UCP_API_BASE =\n process.env.NEXT_PUBLIC_CIMPLIFY_API_URL || "https://api.cimplify.io";\n\n\nexport async function GET() {\n const businessHandle = process.env.NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE;\n\n if (!businessHandle) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_BUSINESS_HANDLE in .env.local (and your deployment env) to your business handle.",\n },\n { status: 500 },\n );\n }\n\n try {\n const response = await fetch(\n `${UCP_API_BASE}/ucp/v1/${businessHandle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!response.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${response.status}` },\n { status: response.status },\n );\n }\n\n const manifest = await response.json();\n return NextResponse.json(manifest, {\n headers: {\n "Content-Type": "application/json",\n "Cache-Control": "public, max-age=3600, s-maxage=3600",\n },\n });\n } catch (error) {\n return NextResponse.json(\n {\n error: "Failed to fetch UCP manifest",\n detail: error instanceof Error ? error.message : String(error),\n },\n { status: 500 },\n );\n }\n}\n' }, { "path": "app/llms.txt/route.ts", "kind": "text", "content": 'import { cacheTag, cacheLife } from "next/cache";\nimport { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\n\nconst SITE_URL =\n process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";\n\nasync function buildLlmsTxt(): Promise<string> {\n "use cache";\n cacheTag(tags.products(), tags.categories(), tags.collections());\n cacheLife("hours");\n\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts({ limit: 500 }),\n client.catalogue.getCategories(),\n client.catalogue.getCollections(),\n ]);\n\n const products: Product[] = productsRes.ok ? productsRes.value.items : [];\n const categories = categoriesRes.ok ? categoriesRes.value : [];\n const collections = collectionsRes.ok ? collectionsRes.value : [];\n\n const lines: string[] = [];\n lines.push(`# ${brand.name}`);\n lines.push("");\n lines.push(`> ${brand.llms.summary}`);\n lines.push("");\n lines.push("## Browse");\n lines.push(`- [Home](${SITE_URL}/)`);\n lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);\n\n if (categories.length > 0) {\n lines.push("");\n lines.push("## Categories");\n for (const c of categories) {\n lines.push(\n `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (collections.length > 0) {\n lines.push("");\n lines.push("## Collections");\n for (const c of collections) {\n lines.push(\n `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,\n );\n }\n }\n\n if (products.length > 0) {\n lines.push("");\n lines.push("## Products");\n for (const p of products) {\n const slug = p.slug ?? p.id;\n const price = `${brand.currency} ${p.default_price}`;\n const desc = p.description ? ` \u2014 ${p.description.replace(/\\s+/g, " ").slice(0, 200)}` : "";\n lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);\n }\n }\n\n lines.push("");\n lines.push("## Information");\n lines.push(`- [About](${SITE_URL}/about)`);\n lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);\n lines.push(`- [Terms of Service](${SITE_URL}/terms)`);\n lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);\n lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);\n lines.push("");\n lines.push("## Contact");\n lines.push(`- Email: ${brand.contact.email}`);\n lines.push(`- Phone: ${brand.contact.phone}`);\n lines.push(`- Address: ${brand.contact.address}`);\n\n return lines.join("\\n") + "\\n";\n}\n\n/**\n * `/llms.txt` \u2014 machine-readable site index for LLMs (per llmstxt.org).\n * Lets coding agents and chat assistants find products, categories, and\n * support pages without scraping HTML. Plain Markdown so it streams cheaply\n * into context windows.\n */\nexport async function GET(): Promise<Response> {\n const body = await buildLlmsTxt();\n return new Response(body, {\n headers: {\n "Content-Type": "text/plain; charset=utf-8",\n "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",\n },\n });\n}\n' }, { "path": "app/search/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { Suspense } from "react";\nimport { SearchClient } from "./search-client";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Search \u2014 ${brand.name}`,\n description: `Search ${brand.name} \u2014 products, collections, categories.`,\n};\n\nexport default function SearchPage() {\n return (\n <article className="max-w-7xl mx-auto px-6 sm:px-8 py-10">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">\n Search\n </p>\n <h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-8 -tracking-[0.02em]">\n Find anything.\n </h1>\n <Suspense fallback={<SearchSkeleton />}>\n <SearchClient />\n </Suspense>\n </article>\n );\n}\n\nfunction SearchSkeleton() {\n return (\n <div>\n <div className="h-12 w-full max-w-xl bg-muted rounded animate-pulse mb-8" />\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' }, { "path": "app/search/search-client.tsx", "kind": "text", "content": '"use client";\n\nimport { SearchPage } from "@cimplify/sdk/react";\n\nexport function SearchClient() {\n return <SearchPage />;\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/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": ".cursor/rules/cimplify-storefront.mdc", "kind": "binary", "content": "LS0tCmRlc2NyaXB0aW9uOiBDaW1wbGlmeSBzdG9yZWZyb250IOKAlCBhcmNoaXRlY3R1cmFsIHJ1bGVzIGFuZCByZWJyYW5kIGNvbnRyYWN0IGZvciBhIHByb2plY3Qgc2NhZmZvbGRlZCBmcm9tIGBjaW1wbGlmeSBpbml0YC4gVHJpZ2dlcnMgd2hlbiBmaWxlcyBsaWtlIGxpYi9icmFuZC50cywgYXBwL2dsb2JhbHMuY3NzLCBjb21wb25lbnRzL2hlYWRlci50c3gsIG9yIC5jaW1wbGlmeS9wcm9qZWN0Lmpzb24gYXJlIGluIHRoZSB3b3Jrc3BhY2UuCmdsb2JzOiBbImxpYi9icmFuZC50cyIsICJhcHAvKioiLCAiY29tcG9uZW50cy8qKiIsICIuZW52LmxvY2FsIiwgIm5leHQuY29uZmlnLnRzIl0KYWx3YXlzQXBwbHk6IHRydWUKLS0tCgojIENpbXBsaWZ5IHN0b3JlZnJvbnQKClRoaXMgcHJvamVjdCB3YXMgc2NhZmZvbGRlZCBmcm9tIGEgQ2ltcGxpZnkgdGVtcGxhdGUuIFJlYWQgYEFHRU5UUy5tZGAgYXQgdGhlIHByb2plY3Qgcm9vdCBmb3IgdGhlIGZpbGUg4oaUIGBicmFuZC5YYCBmaWVsZCBtYXAgYW5kIGAuY2xhdWRlL3NraWxscy9jaW1wbGlmeS1zdG9yZWZyb250L1NLSUxMLm1kYCBmb3IgdGhlIGZ1bGwgcGxheWJvb2suCgojIyBUaGUgY29udHJhY3QKCjEuICoqYGxpYi9icmFuZC50c2AqKiDigJQgZXZlcnkgdmlzaWJsZSBzdHJpbmcuIElmIHNvbWV0aGluZyBpc24ndCB0aGVyZSwgYWRkIGEgZmllbGQgdG8gdGhlIGBCcmFuZGAgaW50ZXJmYWNlOyBkb24ndCBoYXJkY29kZS4KMi4gKipgYXBwL2dsb2JhbHMuY3NzYCBgQHRoZW1lYCBibG9jayoqIOKAlCBwYWxldHRlLCByYWRpdXMsIGZvbnQgcmVmZXJlbmNlcy4KMy4gKipTZXJ2ZXIgQ29tcG9uZW50cyBhcmUgY2FjaGVkKiogdmlhIGAndXNlIGNhY2hlJ2AgKyBgY2FjaGVUYWcodGFncy5YKCkpYCArIGBjYWNoZUxpZmUoImhvdXJzIilgLiBEb24ndCBkb3duZ3JhZGUgdG8gY2xpZW50IHRvIHNpbGVuY2UgYSB3YXJuaW5nLgo0LiAqKkNsaWVudCBpc2xhbmRzKiogYmVoaW5kIGA8U3VzcGVuc2U+YC4KNS4gKipgY2FjaGVDb21wb25lbnRzOiB0cnVlYCoqIGluIGBuZXh0LmNvbmZpZy50c2AgaXMgbm9uLW5lZ290aWFibGUuCjYuIFVzZSAqKmBidW4gcnVuIHRlc3Q6cnVuYCoqICh2aXRlc3QpLCBub3QgYGJ1biB0ZXN0YC4KCiMjIERvbid0CgotIEhhcmRjb2RlIHZpc2libGUgc3RyaW5ncyBpbiBwYWdlcy9jb21wb25lbnRzLgotIFVzZSBgdW5zdGFibGVfY2FjaGVgIChOZXh0IDE2IHVzZXMgYCd1c2UgY2FjaGUnYCkuCi0gQnlwYXNzIGBnZXRTZXJ2ZXJDbGllbnQoKWAgKGxvc2VzIHBlci1yZXF1ZXN0IG1lbW9pemF0aW9uKS4KLSBNdXRhdGUgd2l0aG91dCBjYWxsaW5nIHRoZSBtYXRjaGluZyBgcmV2YWxpZGF0ZSpgIGhlbHBlciBmcm9tIGBAY2ltcGxpZnkvc2RrL3NlcnZlcmAuCg==" }, { "path": ".env.example", "kind": "text", "content": `# Cimplify API base URL.
3886
3886
  # Dev: leave empty so the SDK uses the current origin (localhost:3000),
3887
3887
  # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
3888
3888
  # Production: set to your Cimplify host (e.g. https://api.cimplify.io).