@cimplify/sdk 0.40.1 → 0.41.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.
Files changed (219) hide show
  1. package/dist/dispatcher.mjs +3 -2
  2. package/dist/styles.css +1 -1
  3. package/package.json +1 -1
  4. package/templates/storefront-bakery/.env.example +5 -0
  5. package/templates/storefront-bakery/AGENTS.md +57 -0
  6. package/templates/storefront-bakery/app/about/page.tsx +21 -44
  7. package/templates/storefront-bakery/app/categories/[slug]/page.tsx +2 -1
  8. package/templates/storefront-bakery/app/collections/[slug]/page.tsx +2 -1
  9. package/templates/storefront-bakery/app/error.tsx +60 -0
  10. package/templates/storefront-bakery/app/faq/page.tsx +13 -89
  11. package/templates/storefront-bakery/app/layout.tsx +37 -2
  12. package/templates/storefront-bakery/app/llms.txt/route.ts +94 -0
  13. package/templates/storefront-bakery/app/not-found.tsx +39 -0
  14. package/templates/storefront-bakery/app/page.tsx +6 -6
  15. package/templates/storefront-bakery/app/privacy/page.tsx +24 -68
  16. package/templates/storefront-bakery/app/robots.ts +18 -0
  17. package/templates/storefront-bakery/app/shop/page.tsx +3 -2
  18. package/templates/storefront-bakery/app/sitemap.ts +62 -0
  19. package/templates/storefront-bakery/app/terms/page.tsx +24 -82
  20. package/templates/storefront-bakery/components/footer.tsx +88 -130
  21. package/templates/storefront-bakery/components/header.tsx +8 -16
  22. package/templates/storefront-bakery/lib/brand.ts +453 -0
  23. package/templates/storefront-fashion/.env.example +16 -0
  24. package/templates/storefront-fashion/AGENTS.md +52 -0
  25. package/templates/storefront-fashion/README.md +77 -0
  26. package/templates/storefront-fashion/app/about/page.tsx +41 -0
  27. package/templates/storefront-fashion/app/cart/page.tsx +9 -0
  28. package/templates/storefront-fashion/app/categories/[slug]/listing-client.tsx +19 -0
  29. package/templates/storefront-fashion/app/categories/[slug]/page.tsx +130 -0
  30. package/templates/storefront-fashion/app/checkout/page.tsx +17 -0
  31. package/templates/storefront-fashion/app/collections/[slug]/listing-client.tsx +20 -0
  32. package/templates/storefront-fashion/app/collections/[slug]/page.tsx +130 -0
  33. package/templates/storefront-fashion/app/error.tsx +61 -0
  34. package/templates/storefront-fashion/app/faq/page.tsx +46 -0
  35. package/templates/storefront-fashion/app/globals.css +46 -0
  36. package/templates/storefront-fashion/app/layout.tsx +76 -0
  37. package/templates/storefront-fashion/app/llms.txt/route.ts +94 -0
  38. package/templates/storefront-fashion/app/not-found.tsx +39 -0
  39. package/templates/storefront-fashion/app/orders/[id]/page.tsx +24 -0
  40. package/templates/storefront-fashion/app/page.tsx +183 -0
  41. package/templates/storefront-fashion/app/privacy/page.tsx +44 -0
  42. package/templates/storefront-fashion/app/products/[slug]/page.tsx +165 -0
  43. package/templates/storefront-fashion/app/products/[slug]/product-detail.tsx +71 -0
  44. package/templates/storefront-fashion/app/robots.ts +18 -0
  45. package/templates/storefront-fashion/app/shop/page.tsx +63 -0
  46. package/templates/storefront-fashion/app/shop/shop-client.tsx +32 -0
  47. package/templates/storefront-fashion/app/sitemap.ts +59 -0
  48. package/templates/storefront-fashion/app/terms/page.tsx +44 -0
  49. package/templates/storefront-fashion/components/brand-marquee.tsx +27 -0
  50. package/templates/storefront-fashion/components/cart-pill.tsx +32 -0
  51. package/templates/storefront-fashion/components/category-grid.tsx +28 -0
  52. package/templates/storefront-fashion/components/category-tiles.tsx +104 -0
  53. package/templates/storefront-fashion/components/collection-strip.tsx +45 -0
  54. package/templates/storefront-fashion/components/feature-hero.tsx +82 -0
  55. package/templates/storefront-fashion/components/footer.tsx +153 -0
  56. package/templates/storefront-fashion/components/header.tsx +43 -0
  57. package/templates/storefront-fashion/components/hero.tsx +28 -0
  58. package/templates/storefront-fashion/components/nav-link.tsx +20 -0
  59. package/templates/storefront-fashion/components/newsletter.tsx +50 -0
  60. package/templates/storefront-fashion/components/promo-banner.tsx +41 -0
  61. package/templates/storefront-fashion/components/providers.tsx +31 -0
  62. package/templates/storefront-fashion/components/section-heading.tsx +37 -0
  63. package/templates/storefront-fashion/components/store-product-card.tsx +83 -0
  64. package/templates/storefront-fashion/components/trade-in-cta.tsx +54 -0
  65. package/templates/storefront-fashion/components/trust-bar.tsx +66 -0
  66. package/templates/storefront-fashion/lib/brand.ts +421 -0
  67. package/templates/storefront-fashion/lib/cart.ts +27 -0
  68. package/templates/storefront-fashion/next.config.ts +40 -0
  69. package/templates/storefront-fashion/package.json +28 -0
  70. package/templates/storefront-fashion/postcss.config.mjs +7 -0
  71. package/templates/storefront-fashion/tsconfig.json +23 -0
  72. package/templates/storefront-grocery/.env.example +16 -0
  73. package/templates/storefront-grocery/AGENTS.md +39 -0
  74. package/templates/storefront-grocery/README.md +73 -0
  75. package/templates/storefront-grocery/app/about/page.tsx +38 -0
  76. package/templates/storefront-grocery/app/cart/page.tsx +9 -0
  77. package/templates/storefront-grocery/app/categories/[slug]/listing-client.tsx +19 -0
  78. package/templates/storefront-grocery/app/categories/[slug]/page.tsx +118 -0
  79. package/templates/storefront-grocery/app/checkout/page.tsx +17 -0
  80. package/templates/storefront-grocery/app/collections/[slug]/listing-client.tsx +20 -0
  81. package/templates/storefront-grocery/app/collections/[slug]/page.tsx +118 -0
  82. package/templates/storefront-grocery/app/error.tsx +60 -0
  83. package/templates/storefront-grocery/app/faq/page.tsx +46 -0
  84. package/templates/storefront-grocery/app/globals.css +45 -0
  85. package/templates/storefront-grocery/app/layout.tsx +75 -0
  86. package/templates/storefront-grocery/app/llms.txt/route.ts +94 -0
  87. package/templates/storefront-grocery/app/not-found.tsx +39 -0
  88. package/templates/storefront-grocery/app/orders/[id]/page.tsx +21 -0
  89. package/templates/storefront-grocery/app/page.tsx +97 -0
  90. package/templates/storefront-grocery/app/privacy/page.tsx +44 -0
  91. package/templates/storefront-grocery/app/robots.ts +18 -0
  92. package/templates/storefront-grocery/app/shop/page.tsx +31 -0
  93. package/templates/storefront-grocery/app/shop/shop-client.tsx +27 -0
  94. package/templates/storefront-grocery/app/sitemap.ts +62 -0
  95. package/templates/storefront-grocery/app/terms/page.tsx +44 -0
  96. package/templates/storefront-grocery/components/cart-pill.tsx +32 -0
  97. package/templates/storefront-grocery/components/category-grid.tsx +28 -0
  98. package/templates/storefront-grocery/components/collection-strip.tsx +45 -0
  99. package/templates/storefront-grocery/components/footer.tsx +148 -0
  100. package/templates/storefront-grocery/components/header.tsx +43 -0
  101. package/templates/storefront-grocery/components/hero.tsx +25 -0
  102. package/templates/storefront-grocery/components/nav-link.tsx +20 -0
  103. package/templates/storefront-grocery/components/providers.tsx +31 -0
  104. package/templates/storefront-grocery/components/store-product-card.tsx +83 -0
  105. package/templates/storefront-grocery/lib/brand.ts +284 -0
  106. package/templates/storefront-grocery/lib/cart.ts +27 -0
  107. package/templates/storefront-grocery/next.config.ts +40 -0
  108. package/templates/storefront-grocery/package.json +28 -0
  109. package/templates/storefront-grocery/postcss.config.mjs +7 -0
  110. package/templates/storefront-grocery/tsconfig.json +23 -0
  111. package/templates/storefront-restaurant/.env.example +16 -0
  112. package/templates/storefront-restaurant/AGENTS.md +39 -0
  113. package/templates/storefront-restaurant/README.md +73 -0
  114. package/templates/storefront-restaurant/app/about/page.tsx +38 -0
  115. package/templates/storefront-restaurant/app/cart/page.tsx +9 -0
  116. package/templates/storefront-restaurant/app/categories/[slug]/listing-client.tsx +19 -0
  117. package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +118 -0
  118. package/templates/storefront-restaurant/app/checkout/page.tsx +17 -0
  119. package/templates/storefront-restaurant/app/collections/[slug]/listing-client.tsx +20 -0
  120. package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +118 -0
  121. package/templates/storefront-restaurant/app/error.tsx +60 -0
  122. package/templates/storefront-restaurant/app/faq/page.tsx +46 -0
  123. package/templates/storefront-restaurant/app/globals.css +44 -0
  124. package/templates/storefront-restaurant/app/layout.tsx +80 -0
  125. package/templates/storefront-restaurant/app/llms.txt/route.ts +94 -0
  126. package/templates/storefront-restaurant/app/not-found.tsx +39 -0
  127. package/templates/storefront-restaurant/app/orders/[id]/page.tsx +21 -0
  128. package/templates/storefront-restaurant/app/page.tsx +97 -0
  129. package/templates/storefront-restaurant/app/privacy/page.tsx +44 -0
  130. package/templates/storefront-restaurant/app/robots.ts +18 -0
  131. package/templates/storefront-restaurant/app/shop/page.tsx +31 -0
  132. package/templates/storefront-restaurant/app/shop/shop-client.tsx +27 -0
  133. package/templates/storefront-restaurant/app/sitemap.ts +62 -0
  134. package/templates/storefront-restaurant/app/terms/page.tsx +44 -0
  135. package/templates/storefront-restaurant/components/cart-pill.tsx +32 -0
  136. package/templates/storefront-restaurant/components/category-grid.tsx +28 -0
  137. package/templates/storefront-restaurant/components/collection-strip.tsx +45 -0
  138. package/templates/storefront-restaurant/components/footer.tsx +148 -0
  139. package/templates/storefront-restaurant/components/header.tsx +43 -0
  140. package/templates/storefront-restaurant/components/hero.tsx +25 -0
  141. package/templates/storefront-restaurant/components/nav-link.tsx +20 -0
  142. package/templates/storefront-restaurant/components/product-modal.tsx +105 -0
  143. package/templates/storefront-restaurant/components/providers.tsx +31 -0
  144. package/templates/storefront-restaurant/components/store-product-card.tsx +83 -0
  145. package/templates/storefront-restaurant/lib/brand.ts +287 -0
  146. package/templates/storefront-restaurant/lib/cart.ts +27 -0
  147. package/templates/storefront-restaurant/next.config.ts +40 -0
  148. package/templates/storefront-restaurant/package.json +28 -0
  149. package/templates/storefront-restaurant/postcss.config.mjs +7 -0
  150. package/templates/storefront-restaurant/tsconfig.json +23 -0
  151. package/templates/storefront-retail/.env.example +5 -0
  152. package/templates/storefront-retail/AGENTS.md +64 -0
  153. package/templates/storefront-retail/app/about/page.tsx +24 -54
  154. package/templates/storefront-retail/app/categories/[slug]/page.tsx +2 -1
  155. package/templates/storefront-retail/app/collections/[slug]/page.tsx +2 -1
  156. package/templates/storefront-retail/app/error.tsx +61 -0
  157. package/templates/storefront-retail/app/faq/page.tsx +11 -92
  158. package/templates/storefront-retail/app/layout.tsx +37 -8
  159. package/templates/storefront-retail/app/llms.txt/route.ts +94 -0
  160. package/templates/storefront-retail/app/not-found.tsx +39 -0
  161. package/templates/storefront-retail/app/page.tsx +16 -73
  162. package/templates/storefront-retail/app/privacy/page.tsx +24 -81
  163. package/templates/storefront-retail/app/products/[slug]/page.tsx +165 -0
  164. package/templates/storefront-retail/app/products/[slug]/product-detail.tsx +71 -0
  165. package/templates/storefront-retail/app/robots.ts +18 -0
  166. package/templates/storefront-retail/app/shop/page.tsx +3 -3
  167. package/templates/storefront-retail/app/sitemap.ts +59 -0
  168. package/templates/storefront-retail/app/terms/page.tsx +24 -97
  169. package/templates/storefront-retail/components/brand-marquee.tsx +6 -15
  170. package/templates/storefront-retail/components/feature-hero.tsx +2 -1
  171. package/templates/storefront-retail/components/footer.tsx +85 -120
  172. package/templates/storefront-retail/components/header.tsx +10 -12
  173. package/templates/storefront-retail/components/newsletter.tsx +7 -9
  174. package/templates/storefront-retail/components/promo-banner.tsx +12 -15
  175. package/templates/storefront-retail/components/store-product-card.tsx +7 -7
  176. package/templates/storefront-retail/components/trade-in-cta.tsx +11 -17
  177. package/templates/storefront-retail/components/trust-bar.tsx +45 -5
  178. package/templates/storefront-retail/lib/brand.ts +567 -0
  179. package/templates/storefront-services/.env.example +16 -0
  180. package/templates/storefront-services/AGENTS.md +41 -0
  181. package/templates/storefront-services/README.md +73 -0
  182. package/templates/storefront-services/app/about/page.tsx +38 -0
  183. package/templates/storefront-services/app/cart/page.tsx +9 -0
  184. package/templates/storefront-services/app/categories/[slug]/listing-client.tsx +19 -0
  185. package/templates/storefront-services/app/categories/[slug]/page.tsx +118 -0
  186. package/templates/storefront-services/app/checkout/page.tsx +17 -0
  187. package/templates/storefront-services/app/collections/[slug]/listing-client.tsx +20 -0
  188. package/templates/storefront-services/app/collections/[slug]/page.tsx +118 -0
  189. package/templates/storefront-services/app/error.tsx +60 -0
  190. package/templates/storefront-services/app/faq/page.tsx +46 -0
  191. package/templates/storefront-services/app/globals.css +45 -0
  192. package/templates/storefront-services/app/layout.tsx +80 -0
  193. package/templates/storefront-services/app/llms.txt/route.ts +94 -0
  194. package/templates/storefront-services/app/not-found.tsx +39 -0
  195. package/templates/storefront-services/app/orders/[id]/page.tsx +21 -0
  196. package/templates/storefront-services/app/page.tsx +97 -0
  197. package/templates/storefront-services/app/privacy/page.tsx +44 -0
  198. package/templates/storefront-services/app/robots.ts +18 -0
  199. package/templates/storefront-services/app/shop/page.tsx +31 -0
  200. package/templates/storefront-services/app/shop/shop-client.tsx +27 -0
  201. package/templates/storefront-services/app/sitemap.ts +62 -0
  202. package/templates/storefront-services/app/terms/page.tsx +44 -0
  203. package/templates/storefront-services/components/cart-pill.tsx +32 -0
  204. package/templates/storefront-services/components/category-grid.tsx +28 -0
  205. package/templates/storefront-services/components/collection-strip.tsx +45 -0
  206. package/templates/storefront-services/components/footer.tsx +148 -0
  207. package/templates/storefront-services/components/header.tsx +43 -0
  208. package/templates/storefront-services/components/hero.tsx +25 -0
  209. package/templates/storefront-services/components/nav-link.tsx +20 -0
  210. package/templates/storefront-services/components/product-modal.tsx +105 -0
  211. package/templates/storefront-services/components/providers.tsx +31 -0
  212. package/templates/storefront-services/components/store-product-card.tsx +83 -0
  213. package/templates/storefront-services/lib/brand.ts +303 -0
  214. package/templates/storefront-services/lib/cart.ts +27 -0
  215. package/templates/storefront-services/next.config.ts +40 -0
  216. package/templates/storefront-services/package.json +28 -0
  217. package/templates/storefront-services/postcss.config.mjs +7 -0
  218. package/templates/storefront-services/tsconfig.json +23 -0
  219. /package/templates/{storefront-retail → storefront-grocery}/components/product-modal.tsx +0 -0
