@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,43 @@
1
+ import Link from "next/link";
2
+ import { Suspense } from "react";
3
+ import { NavLink } from "./nav-link";
4
+ import { CartPill, CartPillSkeleton } from "./cart-pill";
5
+ import { brand } from "@/lib/brand";
6
+
7
+ /**
8
+ * Server-rendered header chrome. Brand mark + nav layout streams from the
9
+ * cache; the active-link styling and live cart count are dynamic islands
10
+ * mounted in their own Suspense boundaries so the chrome never blocks.
11
+ */
12
+ export function Header() {
13
+ return (
14
+ <header className="sticky top-0 z-30 flex items-center justify-between px-8 py-4 border-b border-border bg-background/90 backdrop-blur-md">
15
+ <Link href="/" className="flex items-baseline gap-2">
16
+ <span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">
17
+ {brand.shortName}
18
+ </span>
19
+ <span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
20
+ {brand.microTag}
21
+ </span>
22
+ </Link>
23
+ <nav className="flex items-center gap-6">
24
+ {brand.header.nav.map((link) => (
25
+ <Suspense key={link.href} fallback={<NavLinkFallback>{link.label}</NavLinkFallback>}>
26
+ <NavLink href={link.href}>{link.label}</NavLink>
27
+ </Suspense>
28
+ ))}
29
+ <Suspense fallback={<CartPillSkeleton />}>
30
+ <CartPill />
31
+ </Suspense>
32
+ </nav>
33
+ </header>
34
+ );
35
+ }
36
+
37
+ function NavLinkFallback({ children }: { children: React.ReactNode }) {
38
+ return (
39
+ <span className="text-[13px] font-medium tracking-wide text-muted-foreground">
40
+ {children}
41
+ </span>
42
+ );
43
+ }
@@ -0,0 +1,25 @@
1
+ interface HeroProps {
2
+ badge?: string;
3
+ title: string;
4
+ subtitle?: string;
5
+ }
6
+
7
+ export function Hero({ badge, title, subtitle }: HeroProps) {
8
+ return (
9
+ <section className="px-8 py-16 text-center bg-gradient-to-b from-accent to-background">
10
+ {badge && (
11
+ <span className="inline-block mb-4 px-3.5 py-1.5 rounded-full bg-card border border-accent text-accent-foreground text-[11px] font-semibold uppercase tracking-[0.12em]">
12
+ {badge}
13
+ </span>
14
+ )}
15
+ <h1 className="font-serif text-[clamp(2.25rem,5vw,3.5rem)] font-semibold m-0 -tracking-[0.02em]">
16
+ {title}
17
+ </h1>
18
+ {subtitle && (
19
+ <p className="mx-auto mt-2 max-w-xl text-base text-muted-foreground">
20
+ {subtitle}
21
+ </p>
22
+ )}
23
+ </section>
24
+ );
25
+ }
@@ -0,0 +1,20 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
7
+ const pathname = usePathname();
8
+ const active = pathname === href;
9
+ return (
10
+ <Link
11
+ href={href}
12
+ className={[
13
+ "text-[13px] font-medium tracking-wide transition-colors",
14
+ active ? "text-primary" : "text-muted-foreground hover:text-foreground",
15
+ ].join(" ")}
16
+ >
17
+ {children}
18
+ </Link>
19
+ );
20
+ }
@@ -0,0 +1,31 @@
1
+ "use client";
2
+
3
+ import { useMemo, type ReactNode } from "react";
4
+ import { createCimplifyClient } from "@cimplify/sdk";
5
+ import { CimplifyProvider } from "@cimplify/sdk/react";
6
+
7
+ /**
8
+ * Boots the Cimplify SDK client once on the client-side and exposes it via
9
+ * <CimplifyProvider/>.
10
+ *
11
+ * Base-URL resolution:
12
+ * 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.
13
+ * 2) Otherwise use the current origin so requests flow through the
14
+ * Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).
15
+ * 3) Fall back to 127.0.0.1:8787 only during SSR, when there's no window.
16
+ */
17
+ export function Providers({ children }: { children: ReactNode }) {
18
+ const client = useMemo(() => {
19
+ const baseUrl =
20
+ process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||
21
+ (typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");
22
+ const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";
23
+ return createCimplifyClient({
24
+ baseUrl,
25
+ publicKey,
26
+ suppressPublicKeyWarning: true,
27
+ });
28
+ }, []);
29
+
30
+ return <CimplifyProvider client={client}>{children}</CimplifyProvider>;
31
+ }
@@ -0,0 +1,83 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import {
5
+ CardVariant,
6
+ FoodProductCard,
7
+ RetailProductCard,
8
+ WholesaleProductCard,
9
+ DigitalProductCard,
10
+ BundleProductCard,
11
+ CompositeProductCard,
12
+ StandardServiceCard,
13
+ RentalServiceCard,
14
+ AccommodationCard,
15
+ LeaseServiceCard,
16
+ SubscriptionCard,
17
+ } from "@cimplify/sdk/react";
18
+ import type { CardLayoutProps } from "@cimplify/sdk/react";
19
+ import type { Product } from "@cimplify/sdk";
20
+ import { PRODUCT_TYPE, RENDER_HINT, DURATION_UNIT } from "@cimplify/sdk";
21
+
22
+ const RENTAL_UNITS = new Set<string>([DURATION_UNIT.Days, DURATION_UNIT.Hours]);
23
+ const LEASE_UNITS = new Set<string>([DURATION_UNIT.Weeks, DURATION_UNIT.Months, DURATION_UNIT.Years]);
24
+
25
+ const VARIANT_CARDS: Record<CardVariant, React.ComponentType<CardLayoutProps>> = {
26
+ [CardVariant.Food]: FoodProductCard,
27
+ [CardVariant.Retail]: RetailProductCard,
28
+ [CardVariant.Wholesale]: WholesaleProductCard,
29
+ [CardVariant.Digital]: DigitalProductCard,
30
+ [CardVariant.Bundle]: BundleProductCard,
31
+ [CardVariant.Composite]: CompositeProductCard,
32
+ [CardVariant.Standard]: StandardServiceCard,
33
+ [CardVariant.Rental]: RentalServiceCard,
34
+ [CardVariant.Accommodation]: AccommodationCard,
35
+ [CardVariant.Lease]: LeaseServiceCard,
36
+ [CardVariant.Subscription]: SubscriptionCard,
37
+ };
38
+
39
+ function resolveVariant(p: Product): CardVariant {
40
+ if (p.type === PRODUCT_TYPE.Bundle) return CardVariant.Bundle;
41
+ if (p.type === PRODUCT_TYPE.Composite) return CardVariant.Composite;
42
+ if (p.quantity_pricing && p.quantity_pricing.length > 1) return CardVariant.Wholesale;
43
+ if (p.type === PRODUCT_TYPE.Digital) return CardVariant.Digital;
44
+ if (p.type === PRODUCT_TYPE.Service) {
45
+ if (p.duration_unit && RENTAL_UNITS.has(p.duration_unit)) return CardVariant.Rental;
46
+ if (p.duration_unit === DURATION_UNIT.Nights) return CardVariant.Accommodation;
47
+ if (p.duration_unit && LEASE_UNITS.has(p.duration_unit)) return CardVariant.Lease;
48
+ if (p.billing_plans && p.billing_plans.length > 0 && !p.duration_minutes) return CardVariant.Subscription;
49
+ return CardVariant.Standard;
50
+ }
51
+ if (p.render_hint === RENDER_HINT.Food) return CardVariant.Food;
52
+ return CardVariant.Retail;
53
+ }
54
+
55
+ interface Props {
56
+ product: Product;
57
+ /** Override the auto-detected card variant. */
58
+ variant?: CardVariant;
59
+ }
60
+
61
+ /**
62
+ * Variant-aware product card that opens the URL-driven product modal on
63
+ * click. Uses `next/link` with `?product=<slug>` so the link is statically
64
+ * pre-renderable — no `useSearchParams` dependency means cards don't
65
+ * become dynamic islands. The modal (which already reads `useSearchParams`
66
+ * inside a Suspense boundary in the root layout) handles the rest.
67
+ */
68
+ export function StoreProductCard({ product, variant }: Props) {
69
+ const slug = product.slug || product.id;
70
+ const Card = VARIANT_CARDS[variant ?? resolveVariant(product)];
71
+ const href = `?product=${encodeURIComponent(slug)}`;
72
+
73
+ return (
74
+ <Card
75
+ product={product}
76
+ renderLink={({ className, children }) => (
77
+ <Link href={href} scroll={false} prefetch={false} className={className}>
78
+ {children}
79
+ </Link>
80
+ )}
81
+ />
82
+ );
83
+ }
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Brand & content configuration — single source of truth for every visible
3
+ * string. Edit this file to rebrand. See ../AGENTS.md.
4
+ */
5
+
6
+ export interface BrandSocial {
7
+ label: string;
8
+ href: string;
9
+ icon?: "instagram" | "x" | "tiktok" | "facebook" | "youtube" | "linkedin" | "whatsapp";
10
+ }
11
+
12
+ export interface BrandFaqEntry { q: string; a: string }
13
+ export interface BrandFaqSection { title: string; items: BrandFaqEntry[] }
14
+ export interface BrandPolicySection {
15
+ heading: string;
16
+ body: string | { intro: string; bullets: string[] };
17
+ }
18
+ export interface BrandSitemapSection { title: string; links: { label: string; href: string }[] }
19
+
20
+ export interface Brand {
21
+ name: string; shortName: string; microTag: string; description: string;
22
+ schemaType:
23
+ | "Store" | "Bakery" | "Restaurant" | "BeautySalon"
24
+ | "GroceryStore" | "LocalBusiness" | "Organization";
25
+ currency: string; locale: string;
26
+ contact: {
27
+ address: string; streetAddress: string; city: string; countryCode: string;
28
+ phone: string; phoneTel: string; email: string; privacyEmail: string;
29
+ supportEmail?: string; businessEmail?: string; hours: string;
30
+ };
31
+ socials: BrandSocial[];
32
+ header: { nav: { label: string; href: string }[] };
33
+ hero: {
34
+ badge: string; title: string; subtitle: string;
35
+ primaryCtaLabel: string;
36
+ secondaryCtaLabel?: string; secondaryCtaHref?: string;
37
+ };
38
+ trustItems?: { label: string; value: string; description: string; iconKey: string }[];
39
+ brandStrip?: { headline: string; brands: string[] };
40
+ promo?: { badge: string; title: string; body: string; ctaLabel: string; ctaHref: string };
41
+ tradeIn?: {
42
+ eyebrow: string; title: string; body: string;
43
+ primaryCtaLabel: string; primaryCtaHref: string;
44
+ secondaryCtaLabel: string; secondaryCtaHref: string;
45
+ steps: { step: string; title: string; body: string }[];
46
+ };
47
+ newsletter: {
48
+ eyebrow: string; title: string; body: string; placeholder: string;
49
+ submitLabel: string; successLabel: string;
50
+ };
51
+ about: {
52
+ eyebrow: string; title: string; paragraphs: string[];
53
+ sections: { heading: string; body: string }[];
54
+ };
55
+ faq: {
56
+ eyebrow: string; title: string; sections: BrandFaqSection[];
57
+ contactPrompt: string; contactEmail: string;
58
+ };
59
+ terms: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
60
+ privacy: { eyebrow: string; title: string; lastUpdated: string; sections: BrandPolicySection[] };
61
+ footer: {
62
+ blurb: string; sitemap: BrandSitemapSection[];
63
+ poweredBy?: { label: string; href: string };
64
+ };
65
+ llms: { summary: string };
66
+ mock: { seed: string; businessId: string };
67
+ }
68
+
69
+ export const brand: Brand = {
70
+ name: "FreshMart",
71
+ shortName: "FreshMart",
72
+ microTag: "GROCER · ACCRA",
73
+ description:
74
+ "Local produce, weekly basket boxes, and pantry staples — delivered to your door across Greater Accra.",
75
+ schemaType: "GroceryStore",
76
+
77
+ currency: "GHS",
78
+ locale: "en_GH",
79
+
80
+ contact: {
81
+ address: "Achimota Market, Accra",
82
+ streetAddress: "Achimota Market",
83
+ city: "Accra",
84
+ countryCode: "GH",
85
+ phone: "+233 244 555 100",
86
+ phoneTel: "+233244555100",
87
+ email: "hello@freshmart.test",
88
+ privacyEmail: "privacy@freshmart.test",
89
+ hours: "Mon–Sat · 7am–9pm · Sun 8am–7pm",
90
+ },
91
+
92
+ socials: [
93
+ { label: "Instagram", href: "https://instagram.com/freshmart", icon: "instagram" },
94
+ { label: "Facebook", href: "https://facebook.com/freshmart", icon: "facebook" },
95
+ { label: "WhatsApp", href: "https://wa.me/233244555100", icon: "whatsapp" },
96
+ ],
97
+
98
+ header: {
99
+ nav: [
100
+ { label: "Shop", href: "/shop" },
101
+ { label: "Weekly box", href: "/categories/baskets" },
102
+ { label: "Deals", href: "/categories/deals" },
103
+ { label: "About", href: "/about" },
104
+ ],
105
+ },
106
+
107
+ hero: {
108
+ badge: "Free delivery over GH₵150",
109
+ title: "Fresh today.\nIn your kitchen by 5pm.",
110
+ subtitle:
111
+ "Local produce, weekly basket boxes, and pantry staples — sourced from Ghanaian farms, packed this morning, delivered today.",
112
+ primaryCtaLabel: "Shop the aisle",
113
+ secondaryCtaLabel: "Build a basket box",
114
+ secondaryCtaHref: "/categories/baskets",
115
+ },
116
+
117
+ trustItems: [
118
+ { label: "Delivery", value: "Same-day in Accra", description: "Order before 2pm, in your kitchen by 5pm.", iconKey: "delivery" },
119
+ { label: "Sourcing", value: "Direct from farms", description: "70% of produce from Volta + Eastern Region.", iconKey: "verified" },
120
+ { label: "Returns", value: "Anything off, refund", description: "Quality-guaranteed, no questions asked.", iconKey: "warranty" },
121
+ { label: "Subscribe", value: "Weekly basket box", description: "Pause or skip any week.", iconKey: "support" },
122
+ ],
123
+
124
+ promo: {
125
+ badge: "This week",
126
+ title: "Tomato season — GH₵12/kg, today only.",
127
+ body: "Roma, plum, and Volta cherry — direct from Akwapim farmers, picked this morning. Stock up while it's at peak. Limit 5kg per order.",
128
+ ctaLabel: "Shop tomatoes",
129
+ ctaHref: "/categories/produce",
130
+ },
131
+
132
+ newsletter: {
133
+ eyebrow: "FreshMart weekly",
134
+ title: "What's in season, every Sunday.",
135
+ body: "We email a one-page roundup every Sunday: this week's deals, new produce, recipe of the week. Unsubscribe in one click.",
136
+ placeholder: "you@email.com",
137
+ submitLabel: "Get the weekly",
138
+ successLabel: "Subscribed ✓",
139
+ },
140
+
141
+ about: {
142
+ eyebrow: "About FreshMart",
143
+ title: "Local food,\nactually.",
144
+ paragraphs: [
145
+ "FreshMart started in 2022 as a stall at Achimota Market with one delivery rider and a notebook. Today we run a 600-square-metre warehouse and a fleet of nine refrigerated bikes, with one rule: if it's not in season locally, it doesn't lead our shelves.",
146
+ "70% of our produce comes from Ghanaian farms — Akwapim tomatoes, Volta-region pineapple, Brong-Ahafo yam, Nkawkaw plantain. The remaining 30% (citrus, certain herbs, dairy) we source from a tight set of partners we visit quarterly.",
147
+ "Order online by 2pm; we deliver same-day across Greater Accra. Subscribe to our weekly basket box and we'll curate a mix of in-season produce, dairy, and pantry staples every Tuesday or Friday.",
148
+ ],
149
+ sections: [
150
+ {
151
+ heading: "The basket box",
152
+ body: "GH₵180 for the small (feeds 2 for a week), GH₵320 for the family (feeds 4). Tuesday or Friday delivery. Pause, skip, or cancel any week from your account.",
153
+ },
154
+ {
155
+ heading: "Wholesale",
156
+ body: "Cafés, restaurants, and meal-prep operations: we deliver tiered-priced, predictable orders. Email wholesale@freshmart.test with your weekly volume and we'll quote.",
157
+ },
158
+ {
159
+ heading: "Visit",
160
+ body: "Achimota Market, Stalls 14–22. Mon–Sat 7am–9pm, Sun 8am–7pm.",
161
+ },
162
+ ],
163
+ },
164
+
165
+ faq: {
166
+ eyebrow: "Q&A",
167
+ title: "Things people ask.",
168
+ sections: [
169
+ {
170
+ title: "Ordering & delivery",
171
+ items: [
172
+ { q: "When will my order arrive?", a: "Order before 2pm — same-day delivery, in your kitchen by 5pm. After 2pm, next-day before 11am." },
173
+ { q: "Where do you deliver?", a: "Greater Accra, free over GH₵150. Outside Accra, next-day shipping via partner courier." },
174
+ { q: "Can I choose a delivery slot?", a: "Yes — 12pm, 3pm, or 5pm windows in Accra. Subscriber boxes deliver Tuesday or Friday morning." },
175
+ { q: "What if I'm not home?", a: "We coordinate with you by SMS and call. If there's no one, we hold the order at the warehouse and re-attempt the next day at no charge." },
176
+ ],
177
+ },
178
+ {
179
+ title: "Quality & sourcing",
180
+ items: [
181
+ { q: "How fresh is your produce?", a: "Most produce is picked the same day or the day before. We email batch dates with every order." },
182
+ { q: "Is it organic?", a: "We label every product clearly: \"organic\", \"low-spray\", or conventional. About 30% of our produce is certified organic; another 50% is low-spray traceable to specific farms." },
183
+ { q: "What if something arrives off?", a: "Send us a photo within 24 hours. We refund or replace, no questions asked." },
184
+ ],
185
+ },
186
+ {
187
+ title: "Subscriptions",
188
+ items: [
189
+ { q: "How does the basket box work?", a: "Pick small or family. Pick Tuesday or Friday delivery. We curate a mix of in-season produce, dairy, and pantry. Pause, skip, or cancel any week from your account." },
190
+ { q: "Can I customise the box?", a: "Yes — toggle dietary preferences (vegetarian, no-spice, low-carb) and exclude up to 3 items per week." },
191
+ { q: "What if I'm travelling?", a: "Pause the box for any number of weeks. We won't bill while paused." },
192
+ ],
193
+ },
194
+ ],
195
+ contactPrompt: "Other questions? Message us:",
196
+ contactEmail: "hello@freshmart.test",
197
+ },
198
+
199
+ terms: {
200
+ eyebrow: "Terms of service",
201
+ title: "Terms of Service",
202
+ lastUpdated: "1 May 2026",
203
+ sections: [
204
+ { heading: "1. Who we are", body: "FreshMart (\"we\", \"us\") is a grocery business operating from Achimota Market, Accra. By placing an order, you (\"you\", \"customer\") agree to these terms." },
205
+ { heading: "2. Pricing & weights", body: "Prices in GH₵, by weight or unit as marked. Final price computed at checkout based on actual packed weight; we never charge more than 10% over the listed estimate." },
206
+ { heading: "3. Delivery", body: "Same-day in Greater Accra (orders before 2pm); next-day for orders after. Free over GH₵150." },
207
+ { heading: "4. Quality guarantee", body: "Anything off — wrong, spoiled, or not as described — refund or replacement, on us. Photo evidence within 24 hours." },
208
+ { heading: "5. Subscriptions", body: "Weekly basket boxes auto-renew. Pause, skip, or cancel before the next billing date with no penalty." },
209
+ { heading: "6. Liability", body: "Maximum liability for any order is its value. We're not liable for indirect or consequential losses." },
210
+ { heading: "7. Governing law", body: "These terms are governed by the laws of Ghana." },
211
+ ],
212
+ },
213
+
214
+ privacy: {
215
+ eyebrow: "Privacy",
216
+ title: "Privacy Policy",
217
+ lastUpdated: "1 May 2026",
218
+ sections: [
219
+ { heading: "What we collect", body: "Order details (name, address, phone, email), delivery preferences, dietary toggles for subscriptions, and payment data via our processors. We do not store payment cards." },
220
+ { heading: "How we use it", body: { intro: "We use your data to:", bullets: [
221
+ "Pack and deliver your order.",
222
+ "Send delivery updates by SMS.",
223
+ "Curate weekly basket boxes (only for active subscribers).",
224
+ "Send our weekly roundup (only if you opted in).",
225
+ ] } },
226
+ { heading: "Who we share it with", body: "Payment processors (Paystack, Stripe), delivery riders, and our SMS provider. No one else." },
227
+ { heading: "Your rights", body: "Under the Ghana Data Protection Act, 2012 (Act 843), you can access, correct, or delete your data. Email privacy@freshmart.test." },
228
+ { heading: "Retention", body: "Order records: 7 years (tax). Subscription preferences: until you cancel + 90 days." },
229
+ ],
230
+ },
231
+
232
+ footer: {
233
+ blurb:
234
+ "Local produce, weekly basket boxes, and pantry staples. Sourced from Ghanaian farms, delivered same-day across Accra.",
235
+ sitemap: [
236
+ {
237
+ title: "Shop",
238
+ links: [
239
+ { label: "All products", href: "/shop" },
240
+ { label: "Produce", href: "/categories/produce" },
241
+ { label: "Dairy & eggs", href: "/categories/dairy" },
242
+ { label: "Pantry", href: "/categories/pantry" },
243
+ { label: "Frozen", href: "/categories/frozen" },
244
+ { label: "Deals", href: "/categories/deals" },
245
+ ],
246
+ },
247
+ {
248
+ title: "Subscribe",
249
+ links: [
250
+ { label: "Weekly basket box", href: "/categories/baskets" },
251
+ { label: "How it works", href: "/about" },
252
+ { label: "Manage subscription", href: "/about" },
253
+ ],
254
+ },
255
+ {
256
+ title: "Help",
257
+ links: [
258
+ { label: "Contact", href: "mailto:hello@freshmart.test" },
259
+ { label: "FAQ", href: "/faq" },
260
+ { label: "Quality guarantee", href: "/faq" },
261
+ { label: "Wholesale", href: "mailto:wholesale@freshmart.test" },
262
+ ],
263
+ },
264
+ {
265
+ title: "Legal",
266
+ links: [
267
+ { label: "Terms of Service", href: "/terms" },
268
+ { label: "Privacy Policy", href: "/privacy" },
269
+ ],
270
+ },
271
+ ],
272
+ poweredBy: { label: "Cimplify", href: "https://app.cimplify.io" },
273
+ },
274
+
275
+ llms: {
276
+ summary:
277
+ "FreshMart — grocery delivery from Achimota Market, Accra. Direct-from-farm produce, weekly basket box subscriptions, pantry staples. Same-day delivery in Greater Accra; free over GH₵150. Pause-anytime subscriptions for households and wholesale tiers for cafés.",
278
+ },
279
+
280
+ mock: {
281
+ seed: "grocery",
282
+ businessId: "bus_freshmart",
283
+ },
284
+ };
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useCimplifyClient } from "@cimplify/sdk/react";
5
+
6
+ /**
7
+ * Tiny hook for the live cart-count pill in the header. Re-fetches on
8
+ * mount and exposes a refresh callback. The SDK's `useCart` hook is
9
+ * fine-grained but heavier — this is enough for a header badge.
10
+ */
11
+ export function useCartCount(): { count: number; refresh: () => void } {
12
+ const { client } = useCimplifyClient();
13
+ const [count, setCount] = useState(0);
14
+
15
+ const refresh = () => {
16
+ void client.cart.getCount().then((r) => {
17
+ if (r.ok) setCount(r.value);
18
+ });
19
+ };
20
+
21
+ useEffect(() => {
22
+ refresh();
23
+ // eslint-disable-next-line react-hooks/exhaustive-deps
24
+ }, [client]);
25
+
26
+ return { count, refresh };
27
+ }
@@ -0,0 +1,40 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const MOCK_URL = process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";
4
+
5
+ /**
6
+ * Same-origin proxy to the Cimplify mock so the browser never makes a
7
+ * cross-origin request in dev (no CORS preflights, no flaky `Origin`
8
+ * mismatches). The SDK's base URL stays empty/relative — requests go to
9
+ * `/api/v1/...` on the Next origin, then this rewrite forwards them to
10
+ * the mock at `127.0.0.1:8787`.
11
+ *
12
+ * In production, point `NEXT_PUBLIC_CIMPLIFY_API_URL` at your Cimplify
13
+ * host and the rewrite continues to work the same way (or remove it and
14
+ * pass the absolute URL through `<CimplifyProvider/>`).
15
+ */
16
+ const nextConfig: NextConfig = {
17
+ // Enable Next 16's `cacheComponents` mode so we can use `'use cache'` +
18
+ // `cacheTag` / `cacheLife` for SSR caching. Cached chrome streams first,
19
+ // dynamic Suspense boundaries fill in when ready.
20
+ cacheComponents: true,
21
+ async rewrites() {
22
+ return [
23
+ { source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },
24
+ { source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },
25
+ { source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },
26
+ { source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },
27
+ ];
28
+ },
29
+ images: {
30
+ remotePatterns: [
31
+ { protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },
32
+ { protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },
33
+ { protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },
34
+ { protocol: "https", hostname: "loremflickr.com", pathname: "/**" },
35
+ { protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },
36
+ ],
37
+ },
38
+ };
39
+
40
+ export default nextConfig;
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "__STOREFRONT_NAME__",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "scripts": {
6
+ "dev": "concurrently -k -n mock,next -c blue,magenta \"bunx @cimplify/sdk --seed grocery --quiet\" \"next dev\"",
7
+ "dev:storefront": "next dev",
8
+ "dev:mock": "bunx @cimplify/sdk --seed grocery",
9
+ "build": "next build",
10
+ "start": "next start",
11
+ "lint": "next lint"
12
+ },
13
+ "dependencies": {
14
+ "@cimplify/sdk": "^0.40.1",
15
+ "next": "^16.2.4",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@tailwindcss/postcss": "^4.2.4",
21
+ "@types/node": "^22.10.0",
22
+ "@types/react": "^19.0.0",
23
+ "@types/react-dom": "^19.0.0",
24
+ "concurrently": "^9.0.0",
25
+ "tailwindcss": "^4.2.4",
26
+ "typescript": "^5.9.3"
27
+ }
28
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": false,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,16 @@
1
+ # Cimplify API base URL.
2
+ # Dev: leave empty so the SDK uses the current origin (localhost:3000),
3
+ # which Next.js rewrites to the mock at 127.0.0.1:8787 (see next.config.ts).
4
+ # Production: set to your Cimplify host (e.g. https://api.cimplify.io).
5
+ NEXT_PUBLIC_CIMPLIFY_API_URL=
6
+
7
+ # Tenant public key. The mock accepts any value.
8
+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
9
+
10
+ # Business id used by the mock seed (restaurant: Mama's Kitchen).
11
+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_mamas_kitchen
12
+
13
+ # Canonical public site URL — used by sitemap.xml, robots.txt, llms.txt,
14
+ # and OpenGraph metadata. Set this on production deploys (Vercel auto-injects
15
+ # via VERCEL_URL but Next prefers an explicit value).
16
+ NEXT_PUBLIC_SITE_URL=https://example.com