@cimplify/sdk 0.39.0 → 0.40.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 (72) hide show
  1. package/dist/dispatcher.mjs +39 -5
  2. package/dist/styles.css +1 -1
  3. package/package.json +1 -1
  4. package/templates/{storefront → storefront-bakery}/README.md +8 -1
  5. package/templates/storefront-bakery/app/about/page.tsx +61 -0
  6. package/templates/storefront-bakery/app/categories/[slug]/listing-client.tsx +19 -0
  7. package/templates/{storefront → storefront-bakery}/app/categories/[slug]/page.tsx +42 -10
  8. package/templates/storefront-bakery/app/collections/[slug]/listing-client.tsx +20 -0
  9. package/templates/{storefront → storefront-bakery}/app/collections/[slug]/page.tsx +42 -10
  10. package/templates/storefront-bakery/app/faq/page.tsx +122 -0
  11. package/templates/{storefront → storefront-bakery}/app/page.tsx +39 -6
  12. package/templates/storefront-bakery/app/privacy/page.tsx +88 -0
  13. package/templates/storefront-bakery/app/terms/page.tsx +102 -0
  14. package/templates/storefront-bakery/components/cart-pill.tsx +32 -0
  15. package/templates/storefront-bakery/components/footer.tsx +190 -0
  16. package/templates/storefront-bakery/components/header.tsx +51 -0
  17. package/templates/storefront-bakery/components/nav-link.tsx +20 -0
  18. package/templates/{storefront → storefront-bakery}/components/store-product-card.tsx +9 -25
  19. package/templates/{storefront → storefront-bakery}/next.config.ts +3 -4
  20. package/templates/{storefront → storefront-bakery}/package.json +1 -1
  21. package/templates/storefront-retail/.env.example +11 -0
  22. package/templates/storefront-retail/README.md +77 -0
  23. package/templates/storefront-retail/app/about/page.tsx +71 -0
  24. package/templates/storefront-retail/app/cart/page.tsx +9 -0
  25. package/templates/storefront-retail/app/categories/[slug]/listing-client.tsx +19 -0
  26. package/templates/storefront-retail/app/categories/[slug]/page.tsx +117 -0
  27. package/templates/storefront-retail/app/checkout/page.tsx +17 -0
  28. package/templates/storefront-retail/app/collections/[slug]/listing-client.tsx +20 -0
  29. package/templates/storefront-retail/app/collections/[slug]/page.tsx +117 -0
  30. package/templates/storefront-retail/app/faq/page.tsx +127 -0
  31. package/templates/storefront-retail/app/globals.css +47 -0
  32. package/templates/storefront-retail/app/layout.tsx +46 -0
  33. package/templates/storefront-retail/app/orders/[id]/page.tsx +24 -0
  34. package/templates/storefront-retail/app/page.tsx +97 -0
  35. package/templates/storefront-retail/app/privacy/page.tsx +101 -0
  36. package/templates/storefront-retail/app/shop/page.tsx +30 -0
  37. package/templates/storefront-retail/app/shop/shop-client.tsx +27 -0
  38. package/templates/storefront-retail/app/terms/page.tsx +117 -0
  39. package/templates/storefront-retail/components/cart-pill.tsx +32 -0
  40. package/templates/storefront-retail/components/category-grid.tsx +28 -0
  41. package/templates/storefront-retail/components/collection-strip.tsx +45 -0
  42. package/templates/storefront-retail/components/footer.tsx +188 -0
  43. package/templates/storefront-retail/components/header.tsx +47 -0
  44. package/templates/storefront-retail/components/hero.tsx +28 -0
  45. package/templates/storefront-retail/components/nav-link.tsx +20 -0
  46. package/templates/storefront-retail/components/product-modal.tsx +105 -0
  47. package/templates/storefront-retail/components/providers.tsx +31 -0
  48. package/templates/storefront-retail/components/store-product-card.tsx +83 -0
  49. package/templates/storefront-retail/lib/cart.ts +27 -0
  50. package/templates/storefront-retail/next.config.ts +40 -0
  51. package/templates/storefront-retail/package.json +28 -0
  52. package/templates/storefront-retail/postcss.config.mjs +7 -0
  53. package/templates/storefront-retail/tsconfig.json +23 -0
  54. package/templates/storefront/components/footer.tsx +0 -19
  55. package/templates/storefront/components/header.tsx +0 -41
  56. package/templates/storefront/lib/server-client.ts +0 -27
  57. /package/templates/{storefront → storefront-bakery}/.env.example +0 -0
  58. /package/templates/{storefront → storefront-bakery}/app/cart/page.tsx +0 -0
  59. /package/templates/{storefront → storefront-bakery}/app/checkout/page.tsx +0 -0
  60. /package/templates/{storefront → storefront-bakery}/app/globals.css +0 -0
  61. /package/templates/{storefront → storefront-bakery}/app/layout.tsx +0 -0
  62. /package/templates/{storefront → storefront-bakery}/app/orders/[id]/page.tsx +0 -0
  63. /package/templates/{storefront → storefront-bakery}/app/shop/page.tsx +0 -0
  64. /package/templates/{storefront → storefront-bakery}/app/shop/shop-client.tsx +0 -0
  65. /package/templates/{storefront → storefront-bakery}/components/category-grid.tsx +0 -0
  66. /package/templates/{storefront → storefront-bakery}/components/collection-strip.tsx +0 -0
  67. /package/templates/{storefront → storefront-bakery}/components/hero.tsx +0 -0
  68. /package/templates/{storefront → storefront-bakery}/components/product-modal.tsx +0 -0
  69. /package/templates/{storefront → storefront-bakery}/components/providers.tsx +0 -0
  70. /package/templates/{storefront → storefront-bakery}/lib/cart.ts +0 -0
  71. /package/templates/{storefront → storefront-bakery}/postcss.config.mjs +0 -0
  72. /package/templates/{storefront → storefront-bakery}/tsconfig.json +0 -0
