@cimplify/sdk 0.39.1 → 0.40.1
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 +39 -5
- package/dist/mock/cli.mjs +30 -6
- package/dist/mock/library.js +30 -6
- package/dist/mock/library.mjs +30 -6
- package/dist/mock/msw.js +30 -6
- package/dist/mock/msw.mjs +30 -6
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/{storefront → storefront-bakery}/README.md +8 -1
- package/templates/{storefront → storefront-bakery}/package.json +1 -1
- package/templates/storefront-retail/.env.example +11 -0
- package/templates/storefront-retail/README.md +77 -0
- package/templates/storefront-retail/app/about/page.tsx +71 -0
- package/templates/storefront-retail/app/cart/page.tsx +9 -0
- package/templates/storefront-retail/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-retail/app/categories/[slug]/page.tsx +129 -0
- package/templates/storefront-retail/app/checkout/page.tsx +17 -0
- package/templates/storefront-retail/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-retail/app/collections/[slug]/page.tsx +129 -0
- package/templates/storefront-retail/app/faq/page.tsx +127 -0
- package/templates/storefront-retail/app/globals.css +47 -0
- package/templates/storefront-retail/app/layout.tsx +46 -0
- package/templates/storefront-retail/app/orders/[id]/page.tsx +24 -0
- package/templates/storefront-retail/app/page.tsx +239 -0
- package/templates/storefront-retail/app/privacy/page.tsx +101 -0
- package/templates/storefront-retail/app/shop/page.tsx +63 -0
- package/templates/storefront-retail/app/shop/shop-client.tsx +32 -0
- package/templates/storefront-retail/app/terms/page.tsx +117 -0
- package/templates/storefront-retail/components/brand-marquee.tsx +36 -0
- package/templates/storefront-retail/components/cart-pill.tsx +32 -0
- package/templates/storefront-retail/components/category-grid.tsx +28 -0
- package/templates/storefront-retail/components/category-tiles.tsx +104 -0
- package/templates/storefront-retail/components/collection-strip.tsx +45 -0
- package/templates/storefront-retail/components/feature-hero.tsx +83 -0
- package/templates/storefront-retail/components/footer.tsx +188 -0
- package/templates/storefront-retail/components/header.tsx +47 -0
- package/templates/storefront-retail/components/hero.tsx +28 -0
- package/templates/storefront-retail/components/nav-link.tsx +20 -0
- package/templates/storefront-retail/components/newsletter.tsx +52 -0
- package/templates/storefront-retail/components/product-modal.tsx +105 -0
- package/templates/storefront-retail/components/promo-banner.tsx +44 -0
- package/templates/storefront-retail/components/providers.tsx +31 -0
- package/templates/storefront-retail/components/section-heading.tsx +37 -0
- package/templates/storefront-retail/components/store-product-card.tsx +83 -0
- package/templates/storefront-retail/components/trade-in-cta.tsx +60 -0
- package/templates/storefront-retail/components/trust-bar.tsx +26 -0
- package/templates/storefront-retail/lib/cart.ts +27 -0
- package/templates/storefront-retail/next.config.ts +40 -0
- package/templates/storefront-retail/package.json +28 -0
- package/templates/storefront-retail/postcss.config.mjs +7 -0
- package/templates/storefront-retail/tsconfig.json +23 -0
- package/templates/storefront/lib/server-client.ts +0 -27
- /package/templates/{storefront → storefront-bakery}/.env.example +0 -0
- /package/templates/{storefront → storefront-bakery}/app/about/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/cart/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/categories/[slug]/listing-client.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/categories/[slug]/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/checkout/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/collections/[slug]/listing-client.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/collections/[slug]/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/faq/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/globals.css +0 -0
- /package/templates/{storefront → storefront-bakery}/app/layout.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/orders/[id]/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/privacy/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/shop/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/shop/shop-client.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/terms/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/cart-pill.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/category-grid.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/collection-strip.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/footer.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/header.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/hero.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/nav-link.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/product-modal.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/providers.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/components/store-product-card.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/lib/cart.ts +0 -0
- /package/templates/{storefront → storefront-bakery}/next.config.ts +0 -0
- /package/templates/{storefront → storefront-bakery}/postcss.config.mjs +0 -0
- /package/templates/{storefront → storefront-bakery}/tsconfig.json +0 -0
|
@@ -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,44 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
interface PromoBannerProps {
|
|
4
|
+
badge: string;
|
|
5
|
+
title: string;
|
|
6
|
+
body: string;
|
|
7
|
+
cta: { label: string; href: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Full-width promo banner — featured deal / launch / season campaign.
|
|
12
|
+
* Bold geometric background, mono-spaced detail labels.
|
|
13
|
+
*/
|
|
14
|
+
export function PromoBanner({ badge, title, body, cta }: PromoBannerProps) {
|
|
15
|
+
return (
|
|
16
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-6">
|
|
17
|
+
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-primary via-primary to-foreground text-primary-foreground p-8 sm:p-12 lg:p-16">
|
|
18
|
+
<div className="absolute -top-20 -right-20 w-72 h-72 rounded-full bg-background/10 blur-3xl pointer-events-none" />
|
|
19
|
+
<div className="absolute -bottom-32 -left-20 w-96 h-96 rounded-full bg-foreground/30 blur-3xl pointer-events-none" />
|
|
20
|
+
<div className="relative grid grid-cols-1 lg:grid-cols-[1fr_auto] gap-8 items-center">
|
|
21
|
+
<div className="max-w-2xl">
|
|
22
|
+
<span className="inline-flex items-center gap-2 mb-4 px-3 py-1 rounded-full bg-background/15 border border-background/25 text-[11px] font-mono uppercase tracking-[0.16em]">
|
|
23
|
+
<span className="w-1.5 h-1.5 rounded-full bg-background animate-pulse" />
|
|
24
|
+
{badge}
|
|
25
|
+
</span>
|
|
26
|
+
<h2 className="text-[clamp(1.75rem,3.5vw,2.75rem)] font-bold m-0 mb-3 -tracking-[0.025em] leading-[1.1]">
|
|
27
|
+
{title}
|
|
28
|
+
</h2>
|
|
29
|
+
<p className="text-base sm:text-lg opacity-90 leading-relaxed">{body}</p>
|
|
30
|
+
</div>
|
|
31
|
+
<Link
|
|
32
|
+
href={cta.href}
|
|
33
|
+
className="inline-flex items-center gap-2 px-5 py-3 rounded-md bg-background text-foreground font-semibold text-sm hover:bg-background/90 transition-colors whitespace-nowrap"
|
|
34
|
+
>
|
|
35
|
+
{cta.label}
|
|
36
|
+
<svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
37
|
+
<path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />
|
|
38
|
+
</svg>
|
|
39
|
+
</Link>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -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,37 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
interface SectionHeadingProps {
|
|
4
|
+
eyebrow: string;
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
link?: { label: string; href: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function SectionHeading({ eyebrow, title, description, link }: SectionHeadingProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex items-end justify-between gap-6 mb-8">
|
|
13
|
+
<div className="max-w-2xl">
|
|
14
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
|
|
15
|
+
{eyebrow}
|
|
16
|
+
</p>
|
|
17
|
+
<h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold m-0 -tracking-[0.025em]">
|
|
18
|
+
{title}
|
|
19
|
+
</h2>
|
|
20
|
+
{description && (
|
|
21
|
+
<p className="mt-2 text-muted-foreground">{description}</p>
|
|
22
|
+
)}
|
|
23
|
+
</div>
|
|
24
|
+
{link && (
|
|
25
|
+
<Link
|
|
26
|
+
href={link.href}
|
|
27
|
+
className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"
|
|
28
|
+
>
|
|
29
|
+
{link.label}
|
|
30
|
+
<svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
31
|
+
<path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />
|
|
32
|
+
</svg>
|
|
33
|
+
</Link>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -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,60 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export function TradeInCta() {
|
|
4
|
+
return (
|
|
5
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
|
|
6
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-foreground text-background rounded-3xl p-8 sm:p-12 lg:p-14 overflow-hidden relative">
|
|
7
|
+
<div className="absolute top-0 right-0 w-1/2 h-full opacity-[0.04] [background-image:linear-gradient(135deg,white_1px,transparent_1px)] [background-size:24px_24px] pointer-events-none" />
|
|
8
|
+
<div>
|
|
9
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary-foreground/70 mb-3">
|
|
10
|
+
Trade-in programme
|
|
11
|
+
</p>
|
|
12
|
+
<h2 className="text-[clamp(1.75rem,3.5vw,2.5rem)] font-bold m-0 mb-4 -tracking-[0.025em] leading-[1.1]">
|
|
13
|
+
Last year's phone, this year's discount.
|
|
14
|
+
</h2>
|
|
15
|
+
<p className="text-background/75 leading-relaxed mb-6">
|
|
16
|
+
Send us your iPhone, Galaxy, or MacBook. We'll quote within
|
|
17
|
+
48 hours and credit the value toward your next purchase. Devices
|
|
18
|
+
head to certified refurbishers across the continent — yours stays
|
|
19
|
+
in service for years more.
|
|
20
|
+
</p>
|
|
21
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
22
|
+
<Link
|
|
23
|
+
href="/about"
|
|
24
|
+
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
Get a quote
|
|
27
|
+
<svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
|
28
|
+
<path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />
|
|
29
|
+
</svg>
|
|
30
|
+
</Link>
|
|
31
|
+
<Link
|
|
32
|
+
href="/faq"
|
|
33
|
+
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 hover:bg-background/10 transition-colors text-sm font-medium"
|
|
34
|
+
>
|
|
35
|
+
How it works
|
|
36
|
+
</Link>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="grid grid-cols-3 gap-3">
|
|
40
|
+
{[
|
|
41
|
+
{ step: "01", title: "Tell us what you have", body: "Model + condition + photos. 2 minutes." },
|
|
42
|
+
{ step: "02", title: "Ship it free", body: "Drop at our store or book a free pickup." },
|
|
43
|
+
{ step: "03", title: "Get credited", body: "Within 48 hours, in store credit or Mobile Money." },
|
|
44
|
+
].map((s) => (
|
|
45
|
+
<div
|
|
46
|
+
key={s.step}
|
|
47
|
+
className="rounded-2xl p-4 sm:p-5 bg-background/5 border border-background/10"
|
|
48
|
+
>
|
|
49
|
+
<p className="text-[10px] font-mono text-primary tabular-nums mb-3">{s.step}</p>
|
|
50
|
+
<p className="text-[13px] sm:text-sm font-semibold mb-1.5 -tracking-[0.015em]">
|
|
51
|
+
{s.title}
|
|
52
|
+
</p>
|
|
53
|
+
<p className="text-[11px] sm:text-xs text-background/65 leading-snug">{s.body}</p>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</section>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface TrustBarProps {
|
|
2
|
+
items: { label: string; value: string; description: string; icon: React.ReactNode }[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function TrustBar({ items }: TrustBarProps) {
|
|
6
|
+
return (
|
|
7
|
+
<section className="border-y border-border bg-card">
|
|
8
|
+
<div className="max-w-7xl mx-auto px-6 sm:px-8 py-10 grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-8">
|
|
9
|
+
{items.map((it) => (
|
|
10
|
+
<div key={it.label} className="flex items-start gap-3">
|
|
11
|
+
<div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary shrink-0">
|
|
12
|
+
{it.icon}
|
|
13
|
+
</div>
|
|
14
|
+
<div className="min-w-0">
|
|
15
|
+
<p className="text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground mb-0.5">
|
|
16
|
+
{it.label}
|
|
17
|
+
</p>
|
|
18
|
+
<p className="text-base font-semibold -tracking-[0.015em] mb-0.5">{it.value}</p>
|
|
19
|
+
<p className="text-xs text-muted-foreground leading-snug">{it.description}</p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
))}
|
|
23
|
+
</div>
|
|
24
|
+
</section>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -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,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,27 +0,0 @@
|
|
|
1
|
-
import { createCimplifyClient, type CimplifyClient } from "@cimplify/sdk";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Server-side Cimplify client. Used by Server Components to fetch catalogue
|
|
5
|
-
* data during SSR/RSC for SEO-friendly product/category/collection pages.
|
|
6
|
-
*
|
|
7
|
-
* Cached per-request via React's cache mechanism — calling this multiple
|
|
8
|
-
* times within the same request returns the same client instance.
|
|
9
|
-
*
|
|
10
|
-
* Base-URL resolution:
|
|
11
|
-
* 1) NEXT_PUBLIC_CIMPLIFY_API_URL (matches the client-side default)
|
|
12
|
-
* 2) http://127.0.0.1:8787 — the local mock during dev
|
|
13
|
-
*/
|
|
14
|
-
let cached: CimplifyClient | undefined;
|
|
15
|
-
|
|
16
|
-
export function getServerClient(): CimplifyClient {
|
|
17
|
-
if (cached) return cached;
|
|
18
|
-
const baseUrl =
|
|
19
|
-
process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() || "http://127.0.0.1:8787";
|
|
20
|
-
const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";
|
|
21
|
-
cached = createCimplifyClient({
|
|
22
|
-
baseUrl,
|
|
23
|
-
publicKey,
|
|
24
|
-
suppressPublicKeyWarning: true,
|
|
25
|
-
});
|
|
26
|
-
return cached;
|
|
27
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/{storefront → storefront-bakery}/app/categories/[slug]/listing-client.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/{storefront → storefront-bakery}/app/collections/[slug]/listing-client.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|