@cimplify/cli 0.14.0 → 0.15.0
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.
- package/dist/{add-PPUGTTDA.mjs → add-EPY25NNV.mjs} +3 -3
- package/dist/{assets-OEY6FHHZ.mjs → assets-CWB72PZG.mjs} +3 -3
- package/dist/{auth-step-up-ALEUQUCE.mjs → auth-step-up-VL742AS7.mjs} +4 -4
- package/dist/{cancel-FD66LTTS.mjs → cancel-ZHVVILOU.mjs} +3 -3
- package/dist/{chunk-IK6NQHTG.mjs → chunk-3KRSHO6J.mjs} +1 -1
- package/dist/{chunk-4O4G2CDE.mjs → chunk-3QUK5BPY.mjs} +7 -3
- package/dist/{chunk-SOYZZZVT.mjs → chunk-5SMYK3U2.mjs} +5 -5
- package/dist/{chunk-7CI5RWNE.mjs → chunk-A6WHBSXZ.mjs} +5 -5
- package/dist/{dev-L3W6UXYN.mjs → chunk-F2UI225N.mjs} +5 -5
- package/dist/{chunk-KAZMZD4X.mjs → chunk-GCQKTUXN.mjs} +2 -2
- package/dist/{chunk-RU53TQLZ.mjs → chunk-I2UP7YTG.mjs} +1 -1
- package/dist/{chunk-YIBOOKQQ.mjs → chunk-KN2YSKXZ.mjs} +1 -1
- package/dist/{chunk-6CHBU7O4.mjs → chunk-MLE4MVYC.mjs} +1 -1
- package/dist/{chunk-PDGKBATF.mjs → chunk-NK6VC264.mjs} +1 -1
- package/dist/{chunk-TOQGLBWF.mjs → chunk-NYXA6X56.mjs} +1 -1
- package/dist/{chunk-5ZQURRIM.mjs → chunk-TH4XENKH.mjs} +1 -1
- package/dist/{chunk-JOF7ESSA.mjs → chunk-YJDQYKZU.mjs} +8 -8
- package/dist/{deploy-64ZVAQ3R.mjs → deploy-JE6TUYCL.mjs} +53 -9
- package/dist/dev-V7JSUVOI.mjs +8 -0
- package/dist/dispatcher.mjs +29 -29
- package/dist/{doctor-6ZIFM5J5.mjs → doctor-TTI237ZY.mjs} +7 -7
- package/dist/{domains-7D7WJAMC.mjs → domains-BZKGIDF4.mjs} +4 -4
- package/dist/{env-PAENNABF.mjs → env-FZNXKLGL.mjs} +5 -5
- package/dist/{explain-REIDDRKC.mjs → explain-HVK5ZNTV.mjs} +2 -2
- package/dist/{inspect-MGVG4Z44.mjs → inspect-X3M7WSTL.mjs} +2 -2
- package/dist/introspect-KTB4BXU5.mjs +8 -0
- package/dist/{link-AWC7VJNN.mjs → link-4NM2QMI5.mjs} +4 -4
- package/dist/{lint-EYKIQ6RO.mjs → lint-DDZ7HUQN.mjs} +1 -1
- package/dist/{list-7ZL6FRKT.mjs → list-L6FMY3AF.mjs} +2 -2
- package/dist/{login-NCO4P5G5.mjs → login-ZTSWYO5G.mjs} +4 -4
- package/dist/{logout-MF6SOLPE.mjs → logout-Z57X6HPE.mjs} +2 -2
- package/dist/{logs-76TCJBIQ.mjs → logs-36BLYJHJ.mjs} +3 -3
- package/dist/{mock-CAWNXSGC.mjs → mock-EVER34IX.mjs} +1 -1
- package/dist/{projects-P65SUETQ.mjs → projects-4XRNHJAH.mjs} +3 -3
- package/dist/repo-X3KZ5R64.mjs +9 -0
- package/dist/{rollback-ZL3V7SYH.mjs → rollback-WCRWXSE3.mjs} +4 -4
- package/dist/{status-52HG63FE.mjs → status-VA326DWD.mjs} +3 -3
- package/dist/{unlink-TCG3NBAR.mjs → unlink-H3BO3DOQ.mjs} +2 -2
- package/dist/{update-MBEPZANS.mjs → update-XUDRJZDE.mjs} +3 -3
- package/dist/{whoami-B5Y5KSOZ.mjs → whoami-ZWTJMEDB.mjs} +3 -3
- package/package.json +2 -2
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/package.json +1 -1
- package/dist/introspect-APRI7R7K.mjs +0 -8
- package/dist/repo-ZPYZK3KW.mjs +0 -9
|
@@ -576,7 +576,7 @@ export const brand: Brand = {
|
|
|
576
576
|
businessId: "bus_default_akua_bakery",
|
|
577
577
|
},
|
|
578
578
|
};
|
|
579
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed default --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed default",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "bakery",\n "name": "Bakery",\n "tagline": "Warm, food-forward storefront with custom-order forms and same-day delivery checkout.",\n "industry": "food",\n "tags": ["food", "bakery", "pastry"],\n "stability": "stable",\n "schemaType": "Bakery",\n "mock": {\n "seedName": "default",\n "seedBusinessId": "bus_default_akua_bakery"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/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/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-6 sm: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/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\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-6 sm: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/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\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\nexport const revalidate = 3600;\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 const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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: "/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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 { getServerClient, tags } from "@cimplify/sdk/server";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { SupportChat } from "@/components/support-chat";\nimport { OrganizationJsonLd } from "@/components/json-ld";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst playfair = Playfair_Display({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const server = getServerClient();\n const [bizResult, locResult] = await Promise.all([\n server.business.getInfo({ cacheOptions: { revalidate: 3600, tags: [tags.business()] } }),\n server.business.getLocations({ cacheOptions: { revalidate: 3600, tags: [tags.locations()] } }),\n ]);\n const initialBusiness = bizResult.ok ? bizResult.value : null;\n const initialLocations = locResult.ok ? locResult.value : [];\n\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 <Suspense fallback={null}>\n <OrganizationJsonLd />\n </Suspense>\n <Providers initialBusiness={initialBusiness} initialLocations={initialLocations}>\n <Header />\n <main className="flex-1 pb-12 w-full">\n <Suspense fallback={null}>{children}</Suspense>\n </main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n <SupportChat />\n </Providers>\n </body>\n </html>\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-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't supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We'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/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-6 sm: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/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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(await getSiteUrl());\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/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\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 {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
|
|
579
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed default --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed default",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "bakery",\n "name": "Bakery",\n "tagline": "Warm, food-forward storefront with custom-order forms and same-day delivery checkout.",\n "industry": "food",\n "tags": ["food", "bakery", "pastry"],\n "stability": "stable",\n "schemaType": "Bakery",\n "mock": {\n "seedName": "default",\n "seedBusinessId": "bus_default_akua_bakery"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/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/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-6 sm: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/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\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-6 sm: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/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\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\nexport const revalidate = 3600;\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 const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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: "/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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 { getServerClient, tags } from "@cimplify/sdk/server";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { SupportChat } from "@/components/support-chat";\nimport { OrganizationJsonLd } from "@/components/json-ld";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst playfair = Playfair_Display({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const server = getServerClient();\n const [bizResult, locResult] = await Promise.all([\n server.business.getInfo({ cacheOptions: { revalidate: 3600, tags: [tags.business()] } }),\n server.business.getLocations({ cacheOptions: { revalidate: 3600, tags: [tags.locations()] } }),\n ]);\n const initialBusiness = bizResult.ok ? bizResult.value : null;\n const initialLocations = locResult.ok ? locResult.value : [];\n\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 <Suspense fallback={null}>\n <OrganizationJsonLd />\n </Suspense>\n <Providers initialBusiness={initialBusiness} initialLocations={initialLocations}>\n <Header />\n <main className="flex-1 pb-12 w-full">\n <Suspense fallback={null}>{children}</Suspense>\n </main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n <SupportChat />\n </Providers>\n </body>\n </html>\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-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't supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We'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/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-6 sm: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/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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(await getSiteUrl());\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/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\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 {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
|
|
580
580
|
|
|
581
581
|
import { useState } from "react";
|
|
582
582
|
|
|
@@ -1440,7 +1440,7 @@ export const brand: Brand = {
|
|
|
1440
1440
|
businessId: "bus_currents_electronics",
|
|
1441
1441
|
},
|
|
1442
1442
|
};
|
|
1443
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed retail --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed retail",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "retail",\n "name": "Retail",\n "tagline": "Variant-aware retail storefront with full product detail pages and instalment support.",\n "industry": "retail",\n "tags": ["retail", "electronics", "variants"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "retail",\n "seedBusinessId": "bus_currents_electronics"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
1443
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed retail --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed retail",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "retail",\n "name": "Retail",\n "tagline": "Variant-aware retail storefront with full product detail pages and instalment support.",\n "industry": "retail",\n "tags": ["retail", "electronics", "variants"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "retail",\n "seedBusinessId": "bus_currents_electronics"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
1444
1444
|
|
|
1445
1445
|
/* Scan the SDK's bundled JS so utility classes used inside its components
|
|
1446
1446
|
land in the compiled stylesheet alongside our own. */
|
|
@@ -2518,7 +2518,7 @@ export const brand: Brand = {
|
|
|
2518
2518
|
businessId: "bus_wellspring_pharmacy",
|
|
2519
2519
|
},
|
|
2520
2520
|
};
|
|
2521
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed pharmacy --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed pharmacy",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "pharmacy",\n "name": "Pharmacy",\n "tagline": "Licensed community pharmacy with prescription uploads, pharmacist consults, and OTC catalog.",\n "industry": "healthcare",\n "tags": ["healthcare", "regulated", "prescription"],\n "stability": "stable",\n "schemaType": "Pharmacy",\n "mock": {\n "seedName": "pharmacy",\n "seedBusinessId": "bus_wellspring_pharmacy"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
2521
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed pharmacy --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed pharmacy",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "pharmacy",\n "name": "Pharmacy",\n "tagline": "Licensed community pharmacy with prescription uploads, pharmacist consults, and OTC catalog.",\n "industry": "healthcare",\n "tags": ["healthcare", "regulated", "prescription"],\n "stability": "stable",\n "schemaType": "Pharmacy",\n "mock": {\n "seedName": "pharmacy",\n "seedBusinessId": "bus_wellspring_pharmacy"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
2522
2522
|
|
|
2523
2523
|
/* Scan the SDK's bundled JS so utility classes used inside its components
|
|
2524
2524
|
land in the compiled stylesheet alongside our own. */
|
|
@@ -3332,7 +3332,7 @@ export const brand: Brand = {
|
|
|
3332
3332
|
businessId: "bus_studio_frx",
|
|
3333
3333
|
},
|
|
3334
3334
|
};
|
|
3335
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "e2e/visual.spec.ts", "kind": "text", "content": 'import { test, expect } from "@playwright/test";\n\n/**\n * Visual regression on the storefront\'s key surfaces. Catches CSS / layout\n * drift that the contract harness can\'t see.\n *\n * Anti-flake notes:\n * - We disable animations (CSS transitions can leave half-frame snapshots).\n * - We mask images (Unsplash / CDN URLs occasionally render slight pixel\n * differences). The container layout is what we\'re regressing on, not\n * image bytes.\n * - Fonts are served by `next/font` and locked at build time, but if you\n * see flake on a fresh CI runner, `await page.evaluate(() => document.fonts.ready)`\n * before the snapshot.\n */\ntest.describe("visual regression", () => {\n test.beforeEach(async ({ page }) => {\n await page.addStyleTag({\n content: `*, *::before, *::after { animation: none !important; transition: none !important; }`,\n });\n });\n\n test("homepage", async ({ page }) => {\n await page.goto("/");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("homepage.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("shop / catalogue", async ({ page }) => {\n await page.goto("/shop");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("shop.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("cart drawer after adding a product", async ({ page }) => {\n await page.goto("/products/studio-tee-natural");\n await page.evaluate(() => document.fonts.ready);\n // Click whatever the storefront\'s primary Add-to-Cart button is.\n await page.getByRole("button", { name: /add to cart/i }).first().click();\n // Drawer auto-opens via CartDrawerProvider\'s openOnAdd watcher.\n await expect(page.getByRole("dialog", { name: /cart/i })).toBeVisible();\n await expect(page).toHaveScreenshot("cart-drawer.png", {\n mask: [page.locator("img")],\n });\n });\n});\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { expect } from "vitest";\nimport { brand } from "../lib/brand";\n\n/**\n * Cart flow suite \u2014 base + per-merchant assertions.\n *\n * The base suite (from `@cimplify/sdk/testing/suite`) exercises the\n * universal contract: empty cart, add, dedupe, remove, businessId\n * round-trip. The `extend` hook below adds Studio FRX-specific\n * invariants \u2014 copy this pattern when a merchant has unique business\n * rules that need their own coverage.\n *\n * \u2022 `getHandle()` returns the suite\'s current `TestClientHandle`\n * (per `it`) so the seed and session are already wired.\n * \u2022 `it` is the vitest API; cases land inside the suite\'s `describe`\n * so its `beforeEach`/`afterEach` hooks apply automatically.\n */\ncreateCartFlowSuite({\n seed: brand.mock.seed,\n businessId: brand.mock.businessId,\n extend: ({ getHandle, it }) => {\n it("Studio FRX prices everything in GHS", async () => {\n // Currency is brand-level (not per-product) \u2014 Product carries the price\n // string only. Catalog completeness here is just a sanity check.\n expect(brand.currency).toBe("GHS");\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: unknown[] }).items ?? [];\n expect(items.length).toBeGreaterThan(0);\n });\n\n it("every apparel product covers the studio\'s core size run (S/M/L)", async () => {\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: { id: string; name: string }[] }).items ?? [];\n\n // Apparel only \u2014 caps/socks/totes intentionally lack size variants.\n const apparel = items.filter((p) => /tee|hoodie|jacket|trouser|pant/i.test(p.name));\n expect(apparel.length).toBeGreaterThan(0);\n\n const required = new Set(["S", "M", "L"]);\n for (const product of apparel) {\n const variants = await h.client.catalogue.getVariants(product.id);\n if (!variants.ok) throw variants.error;\n const sizes = new Set(variants.value.map((v) => v.name).filter(Boolean));\n for (const size of required) {\n expect(sizes, `${product.name} missing size ${size}`).toContain(size);\n }\n }\n });\n },\n});\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed fashion --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed fashion",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check:visual": "playwright test",\n "check:visual:update": "playwright test --update-snapshots",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@playwright/test": "^1.50.0",\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "fashion",\n "name": "Fashion",\n "tagline": "Editorial multi-drop fashion storefront with lookbooks and size guides.",\n "industry": "fashion",\n "tags": ["fashion", "apparel", "lookbook"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "fashion",\n "seedBusinessId": "bus_studio_frx"\n }\n}\n' }, { "path": "app/size-guide/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Size Guide \u2014 ${brand.name}`,\n description: "Chest, length, and shoulder measurements for every Studio FRX piece.",\n};\n\ninterface SizeRow {\n size: string;\n chest: string;\n length: string;\n shoulder: string;\n}\n\nconst TOPS_CHART: SizeRow[] = [\n { size: "XS", chest: "104 cm / 41 in", length: "68 cm / 26.8 in", shoulder: "50 cm / 19.7 in" },\n { size: "S", chest: "110 cm / 43.3 in", length: "70 cm / 27.5 in", shoulder: "52 cm / 20.5 in" },\n { size: "M", chest: "116 cm / 45.7 in", length: "72 cm / 28.3 in", shoulder: "54 cm / 21.3 in" },\n { size: "L", chest: "122 cm / 48 in", length: "74 cm / 29.1 in", shoulder: "56 cm / 22 in" },\n { size: "XL", chest: "128 cm / 50.4 in", length: "76 cm / 29.9 in", shoulder: "58 cm / 22.8 in" },\n { size: "2XL", chest: "134 cm / 52.7 in", length: "78 cm / 30.7 in", shoulder: "60 cm / 23.6 in" },\n { size: "3XL", chest: "140 cm / 55.1 in", length: "80 cm / 31.5 in", shoulder: "62 cm / 24.4 in" },\n];\n\nconst BOTTOMS_CHART: { size: string; waist: string; hip: string; inseam: string }[] = [\n { size: "XS", waist: "72 cm / 28 in", hip: "92 cm / 36 in", inseam: "78 cm / 30.7 in" },\n { size: "S", waist: "76 cm / 30 in", hip: "96 cm / 38 in", inseam: "78 cm / 30.7 in" },\n { size: "M", waist: "82 cm / 32 in", hip: "102 cm / 40 in", inseam: "80 cm / 31.5 in" },\n { size: "L", waist: "88 cm / 35 in", hip: "108 cm / 42.5 in", inseam: "80 cm / 31.5 in" },\n { size: "XL", waist: "94 cm / 37 in", hip: "114 cm / 45 in", inseam: "82 cm / 32.3 in" },\n { size: "2XL", waist: "102 cm / 40 in", hip: "122 cm / 48 in", inseam: "82 cm / 32.3 in" },\n];\n\nexport default function SizeGuidePage() {\n return (\n <article className="max-w-4xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-2">\n Size guide\n </p>\n <h1 className="font-display text-[clamp(2.5rem,7vw,5rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.95]">\n Find your fit.\n </h1>\n <p className="text-base text-muted-foreground leading-relaxed max-w-2xl mb-12">\n Our pieces are cut for an oversized, dropped-shoulder fit. Order true to size for\n the intended fit; size down for a regular cut. All measurements are flat-laid\n garment measurements.\n </p>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Tops & outerwear</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Hoodies, tees, sweats, jackets. Chest measured pit-to-pit, doubled. Length\n measured from highest shoulder seam to hem.\n </p>\n <SizeTable\n headings={["Size", "Chest", "Length", "Shoulder"]}\n rows={TOPS_CHART.map((r) => [r.size, r.chest, r.length, r.shoulder])}\n />\n </section>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Bottoms</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Trousers, shorts, sweats. Waist measured flat across the top of the\n waistband, doubled.\n </p>\n <SizeTable\n headings={["Size", "Waist", "Hip", "Inseam"]}\n rows={BOTTOMS_CHART.map((r) => [r.size, r.waist, r.hip, r.inseam])}\n />\n </section>\n\n <section className="rounded-2xl bg-foreground text-background p-8">\n <h2 className="text-xl font-bold mb-3 -tracking-[0.025em]">How to measure yourself</h2>\n <ul className="space-y-3 text-background/85 leading-relaxed">\n <li>\n <strong className="text-background">Chest</strong> \u2014 Wrap a soft tape around\n the fullest part of your chest, under the arms, keeping the tape level.\n </li>\n <li>\n <strong className="text-background">Waist</strong> \u2014 Around the narrowest part\n of your waist, just above the navel.\n </li>\n <li>\n <strong className="text-background">Hip</strong> \u2014 Around the fullest part of\n your hips, feet together.\n </li>\n <li>\n <strong className="text-background">Inseam</strong> \u2014 From the very top of the\n inside of your leg down to the floor (no shoes).\n </li>\n </ul>\n </section>\n\n <p className="mt-12 text-sm text-muted-foreground">\n Still unsure?{" "}\n <a\n href={`mailto:${brand.faq.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n Email us your usual size in another brand\n </a>{" "}\n and we\'ll recommend a fit.\n </p>\n </article>\n );\n}\n\nfunction SizeTable({\n headings,\n rows,\n}: {\n headings: string[];\n rows: string[][];\n}) {\n return (\n <div className="overflow-x-auto rounded-2xl border border-border">\n <table className="w-full text-sm">\n <thead className="bg-muted">\n <tr>\n {headings.map((h) => (\n <th\n key={h}\n className="text-left font-semibold uppercase tracking-wider text-[11px] text-muted-foreground px-4 py-3"\n >\n {h}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((r, i) => (\n <tr\n key={r[0]}\n className={i % 2 === 0 ? "bg-background" : "bg-muted/30"}\n >\n {r.map((cell, j) => (\n <td\n key={j}\n className={[\n "px-4 py-3 tabular-nums",\n j === 0 ? "font-semibold text-foreground" : "text-foreground/80",\n ].join(" ")}\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n' }, { "path": "app/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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/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/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-6 sm: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/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\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-6 sm: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/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\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\nexport const revalidate = 3600;\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 const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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: "/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.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/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 { getServerClient, tags } from "@cimplify/sdk/server";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { SupportChat } from "@/components/support-chat";\nimport { OrganizationJsonLd } from "@/components/json-ld";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst anton = Anton({\n subsets: ["latin"],\n variable: "--font-display",\n weight: "400",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const server = getServerClient();\n const [bizResult, locResult] = await Promise.all([\n server.business.getInfo({ cacheOptions: { revalidate: 3600, tags: [tags.business()] } }),\n server.business.getLocations({ cacheOptions: { revalidate: 3600, tags: [tags.locations()] } }),\n ]);\n const initialBusiness = bizResult.ok ? bizResult.value : null;\n const initialLocations = locResult.ok ? locResult.value : [];\n\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 <Suspense fallback={null}>\n <OrganizationJsonLd />\n </Suspense>\n <Providers initialBusiness={initialBusiness} initialLocations={initialLocations}>\n <Header />\n <main className="flex-1 pb-12 w-full">\n <Suspense fallback={null}>{children}</Suspense>\n </main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n <SupportChat />\n </Providers>\n </body>\n </html>\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/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't supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We'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/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-6 sm: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/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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(await getSiteUrl());\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/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\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 {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
|
|
3335
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "e2e/visual.spec.ts", "kind": "text", "content": 'import { test, expect } from "@playwright/test";\n\n/**\n * Visual regression on the storefront\'s key surfaces. Catches CSS / layout\n * drift that the contract harness can\'t see.\n *\n * Anti-flake notes:\n * - We disable animations (CSS transitions can leave half-frame snapshots).\n * - We mask images (Unsplash / CDN URLs occasionally render slight pixel\n * differences). The container layout is what we\'re regressing on, not\n * image bytes.\n * - Fonts are served by `next/font` and locked at build time, but if you\n * see flake on a fresh CI runner, `await page.evaluate(() => document.fonts.ready)`\n * before the snapshot.\n */\ntest.describe("visual regression", () => {\n test.beforeEach(async ({ page }) => {\n await page.addStyleTag({\n content: `*, *::before, *::after { animation: none !important; transition: none !important; }`,\n });\n });\n\n test("homepage", async ({ page }) => {\n await page.goto("/");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("homepage.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("shop / catalogue", async ({ page }) => {\n await page.goto("/shop");\n await page.evaluate(() => document.fonts.ready);\n await expect(page).toHaveScreenshot("shop.png", {\n fullPage: true,\n mask: [page.locator("img")],\n });\n });\n\n test("cart drawer after adding a product", async ({ page }) => {\n await page.goto("/products/studio-tee-natural");\n await page.evaluate(() => document.fonts.ready);\n // Click whatever the storefront\'s primary Add-to-Cart button is.\n await page.getByRole("button", { name: /add to cart/i }).first().click();\n // Drawer auto-opens via CartDrawerProvider\'s openOnAdd watcher.\n await expect(page.getByRole("dialog", { name: /cart/i })).toBeVisible();\n await expect(page).toHaveScreenshot("cart-drawer.png", {\n mask: [page.locator("img")],\n });\n });\n});\n' }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { expect } from "vitest";\nimport { brand } from "../lib/brand";\n\n/**\n * Cart flow suite \u2014 base + per-merchant assertions.\n *\n * The base suite (from `@cimplify/sdk/testing/suite`) exercises the\n * universal contract: empty cart, add, dedupe, remove, businessId\n * round-trip. The `extend` hook below adds Studio FRX-specific\n * invariants \u2014 copy this pattern when a merchant has unique business\n * rules that need their own coverage.\n *\n * \u2022 `getHandle()` returns the suite\'s current `TestClientHandle`\n * (per `it`) so the seed and session are already wired.\n * \u2022 `it` is the vitest API; cases land inside the suite\'s `describe`\n * so its `beforeEach`/`afterEach` hooks apply automatically.\n */\ncreateCartFlowSuite({\n seed: brand.mock.seed,\n businessId: brand.mock.businessId,\n extend: ({ getHandle, it }) => {\n it("Studio FRX prices everything in GHS", async () => {\n // Currency is brand-level (not per-product) \u2014 Product carries the price\n // string only. Catalog completeness here is just a sanity check.\n expect(brand.currency).toBe("GHS");\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: unknown[] }).items ?? [];\n expect(items.length).toBeGreaterThan(0);\n });\n\n it("every apparel product covers the studio\'s core size run (S/M/L)", async () => {\n const h = getHandle();\n const list = await h.client.catalogue.getProducts();\n if (!list.ok) throw list.error;\n const items = (list.value as unknown as { items?: { id: string; name: string }[] }).items ?? [];\n\n // Apparel only \u2014 caps/socks/totes intentionally lack size variants.\n const apparel = items.filter((p) => /tee|hoodie|jacket|trouser|pant/i.test(p.name));\n expect(apparel.length).toBeGreaterThan(0);\n\n const required = new Set(["S", "M", "L"]);\n for (const product of apparel) {\n const variants = await h.client.catalogue.getVariants(product.id);\n if (!variants.ok) throw variants.error;\n const sizes = new Set(variants.value.map((v) => v.name).filter(Boolean));\n for (const size of required) {\n expect(sizes, `${product.name} missing size ${size}`).toContain(size);\n }\n }\n });\n },\n});\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed fashion --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed fashion",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check:visual": "playwright test",\n "check:visual:update": "playwright test --update-snapshots",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@playwright/test": "^1.50.0",\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "fashion",\n "name": "Fashion",\n "tagline": "Editorial multi-drop fashion storefront with lookbooks and size guides.",\n "industry": "fashion",\n "tags": ["fashion", "apparel", "lookbook"],\n "stability": "stable",\n "schemaType": "Store",\n "mock": {\n "seedName": "fashion",\n "seedBusinessId": "bus_studio_frx"\n }\n}\n' }, { "path": "app/size-guide/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Size Guide \u2014 ${brand.name}`,\n description: "Chest, length, and shoulder measurements for every Studio FRX piece.",\n};\n\ninterface SizeRow {\n size: string;\n chest: string;\n length: string;\n shoulder: string;\n}\n\nconst TOPS_CHART: SizeRow[] = [\n { size: "XS", chest: "104 cm / 41 in", length: "68 cm / 26.8 in", shoulder: "50 cm / 19.7 in" },\n { size: "S", chest: "110 cm / 43.3 in", length: "70 cm / 27.5 in", shoulder: "52 cm / 20.5 in" },\n { size: "M", chest: "116 cm / 45.7 in", length: "72 cm / 28.3 in", shoulder: "54 cm / 21.3 in" },\n { size: "L", chest: "122 cm / 48 in", length: "74 cm / 29.1 in", shoulder: "56 cm / 22 in" },\n { size: "XL", chest: "128 cm / 50.4 in", length: "76 cm / 29.9 in", shoulder: "58 cm / 22.8 in" },\n { size: "2XL", chest: "134 cm / 52.7 in", length: "78 cm / 30.7 in", shoulder: "60 cm / 23.6 in" },\n { size: "3XL", chest: "140 cm / 55.1 in", length: "80 cm / 31.5 in", shoulder: "62 cm / 24.4 in" },\n];\n\nconst BOTTOMS_CHART: { size: string; waist: string; hip: string; inseam: string }[] = [\n { size: "XS", waist: "72 cm / 28 in", hip: "92 cm / 36 in", inseam: "78 cm / 30.7 in" },\n { size: "S", waist: "76 cm / 30 in", hip: "96 cm / 38 in", inseam: "78 cm / 30.7 in" },\n { size: "M", waist: "82 cm / 32 in", hip: "102 cm / 40 in", inseam: "80 cm / 31.5 in" },\n { size: "L", waist: "88 cm / 35 in", hip: "108 cm / 42.5 in", inseam: "80 cm / 31.5 in" },\n { size: "XL", waist: "94 cm / 37 in", hip: "114 cm / 45 in", inseam: "82 cm / 32.3 in" },\n { size: "2XL", waist: "102 cm / 40 in", hip: "122 cm / 48 in", inseam: "82 cm / 32.3 in" },\n];\n\nexport default function SizeGuidePage() {\n return (\n <article className="max-w-4xl mx-auto px-6 sm:px-8 py-16">\n <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-primary mb-2">\n Size guide\n </p>\n <h1 className="font-display text-[clamp(2.5rem,7vw,5rem)] uppercase mb-4 -tracking-[0.04em] leading-[0.95]">\n Find your fit.\n </h1>\n <p className="text-base text-muted-foreground leading-relaxed max-w-2xl mb-12">\n Our pieces are cut for an oversized, dropped-shoulder fit. Order true to size for\n the intended fit; size down for a regular cut. All measurements are flat-laid\n garment measurements.\n </p>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Tops & outerwear</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Hoodies, tees, sweats, jackets. Chest measured pit-to-pit, doubled. Length\n measured from highest shoulder seam to hem.\n </p>\n <SizeTable\n headings={["Size", "Chest", "Length", "Shoulder"]}\n rows={TOPS_CHART.map((r) => [r.size, r.chest, r.length, r.shoulder])}\n />\n </section>\n\n <section className="mb-14">\n <h2 className="text-2xl font-bold mb-2 -tracking-[0.025em]">Bottoms</h2>\n <p className="text-sm text-muted-foreground mb-4">\n Trousers, shorts, sweats. Waist measured flat across the top of the\n waistband, doubled.\n </p>\n <SizeTable\n headings={["Size", "Waist", "Hip", "Inseam"]}\n rows={BOTTOMS_CHART.map((r) => [r.size, r.waist, r.hip, r.inseam])}\n />\n </section>\n\n <section className="rounded-2xl bg-foreground text-background p-8">\n <h2 className="text-xl font-bold mb-3 -tracking-[0.025em]">How to measure yourself</h2>\n <ul className="space-y-3 text-background/85 leading-relaxed">\n <li>\n <strong className="text-background">Chest</strong> \u2014 Wrap a soft tape around\n the fullest part of your chest, under the arms, keeping the tape level.\n </li>\n <li>\n <strong className="text-background">Waist</strong> \u2014 Around the narrowest part\n of your waist, just above the navel.\n </li>\n <li>\n <strong className="text-background">Hip</strong> \u2014 Around the fullest part of\n your hips, feet together.\n </li>\n <li>\n <strong className="text-background">Inseam</strong> \u2014 From the very top of the\n inside of your leg down to the floor (no shoes).\n </li>\n </ul>\n </section>\n\n <p className="mt-12 text-sm text-muted-foreground">\n Still unsure?{" "}\n <a\n href={`mailto:${brand.faq.contactEmail}`}\n className="text-primary font-semibold hover:underline"\n >\n Email us your usual size in another brand\n </a>{" "}\n and we\'ll recommend a fit.\n </p>\n </article>\n );\n}\n\nfunction SizeTable({\n headings,\n rows,\n}: {\n headings: string[];\n rows: string[][];\n}) {\n return (\n <div className="overflow-x-auto rounded-2xl border border-border">\n <table className="w-full text-sm">\n <thead className="bg-muted">\n <tr>\n {headings.map((h) => (\n <th\n key={h}\n className="text-left font-semibold uppercase tracking-wider text-[11px] text-muted-foreground px-4 py-3"\n >\n {h}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {rows.map((r, i) => (\n <tr\n key={r[0]}\n className={i % 2 === 0 ? "bg-background" : "bg-muted/30"}\n >\n {r.map((cell, j) => (\n <td\n key={j}\n className={[\n "px-4 py-3 tabular-nums",\n j === 0 ? "font-semibold text-foreground" : "text-foreground/80",\n ].join(" ")}\n >\n {cell}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n}\n' }, { "path": "app/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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/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/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-6 sm: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/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\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-6 sm: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/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\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\nexport const revalidate = 3600;\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 const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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: "/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.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/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 { getServerClient, tags } from "@cimplify/sdk/server";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { SupportChat } from "@/components/support-chat";\nimport { OrganizationJsonLd } from "@/components/json-ld";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst anton = Anton({\n subsets: ["latin"],\n variable: "--font-display",\n weight: "400",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const server = getServerClient();\n const [bizResult, locResult] = await Promise.all([\n server.business.getInfo({ cacheOptions: { revalidate: 3600, tags: [tags.business()] } }),\n server.business.getLocations({ cacheOptions: { revalidate: 3600, tags: [tags.locations()] } }),\n ]);\n const initialBusiness = bizResult.ok ? bizResult.value : null;\n const initialLocations = locResult.ok ? locResult.value : [];\n\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 <Suspense fallback={null}>\n <OrganizationJsonLd />\n </Suspense>\n <Providers initialBusiness={initialBusiness} initialLocations={initialLocations}>\n <Header />\n <main className="flex-1 pb-12 w-full">\n <Suspense fallback={null}>{children}</Suspense>\n </main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n <SupportChat />\n </Providers>\n </body>\n </html>\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/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't supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We'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/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-6 sm: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/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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(await getSiteUrl());\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/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\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 {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
|
|
3336
3336
|
|
|
3337
3337
|
import { useState } from "react";
|
|
3338
3338
|
|
|
@@ -4317,7 +4317,7 @@ export const brand: Brand = {
|
|
|
4317
4317
|
businessId: "bus_driveline_auto",
|
|
4318
4318
|
},
|
|
4319
4319
|
};
|
|
4320
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed auto --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed auto",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "auto",\n "name": "Auto Parts",\n "tagline": "Auto-parts storefront with vehicle fitment finder and installation booking.",\n "industry": "automotive",\n "tags": ["automotive", "parts", "fitment"],\n "stability": "stable",\n "schemaType": "AutoPartsStore",\n "mock": {\n "seedName": "auto",\n "seedBusinessId": "bus_driveline_auto"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
4320
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed auto --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed auto",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "auto",\n "name": "Auto Parts",\n "tagline": "Auto-parts storefront with vehicle fitment finder and installation booking.",\n "industry": "automotive",\n "tags": ["automotive", "parts", "fitment"],\n "stability": "stable",\n "schemaType": "AutoPartsStore",\n "mock": {\n "seedName": "auto",\n "seedBusinessId": "bus_driveline_auto"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">\n <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">\n 404\n </p>\n <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">\n Page not found.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n The URL you followed might be old, a typo, or pointing at a product\n that'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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'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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
4321
4321
|
|
|
4322
4322
|
/* Scan the SDK's bundled JS so utility classes used inside its components
|
|
4323
4323
|
land in the compiled stylesheet alongside our own. */
|
|
@@ -4982,7 +4982,7 @@ export const brand: Brand = {
|
|
|
4982
4982
|
businessId: "bus_mamas_kitchen",
|
|
4983
4983
|
},
|
|
4984
4984
|
};
|
|
4985
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed restaurant --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed restaurant",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "restaurant",\n "name": "Restaurant",\n "tagline": "Menu-driven storefront with reservations and table-service support.",\n "industry": "food",\n "tags": ["food", "restaurant", "reservations"],\n "stability": "stable",\n "schemaType": "Restaurant",\n "mock": {\n "seedName": "restaurant",\n "seedBusinessId": "bus_mamas_kitchen"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
4985
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed restaurant --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed restaurant",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "restaurant",\n "name": "Restaurant",\n "tagline": "Menu-driven storefront with reservations and table-service support.",\n "industry": "food",\n "tags": ["food", "restaurant", "reservations"],\n "stability": "stable",\n "schemaType": "Restaurant",\n "mock": {\n "seedName": "restaurant",\n "seedBusinessId": "bus_mamas_kitchen"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
4986
4986
|
|
|
4987
4987
|
@source "../node_modules/@cimplify/sdk/dist";
|
|
4988
4988
|
|
|
@@ -5601,7 +5601,7 @@ export const brand: Brand = {
|
|
|
5601
5601
|
businessId: "bus_freshmart",
|
|
5602
5602
|
},
|
|
5603
5603
|
};
|
|
5604
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed grocery --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed grocery",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "grocery",\n "name": "Grocery",\n "tagline": "High-SKU grocery storefront with quick-add cart UX and delivery windows.",\n "industry": "retail",\n "tags": ["food", "grocery", "delivery"],\n "stability": "stable",\n "schemaType": "GroceryStore",\n "mock": {\n "seedName": "grocery",\n "seedBusinessId": "bus_freshmart"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
5604
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed grocery --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed grocery",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "grocery",\n "name": "Grocery",\n "tagline": "High-SKU grocery storefront with quick-add cart UX and delivery windows.",\n "industry": "retail",\n "tags": ["food", "grocery", "delivery"],\n "stability": "stable",\n "schemaType": "GroceryStore",\n "mock": {\n "seedName": "grocery",\n "seedBusinessId": "bus_freshmart"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/globals.css", "kind": "text", "content": `@import "tailwindcss";
|
|
5605
5605
|
|
|
5606
5606
|
@source "../node_modules/@cimplify/sdk/dist";
|
|
5607
5607
|
|
|
@@ -6242,7 +6242,7 @@ export const brand: Brand = {
|
|
|
6242
6242
|
businessId: "bus_serene_spa",
|
|
6243
6243
|
},
|
|
6244
6244
|
};
|
|
6245
|
-
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed services --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed services",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.101.1",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "services",\n "name": "Services",\n "tagline": "Bookable-services storefront with calendar slots and staff profiles.",\n "industry": "services",\n "tags": ["services", "bookings", "appointments"],\n "stability": "stable",\n "schemaType": "BeautySalon",\n "mock": {\n "seedName": "services",\n "seedBusinessId": "bus_serene_spa"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/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/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-6 sm: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/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\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-6 sm: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/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\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\nexport const revalidate = 3600;\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 const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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: "/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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 { getServerClient, tags } from "@cimplify/sdk/server";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { SupportChat } from "@/components/support-chat";\nimport { OrganizationJsonLd } from "@/components/json-ld";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst fraunces = Fraunces({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const server = getServerClient();\n const [bizResult, locResult] = await Promise.all([\n server.business.getInfo({ cacheOptions: { revalidate: 3600, tags: [tags.business()] } }),\n server.business.getLocations({ cacheOptions: { revalidate: 3600, tags: [tags.locations()] } }),\n ]);\n const initialBusiness = bizResult.ok ? bizResult.value : null;\n const initialLocations = locResult.ok ? locResult.value : [];\n\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 <Suspense fallback={null}>\n <OrganizationJsonLd />\n </Suspense>\n <Providers initialBusiness={initialBusiness} initialLocations={initialLocations}>\n <Header />\n <main className="flex-1 pb-12 w-full">\n <Suspense fallback={null}>{children}</Suspense>\n </main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n <SupportChat />\n </Providers>\n </body>\n </html>\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-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't supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We'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/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-6 sm: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/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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(await getSiteUrl());\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/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\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 {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
|
|
6245
|
+
` }, { "path": "lib/cimplify-server.ts", "kind": "text", "content": 'import { cookies } from "next/headers";\nimport {\n getAccessTokenFromCookieHeader,\n getSessionFromCookieHeader,\n} from "@cimplify/sdk/server";\nimport type { NavItem } from "@cimplify/sdk/react";\n\nexport const ACCOUNT_NAV: NavItem[] = [\n { label: "Dashboard", href: "/account" },\n { label: "Orders", href: "/account/orders" },\n { label: "Cimplify identity", href: "https://link.cimplify.io", external: true },\n];\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = (process.env.CIMPLIFY_ISSUER ?? "https://api.cimplify.io").replace(/\\/$/, "");\n\nconst OIDC_CONFIG = { clientId: CLIENT_ID, issuer: ISSUER };\n\nasync function cookieHeader(): Promise<string> {\n const store = await cookies();\n return store.getAll().map((c) => `${c.name}=${c.value}`).join("; ");\n}\n\nexport async function serverSession() {\n return getSessionFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\nexport async function serverAccessToken(): Promise<string | null> {\n return getAccessTokenFromCookieHeader(OIDC_CONFIG, await cookieHeader());\n}\n\ninterface Envelope<T> {\n data: T;\n}\n\nexport async function apiFetch<T>(\n path: string,\n init: RequestInit & { bearer: string | null },\n): Promise<T | null> {\n if (!init.bearer) return null;\n const { bearer, ...rest } = init;\n const headers = new Headers(rest.headers);\n headers.set("Authorization", `Bearer ${bearer}`);\n headers.set("Accept", "application/json");\n if (rest.body && !headers.has("Content-Type")) {\n headers.set("Content-Type", "application/json");\n }\n const res = await fetch(`${ISSUER}${path}`, { ...rest, headers, cache: "no-store" });\n if (!res.ok) return null;\n const envelope = (await res.json()) as Envelope<T>;\n return envelope.data;\n}\n' }, { "path": "lib/cimplify-loader.ts", "kind": "text", "content": 'import type { ImageLoader } from "next/image";\n\nimport { assetUrl, isCimplifyAsset } from "@cimplify/sdk";\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, {\n base: cdnBase,\n w: width,\n quality,\n format: "auto",\n });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n' }, { "path": "lib/site-url.ts", "kind": "text", "content": 'import { SITE_URL } from "./site.config";\n\n/** Canonical absolute URL for this storefront. */\nexport async function getSiteUrl(): Promise<string> {\n return SITE_URL;\n}\n' }, { "path": ".env.example", "kind": "text", "content": "# Your tenant public key. Get it from the desk's Developers tab.\n# `cpk_live_*` / `cpk_test_*` keys auto-route same-origin requests\n# to hosted Cimplify in both dev and prod. Anything else (`mock-dev`,\n# empty) falls back to the local `cimplify dev` mock.\nNEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev\n\n# Optional. Forces a fixed canonical URL for sitemap.xml, robots.txt,\n# OpenGraph, and llms.txt. Leave unset and the storefront derives the\n# canonical from the request `Host` header automatically.\n# NEXT_PUBLIC_SITE_URL=\n" }, { "path": "__tests__/contract.test.ts", "kind": "text", "content": 'import { createContractSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateContractSuite({ seed: brand.mock.seed });\n' }, { "path": "__tests__/brand.test.ts", "kind": "text", "content": 'import { createBrandSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateBrandSuite({ brand });\n' }, { "path": "__tests__/cart-flow.test.ts", "kind": "text", "content": 'import { createCartFlowSuite } from "@cimplify/sdk/testing/suite";\nimport { brand } from "../lib/brand";\n\ncreateCartFlowSuite({ seed: brand.mock.seed, businessId: brand.mock.businessId });\n' }, { "path": "package.json", "kind": "text", "content": '{\n "name": "__STOREFRONT_NAME__",\n "private": true,\n "version": "0.0.0",\n "scripts": {\n "dev": "concurrently -k -n mock,next -c blue,magenta \\"cimplify-mock --seed services --quiet\\" \\"next dev\\"",\n "dev:storefront": "next dev",\n "dev:mock": "cimplify-mock --seed services",\n "build": "next build",\n "start": "next start",\n "typecheck": "tsc --noEmit",\n "test": "vitest",\n "test:run": "vitest run",\n "check:brand": "vitest run __tests__/brand.test.ts",\n "check:cart": "vitest run __tests__/cart-flow.test.ts",\n "check:contract": "vitest run __tests__/contract.test.ts",\n "check": "bun run typecheck && bun run test:run"\n },\n "dependencies": {\n "@cimplify/sdk": "^0.102.0",\n "next": "^16.2.6",\n "react": "^19.0.0",\n "react-dom": "^19.0.0"\n },\n "devDependencies": {\n "@tailwindcss/postcss": "^4.2.4",\n "@types/node": "^22.10.0",\n "@types/react": "^19.0.0",\n "@types/react-dom": "^19.0.0",\n "concurrently": "^9.0.0",\n "tailwindcss": "^4.2.4",\n "typescript": "^5.9.3",\n "vitest": "^4.1.5"\n }\n}\n' }, { "path": "metadata.json", "kind": "text", "content": '{\n "id": "services",\n "name": "Services",\n "tagline": "Bookable-services storefront with calendar slots and staff profiles.",\n "industry": "services",\n "tags": ["services", "bookings", "appointments"],\n "stability": "stable",\n "schemaType": "BeautySalon",\n "mock": {\n "seedName": "services",\n "seedBusinessId": "bus_serene_spa"\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/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/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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCategories();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CategoryData {\n category: Category;\n products: Product[];\n}\n\ntype CategoryResult =\n | { ok: true; data: CategoryData }\n | { ok: false; code: string };\n\nasync function getCategory(slug: string): Promise<CategoryResult> {\n const client = getServerClient();\n const catRes = await client.catalogue.getCategoryBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n });\n if (!catRes.ok) return { ok: false, code: catRes.error.code };\n\n const r = await client.catalogue.getCategoryProducts(catRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.category(catRes.value.id),\n tags.categoryProducts(catRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCategory(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCategory(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CategorySkeleton />;\n }\n const { category, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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-6 sm:px-8 py-16 prose prose-lg max-w-none">\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2 not-prose">\n {t.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-2 -tracking-[0.02em]">\n {t.title}\n </h1>\n <p className="text-sm text-muted-foreground not-prose mb-10">\n Last updated: {t.lastUpdated}\n </p>\n\n <section className="space-y-5 leading-relaxed text-foreground/90">\n {t.sections.map((s) => (\n <div key={s.heading}>\n <h2 className="font-serif text-2xl font-semibold mt-0">{s.heading}</h2>\n {typeof s.body === "string" ? (\n <p>{s.body}</p>\n ) : (\n <>\n <p>{s.body.intro}</p>\n <ul className="list-disc pl-6 space-y-2">\n {s.body.bullets.map((b) => (\n <li key={b}>{b}</li>\n ))}\n </ul>\n </>\n )}\n </div>\n ))}\n </section>\n </article>\n );\n}\n' }, { "path": "app/not-found.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Page not found \u2014 ${brand.name}`,\n description: "We couldn\'t find that page.",\n};\n\nexport default function NotFound() {\n return (\n <section className="max-w-2xl mx-auto px-6 sm: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'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/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/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/account/orders/[id]/page.tsx", "kind": "text", "content": 'import { notFound } from "next/navigation";\nimport { AccountSignedOutPrompt, toAccountOrderView } from "@cimplify/sdk/react";\nimport type { EnrichedOrder } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\nimport { OrderDetail } from "@/components/order-detail";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function OrderDetailPage({\n params,\n}: {\n params: Promise<{ id: string }>;\n}) {\n const { id } = await params;\n\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const order = await apiFetch<EnrichedOrder>(`/api/v1/orders/${encodeURIComponent(id)}`, {\n bearer,\n });\n if (!order) notFound();\n\n return (\n <div className="mx-auto w-full max-w-5xl px-4 py-8 sm:px-6">\n <OrderDetail {...toAccountOrderView(order)} />\n </div>\n );\n}\n' }, { "path": "app/account/orders/page.tsx", "kind": "text", "content": 'import {\n AccountOrdersPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { OrderRowData, OrderRowStatus } from "@cimplify/sdk/react";\nimport type { Order, OrderStatus } from "@cimplify/sdk";\nimport { formatMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nfunction mapStatus(status: OrderStatus): OrderRowStatus {\n switch (status) {\n case "in_preparation":\n case "ready_to_serve":\n case "partially_served":\n return { kind: "preparing", label: "Preparing" };\n case "delivered":\n return { kind: "delivered" };\n case "picked_up":\n return { kind: "delivered", label: "Picked up" };\n case "served":\n case "completed":\n return { kind: "completed" };\n case "cancelled":\n case "refunded":\n return { kind: "cancelled", label: status === "refunded" ? "Refunded" : undefined };\n case "pending":\n case "created":\n case "confirmed":\n default:\n return { kind: "active", label: "Confirmed" };\n }\n}\n\nfunction relativeMeta(iso: string, orderNumber: string): string {\n const when = new Date(iso);\n const formatted = when.toLocaleDateString(undefined, {\n month: "short",\n day: "numeric",\n year: when.getFullYear() === new Date().getFullYear() ? undefined : "numeric",\n });\n return `Order ${orderNumber} \xB7 ${formatted}`;\n}\n\nfunction toRowData(order: Order): OrderRowData {\n const count = order.items?.length ?? 0;\n return {\n id: order.id,\n href: `/account/orders/${order.id}`,\n summary: count > 0 ? `${count} ${count === 1 ? "item" : "items"}` : "Order",\n meta: relativeMeta(order.created_at, order.user_friendly_id ?? order.id),\n status: mapStatus(order.status),\n total: formatMoney(order.total_price, order.currency),\n };\n}\n\nexport default async function OrdersPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const orders = await apiFetch<Order[]>(\n `/v1/link/orders?business_id=${encodeURIComponent(brand.mock.businessId)}`,\n { bearer },\n );\n\n return (\n <AccountOrdersPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n orders={(orders ?? []).map(toRowData)}\n />\n );\n}\n' }, { "path": "app/account/page.tsx", "kind": "text", "content": 'import {\n AccountDashboardPage,\n AccountSignedOutPrompt,\n} from "@cimplify/sdk/react";\nimport type { CustomerAddress, CustomerMobileMoney } from "@cimplify/sdk";\nimport { brand } from "@/lib/brand";\nimport { ACCOUNT_NAV, apiFetch, serverAccessToken, serverSession } from "@/lib/cimplify-server";\n\nconst CLIENT_ID = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\nexport default async function AccountPage() {\n const session = await serverSession();\n if (!session) {\n return (\n <AccountSignedOutPrompt\n clientId={CLIENT_ID}\n redirectUri={REDIRECT_URI}\n eyebrow={brand.account.loginEyebrow}\n title={brand.account.loginTitle}\n description={brand.account.loginSubtitle}\n />\n );\n }\n\n const bearer = await serverAccessToken();\n const [addresses, wallets] = await Promise.all([\n apiFetch<CustomerAddress[]>("/v1/link/addresses", { bearer }),\n apiFetch<CustomerMobileMoney[]>("/v1/link/mobile-money", { bearer }),\n ]);\n\n return (\n <AccountDashboardPage\n customerName={session.name ?? ""}\n customerEmail={session.email}\n customerId={session.sub}\n merchantName={brand.name}\n nav={ACCOUNT_NAV}\n identitySummary={{\n addresses: addresses?.length ?? 0,\n paymentMethods: wallets?.length ?? 0,\n }}\n />\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 resolve the business\'s\n * UCP handle from the public key (via the storefront API), then forward\n * to Cimplify for the canonical manifest. Edge-cached for an hour\n * because capabilities change rarely.\n */\nconst UCP_API_BASE = "https://api.cimplify.io";\nconst STOREFRONT_API_BASE =\n process.env.NODE_ENV === "production"\n ? "https://storefronts.cimplify.io"\n : "http://127.0.0.1:8787";\n\nexport async function GET() {\n const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY;\n if (!publicKey) {\n return NextResponse.json(\n {\n error: "NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY not set",\n remediation:\n "Set NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY in .env.local (and your deployment env).",\n },\n { status: 500 },\n );\n }\n\n try {\n // Step 1 \u2014 resolve the business handle from the public key.\n const businessResp = await fetch(`${STOREFRONT_API_BASE}/api/v1/business`, {\n headers: { "X-API-Key": publicKey, "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n });\n\n if (!businessResp.ok) {\n return NextResponse.json(\n { error: `Failed to resolve business: ${businessResp.status}` },\n { status: businessResp.status },\n );\n }\n\n const businessJson = await businessResp.json();\n const handle: string | undefined = businessJson?.data?.handle;\n if (!handle) {\n return NextResponse.json(\n { error: "Business has no handle configured" },\n { status: 500 },\n );\n }\n\n // Step 2 \u2014 fetch the canonical UCP manifest.\n const manifestResp = await fetch(\n `${UCP_API_BASE}/ucp/v1/${handle}/manifest`,\n {\n headers: { "Content-Type": "application/json" },\n next: { revalidate: 3600 },\n },\n );\n\n if (!manifestResp.ok) {\n return NextResponse.json(\n { error: `Upstream UCP manifest fetch failed: ${manifestResp.status}` },\n { status: manifestResp.status },\n );\n }\n\n const manifest = await manifestResp.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/returns/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { brand } from "@/lib/brand";\nimport { PolicyPage } from "@/components/policy-page";\n\nexport const metadata: Metadata = {\n title: `${brand.returns.title} \u2014 ${brand.name}`,\n};\n\nexport default function ReturnsPage() {\n return <PolicyPage policy={brand.returns} />;\n}\n' }, { "path": "app/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-6 sm: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/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/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-6 sm: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/auth/signout/route.ts", "kind": "text", "content": 'import { buildSignoutCookies } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\n\nexport async function POST(): Promise<Response> {\n const headers = new Headers({ "Content-Type": "application/json" });\n for (const cookie of buildSignoutCookies({ clientId: CLIENT_ID })) {\n headers.append("Set-Cookie", cookie);\n }\n return new Response(JSON.stringify({ ok: true }), { status: 200, headers });\n}\n' }, { "path": "app/auth/session/route.ts", "kind": "text", "content": 'import { handleSessionRequest } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\n\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID) {\n return Response.json({ sub: null }, { status: 200 });\n }\n return handleSessionRequest(req, { clientId: CLIENT_ID, issuer: ISSUER });\n}\n' }, { "path": "app/auth/callback/route.ts", "kind": "text", "content": 'import { handleOidcCallback, handleRedirectCallback } from "@cimplify/sdk/server";\n\nconst CLIENT_ID = process.env.CIMPLIFY_CLIENT_ID ?? "";\nconst ISSUER = process.env.CIMPLIFY_ISSUER;\nconst REDIRECT_URI = process.env.CIMPLIFY_REDIRECT_URI ?? "";\n\n// Redirect sign-in: the browser lands here via a top-level GET carrying ?code.\nexport async function GET(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleRedirectCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\n defaultReturnTo: "/account",\n });\n}\n\n// Modal/silent sign-in: posts {code, codeVerifier, state} here for exchange.\nexport async function POST(req: Request): Promise<Response> {\n if (!CLIENT_ID || !REDIRECT_URI) {\n return Response.json({ error: "oidc_not_configured" }, { status: 500 });\n }\n return handleOidcCallback(req, {\n clientId: CLIENT_ID,\n issuer: ISSUER,\n redirectUri: REDIRECT_URI,\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-6 sm: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/sitemap-page/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport Link from "next/link";\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\nexport const revalidate = 3600;\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 const client = getServerClient();\n const [pRes, cRes, colRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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: "/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/robots.ts", "kind": "text", "content": 'import type { MetadataRoute } from "next";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport default async function robots(): Promise<MetadataRoute.Robots> {\n const siteUrl = await getSiteUrl();\n return {\n rules: [\n {\n userAgent: "*",\n allow: "/",\n disallow: ["/cart", "/checkout", "/orders/", "/api/"],\n },\n ],\n sitemap: `${siteUrl}/sitemap.xml`,\n host: siteUrl,\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 {\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\n// See app/products/[slug]/page.tsx for the rationale on generateStaticParams.\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getCollections();\n if (!r.ok || r.value.length === 0) {\n return [{ slug: "__placeholder__" }];\n }\n return r.value.map((c) => ({ slug: c.slug ?? c.id }));\n}\n\nexport const revalidate = 3600;\n\ninterface CollectionData {\n collection: Collection;\n products: Product[];\n}\n\ntype CollectionResult =\n | { ok: true; data: CollectionData }\n | { ok: false; code: string };\n\nasync function getCollection(slug: string): Promise<CollectionResult> {\n const client = getServerClient();\n const colRes = await client.catalogue.getCollectionBySlug(slug, {\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n });\n if (!colRes.ok) return { ok: false, code: colRes.error.code };\n\n const r = await client.catalogue.getCollectionProducts(colRes.value.id, undefined, {\n cacheOptions: {\n revalidate: 3600,\n tags: [\n tags.products(),\n tags.collection(colRes.value.id),\n tags.collectionProducts(colRes.value.id),\n ],\n },\n });\n const products = r.ok\n ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))\n : [];\n return { ok: true, data: { 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 result = await getCollection(slug);\n if (!result.ok) return {};\n const data = result.data;\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 result = await getCollection(slug);\n if (!result.ok) {\n if (result.code === "NOT_FOUND") notFound();\n return <CollectionSkeleton />;\n }\n const { collection, products } = result.data;\n return (\n <section className="max-w-7xl mx-auto px-6 sm: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-6 sm: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/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 { getServerClient, tags } from "@cimplify/sdk/server";\nimport { Header } from "@/components/header";\nimport { Footer } from "@/components/footer";\nimport { ProductModal } from "@/components/product-modal";\nimport { CartDrawer } from "@/components/cart-drawer";\nimport { SupportChat } from "@/components/support-chat";\nimport { OrganizationJsonLd } from "@/components/json-ld";\nimport { Suspense } from "react";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nconst inter = Inter({\n subsets: ["latin"],\n variable: "--font-sans",\n display: "swap",\n});\n\nconst fraunces = Fraunces({\n subsets: ["latin"],\n variable: "--font-serif",\n display: "swap",\n});\n\nexport async function generateMetadata(): Promise<Metadata> {\n const siteUrl = await getSiteUrl();\n return {\n metadataBase: new URL(siteUrl),\n title: {\n default: brand.name,\n template: `%s \u2014 ${brand.name}`,\n },\n description: brand.description,\n openGraph: {\n type: "website",\n siteName: brand.name,\n locale: brand.locale,\n },\n twitter: { card: "summary_large_image" },\n };\n}\n\nexport default async function RootLayout({ children }: { children: React.ReactNode }) {\n const server = getServerClient();\n const [bizResult, locResult] = await Promise.all([\n server.business.getInfo({ cacheOptions: { revalidate: 3600, tags: [tags.business()] } }),\n server.business.getLocations({ cacheOptions: { revalidate: 3600, tags: [tags.locations()] } }),\n ]);\n const initialBusiness = bizResult.ok ? bizResult.value : null;\n const initialLocations = locResult.ok ? locResult.value : [];\n\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 <Suspense fallback={null}>\n <OrganizationJsonLd />\n </Suspense>\n <Providers initialBusiness={initialBusiness} initialLocations={initialLocations}>\n <Header />\n <main className="flex-1 pb-12 w-full">\n <Suspense fallback={null}>{children}</Suspense>\n </main>\n <Footer />\n <Suspense fallback={null}>\n <ProductModal />\n </Suspense>\n <CartDrawer />\n <SupportChat />\n </Providers>\n </body>\n </html>\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-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't supposed to happen.\n </h1>\n <p className="text-muted-foreground leading-relaxed mb-8">\n We'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/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-6 sm: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/llms.txt/route.ts", "kind": "text", "content": 'import { getServerClient, tags, type Product } from "@cimplify/sdk/server";\nimport { brand } from "@/lib/brand";\nimport { getSiteUrl } from "@/lib/site-url";\n\nexport const revalidate = 3600;\n\nasync function buildLlmsTxt(SITE_URL: string): Promise<string> {\n const client = getServerClient();\n const [productsRes, categoriesRes, collectionsRes] = await Promise.all([\n client.catalogue.getProducts(\n { limit: 500 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n ),\n client.catalogue.getCategories({\n cacheOptions: { revalidate: 3600, tags: [tags.categories()] },\n }),\n client.catalogue.getCollections({\n cacheOptions: { revalidate: 3600, tags: [tags.collections()] },\n }),\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(await getSiteUrl());\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/contact/page.tsx", "kind": "text", "content": 'import type { Metadata } from "next";\nimport { ContactForm } from "./contact-form";\nimport { brand } from "@/lib/brand";\n\nexport const metadata: Metadata = {\n title: `Contact \u2014 ${brand.name}`,\n description: brand.contactPage.body,\n};\n\nexport default function ContactPage() {\n const c = brand.contactPage;\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 {c.eyebrow}\n </p>\n <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold mb-4 -tracking-[0.02em]">\n {c.title}\n </h1>\n <p className="text-lg text-muted-foreground max-w-2xl mb-12 leading-relaxed">{c.body}</p>\n\n <div className="grid grid-cols-1 lg:grid-cols-[1.5fr_1fr] gap-10 items-start">\n <ContactForm reasons={c.reasons} />\n <aside className="space-y-5 lg:pl-10 lg:border-l border-border">\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Direct lines\n </p>\n <ul className="space-y-3 list-none p-0 m-0">\n {c.directLines.map((line) => (\n <li key={line.label}>\n <p className="text-xs text-muted-foreground m-0">{line.label}</p>\n <a\n href={line.href}\n className="text-foreground hover:text-primary transition-colors"\n >\n {line.value}\n </a>\n </li>\n ))}\n </ul>\n </div>\n <div>\n <p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground mb-3">\n Visit\n </p>\n <p className="text-foreground m-0">{brand.contact.address}</p>\n <p className="text-xs text-muted-foreground mt-1">{brand.contact.hours}</p>\n </div>\n </aside>\n </div>\n </article>\n );\n}\n' }, { "path": "app/contact/contact-form.tsx", "kind": "text", "content": `"use client";
|
|
6246
6246
|
|
|
6247
6247
|
import { useState } from "react";
|
|
6248
6248
|
|