@@ -0,0 +1,130 @@
1
+ import type { Metadata } from "next";
2
+ import { Suspense } from "react";
3
+ import Link from "next/link";
4
+ import { notFound } from "next/navigation";
5
+ import { cacheTag, cacheLife } from "next/cache";
6
+ import {
7
+ getServerClient,
8
+ tags,
9
+ type Category,
10
+ type Product,
11
+ } from "@cimplify/sdk/server";
12
+ import { ListingClient } from "./listing-client";
13
+ import { brand } from "@/lib/brand";
14
+
15
+ interface CategoryData {
16
+ category: Category;
17
+ products: Product[];
18
+ }
19
+
20
+ async function getCategory(slug: string): Promise<CategoryData | null> {
21
+ "use cache";
22
+ cacheTag(tags.categories());
23
+ cacheLife("hours");
24
+
25
+ const client = getServerClient();
26
+ const catRes = await client.catalogue.getCategoryBySlug(slug);
27
+ if (!catRes.ok) return null;
28
+
29
+ cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));
30
+ const r = await client.catalogue.getCategoryProducts(catRes.value.id);
31
+ const products = r.ok
32
+ ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
33
+ : [];
34
+ return { category: catRes.value, products };
35
+ }
36
+
37
+ export async function generateMetadata({
38
+ params,
39
+ }: {
40
+ params: Promise<{ slug: string }>;
41
+ }): Promise<Metadata> {
42
+ const { slug } = await params;
43
+ const data = await getCategory(slug);
44
+ if (!data) return {};
45
+ return {
46
+ title: `${data.category.name} — ${brand.name}`,
47
+ description: data.category.description ?? undefined,
48
+ };
49
+ }
50
+
51
+ export default async function CategoryPage({
52
+ params,
53
+ }: {
54
+ params: Promise<{ slug: string }>;
55
+ }) {
56
+ return (
57
+ <Suspense fallback={<CategorySkeleton />}>
58
+ <CategoryContent params={params} />
59
+ </Suspense>
60
+ );
61
+ }
62
+
63
+ async function CategoryContent({
64
+ params,
65
+ }: {
66
+ params: Promise<{ slug: string }>;
67
+ }) {
68
+ const { slug } = await params;
69
+ const data = await getCategory(slug);
70
+ if (!data) notFound();
71
+
72
+ const { category, products } = data;
73
+ return (
74
+ <>
75
+ <section className="bg-foreground text-background relative overflow-hidden">
76
+ <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]" />
77
+ <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">
78
+ <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">
79
+ <Link href="/" className="hover:text-background transition-colors">Home</Link>
80
+ <span>/</span>
81
+ <Link href="/shop" className="hover:text-background transition-colors">Shop</Link>
82
+ <span>/</span>
83
+ <span className="text-background/90">{category.name}</span>
84
+ </nav>
85
+ <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">
86
+ {category.name}
87
+ </h1>
88
+ {category.description && (
89
+ <p className="mt-3 max-w-2xl text-base text-background/75">
90
+ {category.description}
91
+ </p>
92
+ )}
93
+ <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">
94
+ {products.length} {products.length === 1 ? "product" : "products"}
95
+ </p>
96
+ </div>
97
+ </section>
98
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">
99
+ <ListingClient products={products} />
100
+ {products.length === 0 && (
101
+ <p className="text-center mt-8">
102
+ <Link href="/shop" className="text-primary font-semibold hover:underline">
103
+ ← Browse all products
104
+ </Link>
105
+ </p>
106
+ )}
107
+ </section>
108
+ </>
109
+ );
110
+ }
111
+
112
+ function CategorySkeleton() {
113
+ return (
114
+ <>
115
+ <section className="bg-foreground py-12 sm:py-14">
116
+ <div className="max-w-7xl mx-auto px-6 sm:px-8">
117
+ <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />
118
+ <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />
119
+ </div>
120
+ </section>
121
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">
122
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
123
+ {Array.from({ length: 8 }).map((_, i) => (
124
+ <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
125
+ ))}
126
+ </div>
127
+ </section>
128
+ </>
129
+ );
130
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";
5
+
6
+ export default function CheckoutPage() {
7
+ const router = useRouter();
8
+ return (
9
+ <SdkCheckoutPage
10
+ onComplete={(result) => {
11
+ if (result.success && result.order) {
12
+ router.push(`/orders/${result.order.id}`);
13
+ }
14
+ }}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import { ProductGrid } from "@cimplify/sdk/react";
4
+ import type { Product } from "@cimplify/sdk";
5
+ import { StoreProductCard } from "@/components/store-product-card";
6
+
7
+ /**
8
+ * Client island for the collection listing. Receives server-fetched
9
+ * products as props (serializable) and owns the `renderCard` function
10
+ * (which can't cross the server/client boundary).
11
+ */
12
+ export function ListingClient({ products }: { products: Product[] }) {
13
+ return (
14
+ <ProductGrid
15
+ products={products}
16
+ emptyMessage="No products in this collection yet."
17
+ renderCard={(p) => <StoreProductCard product={p} />}
18
+ />
19
+ );
20
+ }
@@ -0,0 +1,130 @@
1
+ import type { Metadata } from "next";
2
+ import { Suspense } from "react";
3
+ import Link from "next/link";
4
+ import { notFound } from "next/navigation";
5
+ import { cacheTag, cacheLife } from "next/cache";
6
+ import {
7
+ getServerClient,
8
+ tags,
9
+ type Collection,
10
+ type Product,
11
+ } from "@cimplify/sdk/server";
12
+ import { ListingClient } from "./listing-client";
13
+ import { brand } from "@/lib/brand";
14
+
15
+ interface CollectionData {
16
+ collection: Collection;
17
+ products: Product[];
18
+ }
19
+
20
+ async function getCollection(slug: string): Promise<CollectionData | null> {
21
+ "use cache";
22
+ cacheTag(tags.collections());
23
+ cacheLife("hours");
24
+
25
+ const client = getServerClient();
26
+ const colRes = await client.catalogue.getCollectionBySlug(slug);
27
+ if (!colRes.ok) return null;
28
+
29
+ cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));
30
+ const r = await client.catalogue.getCollectionProducts(colRes.value.id);
31
+ const products = r.ok
32
+ ? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
33
+ : [];
34
+ return { collection: colRes.value, products };
35
+ }
36
+
37
+ export async function generateMetadata({
38
+ params,
39
+ }: {
40
+ params: Promise<{ slug: string }>;
41
+ }): Promise<Metadata> {
42
+ const { slug } = await params;
43
+ const data = await getCollection(slug);
44
+ if (!data) return {};
45
+ return {
46
+ title: `${data.collection.name} — ${brand.name}`,
47
+ description: data.collection.description ?? undefined,
48
+ };
49
+ }
50
+
51
+ export default async function CollectionPage({
52
+ params,
53
+ }: {
54
+ params: Promise<{ slug: string }>;
55
+ }) {
56
+ return (
57
+ <Suspense fallback={<CollectionSkeleton />}>
58
+ <CollectionContent params={params} />
59
+ </Suspense>
60
+ );
61
+ }
62
+
63
+ async function CollectionContent({
64
+ params,
65
+ }: {
66
+ params: Promise<{ slug: string }>;
67
+ }) {
68
+ const { slug } = await params;
69
+ const data = await getCollection(slug);
70
+ if (!data) notFound();
71
+
72
+ const { collection, products } = data;
73
+ return (
74
+ <>
75
+ <section className="bg-foreground text-background relative overflow-hidden">
76
+ <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]" />
77
+ <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">
78
+ <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">
79
+ <Link href="/" className="hover:text-background transition-colors">Home</Link>
80
+ <span>/</span>
81
+ <span className="text-background/90">Collections</span>
82
+ <span>/</span>
83
+ <span className="text-background/90">{collection.name}</span>
84
+ </nav>
85
+ <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">
86
+ {collection.name}
87
+ </h1>
88
+ {collection.description && (
89
+ <p className="mt-3 max-w-2xl text-base text-background/75">
90
+ {collection.description}
91
+ </p>
92
+ )}
93
+ <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">
94
+ {products.length} {products.length === 1 ? "product" : "products"}
95
+ </p>
96
+ </div>
97
+ </section>
98
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">
99
+ <ListingClient products={products} />
100
+ {products.length === 0 && (
101
+ <p className="text-center mt-8">
102
+ <Link href="/shop" className="text-primary font-semibold hover:underline">
103
+ ← Browse all products
104
+ </Link>
105
+ </p>
106
+ )}
107
+ </section>
108
+ </>
109
+ );
110
+ }
111
+
112
+ function CollectionSkeleton() {
113
+ return (
114
+ <>
115
+ <section className="bg-foreground py-12 sm:py-14">
116
+ <div className="max-w-7xl mx-auto px-6 sm:px-8">
117
+ <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />
118
+ <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />
119
+ </div>
120
+ </section>
121
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">
122
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
123
+ {Array.from({ length: 8 }).map((_, i) => (
124
+ <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
125
+ ))}
126
+ </div>
127
+ </section>
128
+ </>
129
+ );
130
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import Link from "next/link";
5
+
6
+ /**
7
+ * Root error boundary. Next.js calls this whenever a thrown error escapes
8
+ * a Server Component without being caught by a closer `error.tsx`.
9
+ *
10
+ * Wire `reportError` (Sentry, Datadog, etc.) here so production failures
11
+ * surface — silently recovering hides real bugs.
12
+ */
13
+ export default function GlobalError({
14
+ error,
15
+ reset,
16
+ }: {
17
+ error: Error & { digest?: string };
18
+ reset: () => void;
19
+ }) {
20
+ useEffect(() => {
21
+ // TODO: replace with your error reporter
22
+ // eslint-disable-next-line no-console
23
+ console.error("Storefront error:", error);
24
+ }, [error]);
25
+
26
+ return (
27
+ <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">
28
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">
29
+ Something broke
30
+ </p>
31
+ <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold mb-4 -tracking-[0.025em]">
32
+ That wasn&apos;t supposed to happen.
33
+ </h1>
34
+ <p className="text-muted-foreground leading-relaxed mb-8">
35
+ We&apos;ve been notified. Refresh the page, or head back home — most
36
+ of the time the second try just works.
37
+ </p>
38
+ {error.digest && (
39
+ <p className="text-xs font-mono text-muted-foreground mb-6">
40
+ Reference:{" "}
41
+ <code className="text-foreground">{error.digest}</code>
42
+ </p>
43
+ )}
44
+ <div className="flex flex-wrap items-center justify-center gap-3">
45
+ <button
46
+ type="button"
47
+ onClick={reset}
48
+ 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"
49
+ >
50
+ Try again
51
+ </button>
52
+ <Link
53
+ href="/"
54
+ 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"
55
+ >
56
+ Back home
57
+ </Link>
58
+ </div>
59
+ </section>
60
+ );
61
+ }
@@ -0,0 +1,46 @@
1
+ import type { Metadata } from "next";
2
+ import { brand } from "@/lib/brand";
3
+
4
+ export const metadata: Metadata = {
5
+ title: `Support — ${brand.name}`,
6
+ description: "Shipping, warranty, repairs, returns, payment — answers to the questions we hear most often.",
7
+ };
8
+
9
+ export default function FaqPage() {
10
+ const f = brand.faq;
11
+ return (
12
+ <article className="max-w-3xl mx-auto px-8 py-16">
13
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
14
+ {f.eyebrow}
15
+ </p>
16
+ <h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">
17
+ {f.title}
18
+ </h1>
19
+ <div className="space-y-12">
20
+ {f.sections.map((section) => (
21
+ <section key={section.title}>
22
+ <h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">{section.title}</h2>
23
+ <dl className="space-y-6">
24
+ {section.items.map((item) => (
25
+ <div key={item.q}>
26
+ <dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>
27
+ <dd className="text-muted-foreground leading-relaxed">{item.a}</dd>
28
+ </div>
29
+ ))}
30
+ </dl>
31
+ </section>
32
+ ))}
33
+ </div>
34
+ <p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">
35
+ {f.contactPrompt}{" "}
36
+ <a
37
+ href={`mailto:${f.contactEmail}`}
38
+ className="text-primary font-semibold hover:underline"
39
+ >
40
+ {f.contactEmail}
41
+ </a>{" "}
42
+ and a real human will reply within 24 hours.
43
+ </p>
44
+ </article>
45
+ );
46
+ }
@@ -0,0 +1,46 @@
1
+ @import "tailwindcss";
2
+
3
+ @source "../node_modules/@cimplify/sdk/dist";
4
+
5
+ /* Studio FRX — Nike/Aritzia-style streetwear palette. High-contrast
6
+ neutrals with one electric accent. Tweak primary to retheme everything. */
7
+ @theme {
8
+ --color-background: oklch(0.99 0 0);
9
+ --color-foreground: oklch(0.12 0 0);
10
+ --color-card: oklch(1 0 0);
11
+ --color-card-foreground: oklch(0.12 0 0);
12
+ --color-popover: oklch(1 0 0);
13
+ --color-popover-foreground: oklch(0.12 0 0);
14
+ --color-primary: oklch(0.7 0.24 30);
15
+ --color-primary-foreground: oklch(0.99 0 0);
16
+ --color-secondary: oklch(0.96 0 0);
17
+ --color-secondary-foreground: oklch(0.18 0 0);
18
+ --color-muted: oklch(0.96 0 0);
19
+ --color-muted-foreground: oklch(0.45 0 0);
20
+ --color-accent: oklch(0.92 0 0);
21
+ --color-accent-foreground: oklch(0.18 0 0);
22
+ --color-destructive: oklch(0.58 0.24 27);
23
+ --color-destructive-foreground: oklch(0.99 0 0);
24
+ --color-border: oklch(0.9 0 0);
25
+ --color-input: oklch(0.93 0 0);
26
+ --color-ring: oklch(0.7 0.24 30);
27
+ --radius: 0.125rem;
28
+ }
29
+
30
+ @layer base {
31
+ html, body {
32
+ margin: 0;
33
+ padding: 0;
34
+ background: var(--color-background);
35
+ color: var(--color-foreground);
36
+ -webkit-font-smoothing: antialiased;
37
+ -moz-osx-font-smoothing: grayscale;
38
+ }
39
+ a { color: inherit; text-decoration: none; }
40
+ h1, h2, h3, h4 {
41
+ font-family: var(--font-display);
42
+ letter-spacing: -0.04em;
43
+ font-weight: 800;
44
+ line-height: 0.95;
45
+ }
46
+ }
@@ -0,0 +1,76 @@
1
+ import type { Metadata } from "next";
2
+ import { Inter, Anton } from "next/font/google";
3
+ import "./globals.css";
4
+ import { Providers } from "@/components/providers";
5
+ import { Header } from "@/components/header";
6
+ import { Footer } from "@/components/footer";
7
+ import { brand } from "@/lib/brand";
8
+
9
+ const inter = Inter({
10
+ subsets: ["latin"],
11
+ variable: "--font-sans",
12
+ display: "swap",
13
+ });
14
+
15
+ const anton = Anton({
16
+ subsets: ["latin"],
17
+ variable: "--font-display",
18
+ weight: "400",
19
+ display: "swap",
20
+ });
21
+
22
+ const SITE_URL =
23
+ process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
24
+
25
+ export const metadata: Metadata = {
26
+ metadataBase: new URL(SITE_URL),
27
+ title: {
28
+ default: brand.name,
29
+ template: `%s — ${brand.name}`,
30
+ },
31
+ description: brand.description,
32
+ openGraph: {
33
+ type: "website",
34
+ siteName: brand.name,
35
+ locale: brand.locale,
36
+ },
37
+ twitter: { card: "summary_large_image" },
38
+ };
39
+
40
+ const ORGANIZATION_LD = {
41
+ "@context": "https://schema.org",
42
+ "@type": brand.schemaType,
43
+ name: brand.name,
44
+ url: SITE_URL,
45
+ description: brand.description,
46
+ email: brand.contact.email,
47
+ telephone: brand.contact.phoneTel,
48
+ address: {
49
+ "@type": "PostalAddress",
50
+ streetAddress: brand.contact.streetAddress,
51
+ addressLocality: brand.contact.city,
52
+ addressCountry: brand.contact.countryCode,
53
+ },
54
+ sameAs: brand.socials.map((s) => s.href),
55
+ };
56
+
57
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
58
+ return (
59
+ <html lang="en" className={`${inter.variable} ${anton.variable}`}>
60
+ <body
61
+ suppressHydrationWarning
62
+ className="min-h-screen flex flex-col bg-background text-foreground font-sans"
63
+ >
64
+ <script
65
+ type="application/ld+json"
66
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(ORGANIZATION_LD) }}
67
+ />
68
+ <Providers>
69
+ <Header />
70
+ <main className="flex-1 pb-12 w-full">{children}</main>
71
+ <Footer />
72
+ </Providers>
73
+ </body>
74
+ </html>
75
+ );
76
+ }
@@ -0,0 +1,94 @@
1
+ import { cacheTag, cacheLife } from "next/cache";
2
+ import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
3
+ import { brand } from "@/lib/brand";
4
+
5
+ const SITE_URL =
6
+ process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
7
+
8
+ async function buildLlmsTxt(): Promise<string> {
9
+ "use cache";
10
+ cacheTag(tags.products(), tags.categories(), tags.collections());
11
+ cacheLife("hours");
12
+
13
+ const client = getServerClient();
14
+ const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
15
+ client.catalogue.getProducts({ limit: 500 }),
16
+ client.catalogue.getCategories(),
17
+ client.catalogue.getCollections(),
18
+ ]);
19
+
20
+ const products: Product[] = productsRes.ok ? productsRes.value.items : [];
21
+ const categories = categoriesRes.ok ? categoriesRes.value : [];
22
+ const collections = collectionsRes.ok ? collectionsRes.value : [];
23
+
24
+ const lines: string[] = [];
25
+ lines.push(`# ${brand.name}`);
26
+ lines.push("");
27
+ lines.push(`> ${brand.llms.summary}`);
28
+ lines.push("");
29
+ lines.push("## Browse");
30
+ lines.push(`- [Home](${SITE_URL}/)`);
31
+ lines.push(`- [Shop](${SITE_URL}/shop): Full catalogue with filter and sort.`);
32
+
33
+ if (categories.length > 0) {
34
+ lines.push("");
35
+ lines.push("## Categories");
36
+ for (const c of categories) {
37
+ lines.push(
38
+ `- [${c.name}](${SITE_URL}/categories/${c.slug})${c.description ? `: ${c.description}` : ""}`,
39
+ );
40
+ }
41
+ }
42
+
43
+ if (collections.length > 0) {
44
+ lines.push("");
45
+ lines.push("## Collections");
46
+ for (const c of collections) {
47
+ lines.push(
48
+ `- [${c.name}](${SITE_URL}/collections/${c.slug})${c.description ? `: ${c.description}` : ""}`,
49
+ );
50
+ }
51
+ }
52
+
53
+ if (products.length > 0) {
54
+ lines.push("");
55
+ lines.push("## Products");
56
+ for (const p of products) {
57
+ const slug = p.slug ?? p.id;
58
+ const price = `${p.currency ?? brand.currency} ${p.default_price}`;
59
+ const desc = p.description ? ` — ${p.description.replace(/\s+/g, " ").slice(0, 200)}` : "";
60
+ lines.push(`- [${p.name}](${SITE_URL}/products/${slug}) (${price})${desc}`);
61
+ }
62
+ }
63
+
64
+ lines.push("");
65
+ lines.push("## Information");
66
+ lines.push(`- [About](${SITE_URL}/about)`);
67
+ lines.push(`- [Support / FAQ](${SITE_URL}/faq)`);
68
+ lines.push(`- [Terms of Service](${SITE_URL}/terms)`);
69
+ lines.push(`- [Privacy Policy](${SITE_URL}/privacy)`);
70
+ lines.push(`- [Sitemap (XML)](${SITE_URL}/sitemap.xml)`);
71
+ lines.push("");
72
+ lines.push("## Contact");
73
+ lines.push(`- Email: ${brand.contact.email}`);
74
+ lines.push(`- Phone: ${brand.contact.phone}`);
75
+ lines.push(`- Address: ${brand.contact.address}`);
76
+
77
+ return lines.join("\n") + "\n";
78
+ }
79
+
80
+ /**
81
+ * `/llms.txt` — machine-readable site index for LLMs (per llmstxt.org).
82
+ * Lets coding agents and chat assistants find products, categories, and
83
+ * support pages without scraping HTML. Plain Markdown so it streams cheaply
84
+ * into context windows.
85
+ */
86
+ export async function GET(): Promise<Response> {
87
+ const body = await buildLlmsTxt();
88
+ return new Response(body, {
89
+ headers: {
90
+ "Content-Type": "text/plain; charset=utf-8",
91
+ "Cache-Control": "public, max-age=0, s-maxage=3600, stale-while-revalidate=86400",
92
+ },
93
+ });
94
+ }
@@ -0,0 +1,39 @@
1
+ import type { Metadata } from "next";
2
+ import Link from "next/link";
3
+ import { brand } from "@/lib/brand";
4
+
5
+ export const metadata: Metadata = {
6
+ title: `Page not found — ${brand.name}`,
7
+ description: "We couldn't find that page.",
8
+ };
9
+
10
+ export default function NotFound() {
11
+ return (
12
+ <section className="max-w-2xl mx-auto px-6 sm:px-8 py-20 text-center">
13
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-3">
14
+ 404
15
+ </p>
16
+ <h1 className="text-[clamp(2.5rem,5vw,4rem)] font-bold mb-4 -tracking-[0.03em]">
17
+ Page not found.
18
+ </h1>
19
+ <p className="text-muted-foreground leading-relaxed mb-8">
20
+ The URL you followed might be old, a typo, or pointing at a product
21
+ that&apos;s no longer in stock. Try the menu or head back home.
22
+ </p>
23
+ <div className="flex flex-wrap items-center justify-center gap-3">
24
+ <Link
25
+ href="/shop"
26
+ 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"
27
+ >
28
+ Browse the shop
29
+ </Link>
30
+ <Link
31
+ href="/"
32
+ 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"
33
+ >
34
+ Back home
35
+ </Link>
36
+ </div>
37
+ </section>
38
+ );
39
+ }