@@ -0,0 +1,188 @@
1
+ import Link from "next/link";
2
+
3
+ const SOCIALS: { label: string; href: string; svg: React.ReactNode }[] = [
4
+ {
5
+ label: "Instagram",
6
+ href: "https://instagram.com/currentselectronics",
7
+ svg: (
8
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">
9
+ <rect x="3" y="3" width="18" height="18" rx="5" />
10
+ <circle cx="12" cy="12" r="4" />
11
+ <circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />
12
+ </svg>
13
+ ),
14
+ },
15
+ {
16
+ label: "X",
17
+ href: "https://x.com/currentselectronics",
18
+ svg: (
19
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
20
+ <path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />
21
+ </svg>
22
+ ),
23
+ },
24
+ {
25
+ label: "YouTube",
26
+ href: "https://youtube.com/@currentselectronics",
27
+ svg: (
28
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
29
+ <path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />
30
+ </svg>
31
+ ),
32
+ },
33
+ {
34
+ label: "WhatsApp",
35
+ href: "https://wa.me/233244000000",
36
+ svg: (
37
+ <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
38
+ <path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />
39
+ </svg>
40
+ ),
41
+ },
42
+ ];
43
+
44
+ const SITEMAP: { title: string; links: { label: string; href: string }[] }[] = [
45
+ {
46
+ title: "Shop",
47
+ links: [
48
+ { label: "All products", href: "/shop" },
49
+ { label: "Laptops", href: "/categories/laptops" },
50
+ { label: "Phones", href: "/categories/phones" },
51
+ { label: "Audio", href: "/categories/audio" },
52
+ { label: "Accessories", href: "/categories/accessories" },
53
+ { label: "Deals", href: "/categories/deals" },
54
+ ],
55
+ },
56
+ {
57
+ title: "Support",
58
+ links: [
59
+ { label: "Contact", href: "mailto:hello@currentselectronics.test" },
60
+ { label: "Warranty", href: "/faq#warranty--repairs" },
61
+ { label: "Returns", href: "/faq#payment--returns" },
62
+ { label: "Shipping", href: "/faq#ordering--delivery" },
63
+ ],
64
+ },
65
+ {
66
+ title: "Company",
67
+ links: [
68
+ { label: "About", href: "/about" },
69
+ { label: "Trade-in", href: "/about" },
70
+ { label: "Business sales", href: "mailto:business@currentselectronics.test" },
71
+ { label: "Careers", href: "/about" },
72
+ ],
73
+ },
74
+ {
75
+ title: "Legal",
76
+ links: [
77
+ { label: "Terms of Service", href: "/terms" },
78
+ { label: "Privacy Policy", href: "/privacy" },
79
+ ],
80
+ },
81
+ ];
82
+
83
+ export async function Footer() {
84
+ "use cache";
85
+ const year = new Date().getFullYear();
86
+ return (
87
+ <footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">
88
+ <div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">
89
+ <div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">
90
+ <div>
91
+ <div className="flex items-center gap-2.5 mb-4">
92
+ <span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">
93
+ C
94
+ </span>
95
+ <span className="text-background text-lg font-bold -tracking-[0.025em]">
96
+ Currents Electronics
97
+ </span>
98
+ </div>
99
+ <p className="leading-relaxed mb-4 max-w-sm">
100
+ Authorised dealer for Apple, Samsung, Sony, Bose. Same-day
101
+ delivery in Accra; nationwide via partner courier. Two-year
102
+ warranty on every product.
103
+ </p>
104
+ <address className="not-italic space-y-1">
105
+ <p className="m-0">Atomic Junction, East Legon, Accra</p>
106
+ <p className="m-0">
107
+ <a href="tel:+233244000000" className="hover:text-background transition-colors">
108
+ +233 244 000 000
109
+ </a>
110
+ </p>
111
+ <p className="m-0">
112
+ <a
113
+ href="mailto:hello@currentselectronics.test"
114
+ className="hover:text-background transition-colors"
115
+ >
116
+ hello@currentselectronics.test
117
+ </a>
118
+ </p>
119
+ <p className="m-0 text-xs">Mon–Sat · 9am–8pm</p>
120
+ </address>
121
+ <div className="flex items-center gap-3 mt-5">
122
+ {SOCIALS.map((s) => (
123
+ <a
124
+ key={s.label}
125
+ href={s.href}
126
+ aria-label={s.label}
127
+ target="_blank"
128
+ rel="noopener noreferrer"
129
+ className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"
130
+ >
131
+ {s.svg}
132
+ </a>
133
+ ))}
134
+ </div>
135
+ </div>
136
+ {SITEMAP.map((section) => (
137
+ <nav key={section.title} aria-labelledby={`footer-${section.title}`}>
138
+ <p
139
+ id={`footer-${section.title}`}
140
+ className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"
141
+ >
142
+ {section.title}
143
+ </p>
144
+ <ul className="space-y-2 m-0 p-0 list-none">
145
+ {section.links.map((link) => (
146
+ <li key={link.label}>
147
+ <Link
148
+ href={link.href}
149
+ className="hover:text-background transition-colors"
150
+ >
151
+ {link.label}
152
+ </Link>
153
+ </li>
154
+ ))}
155
+ </ul>
156
+ </nav>
157
+ ))}
158
+ </div>
159
+
160
+ <div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">
161
+ <p className="m-0">© {year} Currents Electronics. All rights reserved.</p>
162
+ <p className="m-0 inline-flex items-center gap-1.5">
163
+ <span className="opacity-70">Powered by</span>
164
+ <a
165
+ href="https://app.cimplify.io"
166
+ target="_blank"
167
+ rel="noopener noreferrer"
168
+ aria-label="Cimplify"
169
+ className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"
170
+ >
171
+ <span className="font-semibold tracking-tight">Cimplify</span>
172
+ <svg
173
+ viewBox="0 0 12 12"
174
+ aria-hidden
175
+ className="w-3 h-3 opacity-70"
176
+ fill="none"
177
+ stroke="currentColor"
178
+ strokeWidth="1.5"
179
+ >
180
+ <path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />
181
+ </svg>
182
+ </a>
183
+ </p>
184
+ </div>
185
+ </div>
186
+ </footer>
187
+ );
188
+ }
@@ -0,0 +1,47 @@
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
+
6
+ /**
7
+ * Server-rendered header chrome. Brand mark + nav layout streams from the
8
+ * cache; the active-link styling and live cart count are dynamic islands
9
+ * mounted in their own Suspense boundaries so the chrome never blocks.
10
+ */
11
+ export function Header() {
12
+ return (
13
+ <header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">
14
+ <Link href="/" className="flex items-center gap-2.5 group">
15
+ <span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">
16
+ C
17
+ </span>
18
+ <span className="text-[18px] font-bold -tracking-[0.025em]">Currents</span>
19
+ <span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">
20
+ ELECTRONICS
21
+ </span>
22
+ </Link>
23
+ <nav className="flex items-center gap-5 sm:gap-6">
24
+ <Suspense fallback={<NavLinkFallback>Shop</NavLinkFallback>}>
25
+ <NavLink href="/shop">Shop</NavLink>
26
+ </Suspense>
27
+ <Suspense fallback={<NavLinkFallback>Deals</NavLinkFallback>}>
28
+ <NavLink href="/categories/deals">Deals</NavLink>
29
+ </Suspense>
30
+ <Suspense fallback={<NavLinkFallback>Support</NavLinkFallback>}>
31
+ <NavLink href="/faq">Support</NavLink>
32
+ </Suspense>
33
+ <Suspense fallback={<CartPillSkeleton />}>
34
+ <CartPill />
35
+ </Suspense>
36
+ </nav>
37
+ </header>
38
+ );
39
+ }
40
+
41
+ function NavLinkFallback({ children }: { children: React.ReactNode }) {
42
+ return (
43
+ <span className="text-[13px] font-medium tracking-wide text-muted-foreground">
44
+ {children}
45
+ </span>
46
+ );
47
+ }
@@ -0,0 +1,28 @@
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="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">
10
+ <div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />
11
+ <div className="relative max-w-3xl mx-auto">
12
+ {badge && (
13
+ <span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">
14
+ {badge}
15
+ </span>
16
+ )}
17
+ <h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">
18
+ {title}
19
+ </h1>
20
+ {subtitle && (
21
+ <p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">
22
+ {subtitle}
23
+ </p>
24
+ )}
25
+ </div>
26
+ </section>
27
+ );
28
+ }
@@ -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,105 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import Image from "next/image";
5
+ import { useRouter, useSearchParams, usePathname } from "next/navigation";
6
+ import { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";
7
+
8
+ /**
9
+ * URL-driven product modal. Reads `?product=<slug>` and renders the SDK's
10
+ * `<ProductSheet/>` — vertical layout with image-on-top, then header, then
11
+ * the variant/add-on/composite/bundle customizer. Closing the modal clears
12
+ * the search param. Deep-linkable and survives reloads.
13
+ */
14
+ export function ProductModal() {
15
+ const router = useRouter();
16
+ const pathname = usePathname();
17
+ const searchParams = useSearchParams();
18
+ const slug = searchParams?.get("product") ?? null;
19
+
20
+ const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });
21
+ const { addToCart } = useCart();
22
+
23
+ useEffect(() => {
24
+ if (!slug) return;
25
+ const original = document.body.style.overflow;
26
+ document.body.style.overflow = "hidden";
27
+ return () => {
28
+ document.body.style.overflow = original;
29
+ };
30
+ }, [slug]);
31
+
32
+ useEffect(() => {
33
+ if (!slug) return;
34
+ const onKey = (e: KeyboardEvent) => {
35
+ if (e.key === "Escape") close();
36
+ };
37
+ window.addEventListener("keydown", onKey);
38
+ return () => window.removeEventListener("keydown", onKey);
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ }, [slug]);
41
+
42
+ if (!slug) return null;
43
+
44
+ function close() {
45
+ const next = new URLSearchParams(searchParams?.toString() ?? "");
46
+ next.delete("product");
47
+ const qs = next.toString();
48
+ router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
49
+ }
50
+
51
+ return (
52
+ <div
53
+ role="dialog"
54
+ aria-modal="true"
55
+ onClick={close}
56
+ className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"
57
+ >
58
+ <div
59
+ onClick={(e) => e.stopPropagation()}
60
+ className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"
61
+ >
62
+ <button
63
+ onClick={close}
64
+ aria-label="Close product details"
65
+ className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"
66
+ >
67
+ ✕
68
+ </button>
69
+ {product ? (
70
+ <ProductSheet
71
+ product={product}
72
+ onClose={close}
73
+ onAddToCart={async (p, qty, options) => {
74
+ await addToCart(p.id, qty, options);
75
+ close();
76
+ router.push("/cart");
77
+ }}
78
+ renderImage={({ src, alt, className }) => (
79
+ <Image
80
+ src={src}
81
+ alt={alt}
82
+ width={1200}
83
+ height={900}
84
+ className={className}
85
+ style={{ width: "100%", height: "auto", objectFit: "cover" }}
86
+ priority
87
+ />
88
+ )}
89
+ classNames={{
90
+ root: "p-6 sm:p-8 gap-4",
91
+ image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",
92
+ header: "flex items-baseline justify-between gap-4",
93
+ name: "font-serif text-2xl font-semibold m-0",
94
+ price: "text-lg font-semibold text-primary",
95
+ description: "text-sm text-muted-foreground leading-relaxed",
96
+ customizer: "pt-2",
97
+ }}
98
+ />
99
+ ) : (
100
+ <div className="p-8 text-center text-muted-foreground">Loading…</div>
101
+ )}
102
+ </div>
103
+ </div>
104
+ );
105
+ }
@@ -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,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 retail --quiet\" \"next dev\"",
7
+ "dev:storefront": "next dev",
8
+ "dev:mock": "bunx @cimplify/sdk --seed retail",
9
+ "build": "next build",
10
+ "start": "next start",
11
+ "lint": "next lint"
12
+ },
13
+ "dependencies": {
14
+ "@cimplify/sdk": "^0.39.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
+ }
@@ -1,19 +0,0 @@
1
- export function Footer() {
2
- return (
3
- <footer className="mt-12 px-8 py-10 text-center text-xs text-muted-foreground border-t border-border bg-card">
4
- <p className="m-0 font-serif text-base text-foreground">Akua's Bakery</p>
5
- <p className="mt-1 mb-3">12 Independence Ave, Accra · +233 244 000 000</p>
6
- <p className="m-0">
7
- Built with{" "}
8
- <a
9
- href="https://www.npmjs.com/package/@cimplify/sdk"
10
- target="_blank"
11
- rel="noopener noreferrer"
12
- className="font-semibold text-primary hover:underline"
13
- >
14
- @cimplify/sdk
15
- </a>
16
- </p>
17
- </footer>
18
- );
19
- }