@cimplify/sdk 0.38.2 → 0.38.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dispatcher.mjs +1 -1
- package/dist/mock/cli.mjs +956 -436
- package/dist/mock/library.d.mts +2 -2
- package/dist/mock/library.d.ts +2 -2
- package/dist/mock/library.js +956 -436
- package/dist/mock/library.mjs +956 -436
- package/dist/mock/msw.d.mts +1 -1
- package/dist/mock/msw.d.ts +1 -1
- package/dist/mock/msw.js +956 -436
- package/dist/mock/msw.mjs +956 -436
- package/dist/{server-B8TeE2OD.d.mts → server-BNy6Ri-4.d.mts} +18 -0
- package/dist/{server-B8TeE2OD.d.ts → server-BNy6Ri-4.d.ts} +18 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/storefront/.env.example +11 -2
- package/templates/storefront/README.md +35 -18
- package/templates/storefront/app/cart/page.tsx +9 -0
- package/templates/storefront/app/categories/[slug]/page.tsx +80 -0
- package/templates/storefront/app/checkout/page.tsx +17 -0
- package/templates/storefront/app/collections/[slug]/page.tsx +80 -0
- package/templates/storefront/app/globals.css +47 -0
- package/templates/storefront/app/layout.tsx +45 -0
- package/templates/storefront/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront/app/page.tsx +65 -0
- package/templates/storefront/app/shop/page.tsx +46 -0
- package/templates/storefront/components/category-grid.tsx +28 -0
- package/templates/storefront/components/collection-strip.tsx +45 -0
- package/templates/storefront/components/footer.tsx +19 -0
- package/templates/storefront/components/header.tsx +41 -0
- package/templates/storefront/components/hero.tsx +25 -0
- package/templates/storefront/components/product-modal.tsx +105 -0
- package/templates/storefront/components/providers.tsx +31 -0
- package/templates/storefront/components/store-product-card.tsx +48 -0
- package/templates/storefront/lib/cart.ts +27 -0
- package/templates/storefront/next.config.ts +36 -0
- package/templates/storefront/package.json +13 -14
- package/templates/storefront/postcss.config.mjs +7 -0
- package/templates/storefront/tsconfig.json +15 -11
- package/templates/storefront/index.html +0 -12
- package/templates/storefront/src/App.tsx +0 -118
- package/templates/storefront/src/main.tsx +0 -28
- package/templates/storefront/src/styles.css +0 -65
- package/templates/storefront/vite.config.ts +0 -8
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { useCimplifyClient } from "@cimplify/sdk/react";
|
|
5
|
+
import type { Category, Collection, Product } from "@cimplify/sdk";
|
|
6
|
+
import { Hero } from "@/components/hero";
|
|
7
|
+
import { CollectionStrip } from "@/components/collection-strip";
|
|
8
|
+
import { CategoryGrid } from "@/components/category-grid";
|
|
9
|
+
|
|
10
|
+
interface CollectionWithProducts {
|
|
11
|
+
collection: Collection;
|
|
12
|
+
products: Product[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function HomePage() {
|
|
16
|
+
const { client } = useCimplifyClient();
|
|
17
|
+
const [collections, setCollections] = useState<CollectionWithProducts[]>([]);
|
|
18
|
+
const [categories, setCategories] = useState<Category[]>([]);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let cancelled = false;
|
|
22
|
+
void (async () => {
|
|
23
|
+
const [colRes, catRes] = await Promise.all([
|
|
24
|
+
client.catalogue.getCollections(),
|
|
25
|
+
client.catalogue.getCategories(),
|
|
26
|
+
]);
|
|
27
|
+
if (cancelled) return;
|
|
28
|
+
if (catRes.ok) setCategories(catRes.value);
|
|
29
|
+
if (colRes.ok) {
|
|
30
|
+
const withProducts = await Promise.all(
|
|
31
|
+
colRes.value.map(async (col) => {
|
|
32
|
+
const r = await client.catalogue.getCollectionProducts(col.id);
|
|
33
|
+
const items = r.ok
|
|
34
|
+
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
35
|
+
: [];
|
|
36
|
+
return { collection: col, products: items };
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
if (!cancelled) setCollections(withProducts.filter((x) => x.products.length > 0));
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
return () => {
|
|
43
|
+
cancelled = true;
|
|
44
|
+
};
|
|
45
|
+
}, [client]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Hero
|
|
50
|
+
badge="BAKERY · PÂTISSERIE · COFFEE"
|
|
51
|
+
title="Fresh from the oven, every day."
|
|
52
|
+
subtitle="Sourdough, croissants, cakes, and more — handcrafted in East Legon, delivered across Accra."
|
|
53
|
+
/>
|
|
54
|
+
{collections.map(({ collection, products }) => (
|
|
55
|
+
<CollectionStrip
|
|
56
|
+
key={collection.id}
|
|
57
|
+
collection={collection}
|
|
58
|
+
products={products}
|
|
59
|
+
collectionHref={`/collections/${collection.slug}`}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
<CategoryGrid categories={categories} />
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { CataloguePage, useCimplifyClient } from "@cimplify/sdk/react";
|
|
5
|
+
import type { Category, Product } from "@cimplify/sdk";
|
|
6
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
7
|
+
|
|
8
|
+
/** Full catalogue page — every product, with the SDK's filterable layout. */
|
|
9
|
+
export default function ShopPage() {
|
|
10
|
+
const { client } = useCimplifyClient();
|
|
11
|
+
const [products, setProducts] = useState<Product[]>();
|
|
12
|
+
const [categories, setCategories] = useState<Category[]>();
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let cancelled = false;
|
|
17
|
+
void (async () => {
|
|
18
|
+
try {
|
|
19
|
+
const [p, c] = await Promise.all([
|
|
20
|
+
client.catalogue.getProducts({ limit: 50 }),
|
|
21
|
+
client.catalogue.getCategories(),
|
|
22
|
+
]);
|
|
23
|
+
if (cancelled) return;
|
|
24
|
+
if (p.ok) setProducts(p.value.items);
|
|
25
|
+
if (c.ok) setCategories(c.value);
|
|
26
|
+
} finally {
|
|
27
|
+
if (!cancelled) setLoading(false);
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
30
|
+
return () => {
|
|
31
|
+
cancelled = true;
|
|
32
|
+
};
|
|
33
|
+
}, [client]);
|
|
34
|
+
|
|
35
|
+
if (loading) {
|
|
36
|
+
return <p className="text-center py-20 text-muted-foreground">Loading shop…</p>;
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
<CataloguePage
|
|
40
|
+
title="The Menu"
|
|
41
|
+
products={products}
|
|
42
|
+
categories={categories}
|
|
43
|
+
renderCard={(p) => <StoreProductCard product={p} />}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";
|
|
5
|
+
import type { Category } from "@cimplify/sdk";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Homepage category tiles — defers to the SDK's <CategoryGrid/> for layout
|
|
9
|
+
* and accessibility, and routes selections to /categories/:slug.
|
|
10
|
+
*/
|
|
11
|
+
export function CategoryGrid({ categories }: { categories?: Category[] }) {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
return (
|
|
14
|
+
<section className="max-w-7xl mx-auto px-8 pt-14">
|
|
15
|
+
<h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>
|
|
16
|
+
<SdkCategoryGrid
|
|
17
|
+
categories={categories}
|
|
18
|
+
onSelect={(c) => router.push(`/categories/${c.slug}`)}
|
|
19
|
+
classNames={{
|
|
20
|
+
item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",
|
|
21
|
+
name: "font-serif text-lg font-semibold text-foreground",
|
|
22
|
+
description: "text-xs text-muted-foreground",
|
|
23
|
+
count: "text-xs text-muted-foreground mt-1",
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { Collection, Product } from "@cimplify/sdk";
|
|
3
|
+
import { StoreProductCard } from "./store-product-card";
|
|
4
|
+
|
|
5
|
+
interface CollectionStripProps {
|
|
6
|
+
collection: Collection;
|
|
7
|
+
products: Product[];
|
|
8
|
+
collectionHref?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Horizontal strip of products under a collection title. Cards come from the
|
|
13
|
+
* SDK so every variant (Food, Bundle, Composite, Service, …) renders
|
|
14
|
+
* correctly; clicks open the shared URL-driven product modal.
|
|
15
|
+
*/
|
|
16
|
+
export function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {
|
|
17
|
+
if (products.length === 0) return null;
|
|
18
|
+
return (
|
|
19
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
20
|
+
<header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">
|
|
21
|
+
<h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>
|
|
22
|
+
{collectionHref && (
|
|
23
|
+
<Link
|
|
24
|
+
href={collectionHref}
|
|
25
|
+
className="text-[13px] font-semibold text-primary hover:underline"
|
|
26
|
+
>
|
|
27
|
+
See all →
|
|
28
|
+
</Link>
|
|
29
|
+
)}
|
|
30
|
+
{collection.description && (
|
|
31
|
+
<p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">
|
|
32
|
+
{collection.description}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
</header>
|
|
36
|
+
<div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">
|
|
37
|
+
{products.slice(0, 8).map((p) => (
|
|
38
|
+
<div key={p.id} className="snap-start">
|
|
39
|
+
<StoreProductCard product={p} aspectRatio="square" />
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
import { useCartCount } from "@/lib/cart";
|
|
6
|
+
|
|
7
|
+
const TAGLINE = "ACCRA · SINCE 2018";
|
|
8
|
+
|
|
9
|
+
function navLinkClass(active: boolean) {
|
|
10
|
+
return [
|
|
11
|
+
"text-[13px] font-medium tracking-wide transition-colors",
|
|
12
|
+
active ? "text-primary" : "text-muted-foreground hover:text-foreground",
|
|
13
|
+
].join(" ");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Header() {
|
|
17
|
+
const pathname = usePathname();
|
|
18
|
+
const { count } = useCartCount();
|
|
19
|
+
return (
|
|
20
|
+
<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">
|
|
21
|
+
<Link href="/" className="flex items-baseline gap-2">
|
|
22
|
+
<span className="font-serif text-[22px] font-semibold -tracking-[0.02em]">
|
|
23
|
+
Akua's Bakery
|
|
24
|
+
</span>
|
|
25
|
+
<span className="text-[11px] font-medium uppercase tracking-[0.12em] text-muted-foreground">
|
|
26
|
+
{TAGLINE}
|
|
27
|
+
</span>
|
|
28
|
+
</Link>
|
|
29
|
+
<nav className="flex items-center gap-6">
|
|
30
|
+
<Link href="/" className={navLinkClass(pathname === "/")}>Home</Link>
|
|
31
|
+
<Link href="/shop" className={navLinkClass(pathname === "/shop")}>Shop</Link>
|
|
32
|
+
<Link
|
|
33
|
+
href="/cart"
|
|
34
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105"
|
|
35
|
+
>
|
|
36
|
+
Cart · {count}
|
|
37
|
+
</Link>
|
|
38
|
+
</nav>
|
|
39
|
+
</header>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -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,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,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
|
4
|
+
import { ProductCard } from "@cimplify/sdk/react";
|
|
5
|
+
import type { ProductCardProps } from "@cimplify/sdk/react";
|
|
6
|
+
import type { Product } from "@cimplify/sdk";
|
|
7
|
+
|
|
8
|
+
type Props = Omit<ProductCardProps, "displayMode" | "renderLink"> & {
|
|
9
|
+
product: Product;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wraps the SDK `<ProductCard>` so a click sets `?product=<slug>` instead of
|
|
14
|
+
* opening the SDK's own modal. All variant card styling (Food, Bundle,
|
|
15
|
+
* Composite, …) is preserved — we only intercept navigation so a single
|
|
16
|
+
* deep-linkable `<ProductModal>` instance handles every product surface.
|
|
17
|
+
*/
|
|
18
|
+
export function StoreProductCard(props: Props) {
|
|
19
|
+
const router = useRouter();
|
|
20
|
+
const pathname = usePathname();
|
|
21
|
+
const searchParams = useSearchParams();
|
|
22
|
+
|
|
23
|
+
const open = (slug: string) => {
|
|
24
|
+
const next = new URLSearchParams(searchParams?.toString() ?? "");
|
|
25
|
+
next.set("product", slug);
|
|
26
|
+
router.push(`${pathname}?${next.toString()}`, { scroll: false });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<ProductCard
|
|
31
|
+
{...props}
|
|
32
|
+
displayMode="page"
|
|
33
|
+
href={`?product=${props.product.slug || props.product.id}`}
|
|
34
|
+
renderLink={({ href, className, children }) => (
|
|
35
|
+
<a
|
|
36
|
+
href={href}
|
|
37
|
+
className={className}
|
|
38
|
+
onClick={(e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
open(props.product.slug || props.product.id);
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
{children}
|
|
44
|
+
</a>
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -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,36 @@
|
|
|
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
|
+
async rewrites() {
|
|
18
|
+
return [
|
|
19
|
+
{ source: "/api/v1/:path*", destination: `${MOCK_URL}/api/v1/:path*` },
|
|
20
|
+
{ source: "/img/:path*", destination: `${MOCK_URL}/img/:path*` },
|
|
21
|
+
{ source: "/elements/:path*", destination: `${MOCK_URL}/elements/:path*` },
|
|
22
|
+
{ source: "/_mock/:path*", destination: `${MOCK_URL}/_mock/:path*` },
|
|
23
|
+
];
|
|
24
|
+
},
|
|
25
|
+
images: {
|
|
26
|
+
remotePatterns: [
|
|
27
|
+
{ protocol: "http", hostname: "127.0.0.1", port: "8787", pathname: "/img/**" },
|
|
28
|
+
{ protocol: "http", hostname: "localhost", port: "8787", pathname: "/img/**" },
|
|
29
|
+
{ protocol: "http", hostname: "localhost", port: "3000", pathname: "/img/**" },
|
|
30
|
+
{ protocol: "https", hostname: "loremflickr.com", pathname: "/**" },
|
|
31
|
+
{ protocol: "https", hostname: "images.unsplash.com", pathname: "/**" },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default nextConfig;
|
|
@@ -2,28 +2,27 @@
|
|
|
2
2
|
"name": "__STOREFRONT_NAME__",
|
|
3
3
|
"private": true,
|
|
4
4
|
"version": "0.0.0",
|
|
5
|
-
"type": "module",
|
|
6
5
|
"scripts": {
|
|
7
|
-
"dev": "concurrently -k -n mock,
|
|
8
|
-
"dev:storefront": "
|
|
6
|
+
"dev": "concurrently -k -n mock,next -c blue,magenta \"bunx @cimplify/sdk --quiet\" \"next dev\"",
|
|
7
|
+
"dev:storefront": "next dev",
|
|
9
8
|
"dev:mock": "bunx @cimplify/sdk",
|
|
10
|
-
"build": "
|
|
11
|
-
"
|
|
9
|
+
"build": "next build",
|
|
10
|
+
"start": "next start",
|
|
11
|
+
"lint": "next lint"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@cimplify/sdk": "^0.38.1",
|
|
15
|
-
"
|
|
16
|
-
"react
|
|
17
|
-
"react-
|
|
15
|
+
"next": "^15.1.0",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
|
-
"@tailwindcss/
|
|
21
|
-
"@types/
|
|
22
|
-
"@types/react
|
|
23
|
-
"@
|
|
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
24
|
"concurrently": "^9.0.0",
|
|
25
25
|
"tailwindcss": "^4.2.4",
|
|
26
|
-
"typescript": "^5.9.3"
|
|
27
|
-
"vite": "^7.0.0"
|
|
26
|
+
"typescript": "^5.9.3"
|
|
28
27
|
}
|
|
29
28
|
}
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"lib": ["
|
|
5
|
-
"
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": false,
|
|
6
6
|
"skipLibCheck": true,
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
9
12
|
"resolveJsonModule": true,
|
|
10
13
|
"isolatedModules": true,
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
17
20
|
},
|
|
18
|
-
"include": ["
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
19
23
|
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>__STOREFRONT_NAME__</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<div id="root"></div>
|
|
10
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|