@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.
- package/dist/dispatcher.mjs +3 -2
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/storefront-bakery/.env.example +5 -0
- package/templates/storefront-bakery/AGENTS.md +57 -0
- package/templates/storefront-bakery/app/about/page.tsx +21 -44
- package/templates/storefront-bakery/app/categories/[slug]/page.tsx +2 -1
- package/templates/storefront-bakery/app/collections/[slug]/page.tsx +2 -1
- package/templates/storefront-bakery/app/error.tsx +60 -0
- package/templates/storefront-bakery/app/faq/page.tsx +13 -89
- package/templates/storefront-bakery/app/layout.tsx +37 -2
- package/templates/storefront-bakery/app/llms.txt/route.ts +94 -0
- package/templates/storefront-bakery/app/not-found.tsx +39 -0
- package/templates/storefront-bakery/app/page.tsx +6 -6
- package/templates/storefront-bakery/app/privacy/page.tsx +24 -68
- package/templates/storefront-bakery/app/robots.ts +18 -0
- package/templates/storefront-bakery/app/shop/page.tsx +3 -2
- package/templates/storefront-bakery/app/sitemap.ts +62 -0
- package/templates/storefront-bakery/app/terms/page.tsx +24 -82
- package/templates/storefront-bakery/components/footer.tsx +88 -130
- package/templates/storefront-bakery/components/header.tsx +8 -16
- package/templates/storefront-bakery/lib/brand.ts +453 -0
- package/templates/storefront-fashion/.env.example +16 -0
- package/templates/storefront-fashion/AGENTS.md +52 -0
- package/templates/storefront-fashion/README.md +77 -0
- package/templates/storefront-fashion/app/about/page.tsx +41 -0
- package/templates/storefront-fashion/app/cart/page.tsx +9 -0
- package/templates/storefront-fashion/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-fashion/app/categories/[slug]/page.tsx +130 -0
- package/templates/storefront-fashion/app/checkout/page.tsx +17 -0
- package/templates/storefront-fashion/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-fashion/app/collections/[slug]/page.tsx +130 -0
- package/templates/storefront-fashion/app/error.tsx +61 -0
- package/templates/storefront-fashion/app/faq/page.tsx +46 -0
- package/templates/storefront-fashion/app/globals.css +46 -0
- package/templates/storefront-fashion/app/layout.tsx +76 -0
- package/templates/storefront-fashion/app/llms.txt/route.ts +94 -0
- package/templates/storefront-fashion/app/not-found.tsx +39 -0
- package/templates/storefront-fashion/app/orders/[id]/page.tsx +24 -0
- package/templates/storefront-fashion/app/page.tsx +183 -0
- package/templates/storefront-fashion/app/privacy/page.tsx +44 -0
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +165 -0
- package/templates/storefront-fashion/app/products/[slug]/product-detail.tsx +71 -0
- package/templates/storefront-fashion/app/robots.ts +18 -0
- package/templates/storefront-fashion/app/shop/page.tsx +63 -0
- package/templates/storefront-fashion/app/shop/shop-client.tsx +32 -0
- package/templates/storefront-fashion/app/sitemap.ts +59 -0
- package/templates/storefront-fashion/app/terms/page.tsx +44 -0
- package/templates/storefront-fashion/components/brand-marquee.tsx +27 -0
- package/templates/storefront-fashion/components/cart-pill.tsx +32 -0
- package/templates/storefront-fashion/components/category-grid.tsx +28 -0
- package/templates/storefront-fashion/components/category-tiles.tsx +104 -0
- package/templates/storefront-fashion/components/collection-strip.tsx +45 -0
- package/templates/storefront-fashion/components/feature-hero.tsx +82 -0
- package/templates/storefront-fashion/components/footer.tsx +153 -0
- package/templates/storefront-fashion/components/header.tsx +43 -0
- package/templates/storefront-fashion/components/hero.tsx +28 -0
- package/templates/storefront-fashion/components/nav-link.tsx +20 -0
- package/templates/storefront-fashion/components/newsletter.tsx +50 -0
- package/templates/storefront-fashion/components/promo-banner.tsx +41 -0
- package/templates/storefront-fashion/components/providers.tsx +31 -0
- package/templates/storefront-fashion/components/section-heading.tsx +37 -0
- package/templates/storefront-fashion/components/store-product-card.tsx +83 -0
- package/templates/storefront-fashion/components/trade-in-cta.tsx +54 -0
- package/templates/storefront-fashion/components/trust-bar.tsx +66 -0
- package/templates/storefront-fashion/lib/brand.ts +421 -0
- package/templates/storefront-fashion/lib/cart.ts +27 -0
- package/templates/storefront-fashion/next.config.ts +40 -0
- package/templates/storefront-fashion/package.json +28 -0
- package/templates/storefront-fashion/postcss.config.mjs +7 -0
- package/templates/storefront-fashion/tsconfig.json +23 -0
- package/templates/storefront-grocery/.env.example +16 -0
- package/templates/storefront-grocery/AGENTS.md +39 -0
- package/templates/storefront-grocery/README.md +73 -0
- package/templates/storefront-grocery/app/about/page.tsx +38 -0
- package/templates/storefront-grocery/app/cart/page.tsx +9 -0
- package/templates/storefront-grocery/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-grocery/app/categories/[slug]/page.tsx +118 -0
- package/templates/storefront-grocery/app/checkout/page.tsx +17 -0
- package/templates/storefront-grocery/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-grocery/app/collections/[slug]/page.tsx +118 -0
- package/templates/storefront-grocery/app/error.tsx +60 -0
- package/templates/storefront-grocery/app/faq/page.tsx +46 -0
- package/templates/storefront-grocery/app/globals.css +45 -0
- package/templates/storefront-grocery/app/layout.tsx +75 -0
- package/templates/storefront-grocery/app/llms.txt/route.ts +94 -0
- package/templates/storefront-grocery/app/not-found.tsx +39 -0
- package/templates/storefront-grocery/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront-grocery/app/page.tsx +97 -0
- package/templates/storefront-grocery/app/privacy/page.tsx +44 -0
- package/templates/storefront-grocery/app/robots.ts +18 -0
- package/templates/storefront-grocery/app/shop/page.tsx +31 -0
- package/templates/storefront-grocery/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-grocery/app/sitemap.ts +62 -0
- package/templates/storefront-grocery/app/terms/page.tsx +44 -0
- package/templates/storefront-grocery/components/cart-pill.tsx +32 -0
- package/templates/storefront-grocery/components/category-grid.tsx +28 -0
- package/templates/storefront-grocery/components/collection-strip.tsx +45 -0
- package/templates/storefront-grocery/components/footer.tsx +148 -0
- package/templates/storefront-grocery/components/header.tsx +43 -0
- package/templates/storefront-grocery/components/hero.tsx +25 -0
- package/templates/storefront-grocery/components/nav-link.tsx +20 -0
- package/templates/storefront-grocery/components/providers.tsx +31 -0
- package/templates/storefront-grocery/components/store-product-card.tsx +83 -0
- package/templates/storefront-grocery/lib/brand.ts +284 -0
- package/templates/storefront-grocery/lib/cart.ts +27 -0
- package/templates/storefront-grocery/next.config.ts +40 -0
- package/templates/storefront-grocery/package.json +28 -0
- package/templates/storefront-grocery/postcss.config.mjs +7 -0
- package/templates/storefront-grocery/tsconfig.json +23 -0
- package/templates/storefront-restaurant/.env.example +16 -0
- package/templates/storefront-restaurant/AGENTS.md +39 -0
- package/templates/storefront-restaurant/README.md +73 -0
- package/templates/storefront-restaurant/app/about/page.tsx +38 -0
- package/templates/storefront-restaurant/app/cart/page.tsx +9 -0
- package/templates/storefront-restaurant/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-restaurant/app/categories/[slug]/page.tsx +118 -0
- package/templates/storefront-restaurant/app/checkout/page.tsx +17 -0
- package/templates/storefront-restaurant/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-restaurant/app/collections/[slug]/page.tsx +118 -0
- package/templates/storefront-restaurant/app/error.tsx +60 -0
- package/templates/storefront-restaurant/app/faq/page.tsx +46 -0
- package/templates/storefront-restaurant/app/globals.css +44 -0
- package/templates/storefront-restaurant/app/layout.tsx +80 -0
- package/templates/storefront-restaurant/app/llms.txt/route.ts +94 -0
- package/templates/storefront-restaurant/app/not-found.tsx +39 -0
- package/templates/storefront-restaurant/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront-restaurant/app/page.tsx +97 -0
- package/templates/storefront-restaurant/app/privacy/page.tsx +44 -0
- package/templates/storefront-restaurant/app/robots.ts +18 -0
- package/templates/storefront-restaurant/app/shop/page.tsx +31 -0
- package/templates/storefront-restaurant/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-restaurant/app/sitemap.ts +62 -0
- package/templates/storefront-restaurant/app/terms/page.tsx +44 -0
- package/templates/storefront-restaurant/components/cart-pill.tsx +32 -0
- package/templates/storefront-restaurant/components/category-grid.tsx +28 -0
- package/templates/storefront-restaurant/components/collection-strip.tsx +45 -0
- package/templates/storefront-restaurant/components/footer.tsx +148 -0
- package/templates/storefront-restaurant/components/header.tsx +43 -0
- package/templates/storefront-restaurant/components/hero.tsx +25 -0
- package/templates/storefront-restaurant/components/nav-link.tsx +20 -0
- package/templates/storefront-restaurant/components/product-modal.tsx +105 -0
- package/templates/storefront-restaurant/components/providers.tsx +31 -0
- package/templates/storefront-restaurant/components/store-product-card.tsx +83 -0
- package/templates/storefront-restaurant/lib/brand.ts +287 -0
- package/templates/storefront-restaurant/lib/cart.ts +27 -0
- package/templates/storefront-restaurant/next.config.ts +40 -0
- package/templates/storefront-restaurant/package.json +28 -0
- package/templates/storefront-restaurant/postcss.config.mjs +7 -0
- package/templates/storefront-restaurant/tsconfig.json +23 -0
- package/templates/storefront-retail/.env.example +5 -0
- package/templates/storefront-retail/AGENTS.md +64 -0
- package/templates/storefront-retail/app/about/page.tsx +24 -54
- package/templates/storefront-retail/app/categories/[slug]/page.tsx +2 -1
- package/templates/storefront-retail/app/collections/[slug]/page.tsx +2 -1
- package/templates/storefront-retail/app/error.tsx +61 -0
- package/templates/storefront-retail/app/faq/page.tsx +11 -92
- package/templates/storefront-retail/app/layout.tsx +37 -8
- package/templates/storefront-retail/app/llms.txt/route.ts +94 -0
- package/templates/storefront-retail/app/not-found.tsx +39 -0
- package/templates/storefront-retail/app/page.tsx +16 -73
- package/templates/storefront-retail/app/privacy/page.tsx +24 -81
- package/templates/storefront-retail/app/products/[slug]/page.tsx +165 -0
- package/templates/storefront-retail/app/products/[slug]/product-detail.tsx +71 -0
- package/templates/storefront-retail/app/robots.ts +18 -0
- package/templates/storefront-retail/app/shop/page.tsx +3 -3
- package/templates/storefront-retail/app/sitemap.ts +59 -0
- package/templates/storefront-retail/app/terms/page.tsx +24 -97
- package/templates/storefront-retail/components/brand-marquee.tsx +6 -15
- package/templates/storefront-retail/components/feature-hero.tsx +2 -1
- package/templates/storefront-retail/components/footer.tsx +85 -120
- package/templates/storefront-retail/components/header.tsx +10 -12
- package/templates/storefront-retail/components/newsletter.tsx +7 -9
- package/templates/storefront-retail/components/promo-banner.tsx +12 -15
- package/templates/storefront-retail/components/store-product-card.tsx +7 -7
- package/templates/storefront-retail/components/trade-in-cta.tsx +11 -17
- package/templates/storefront-retail/components/trust-bar.tsx +45 -5
- package/templates/storefront-retail/lib/brand.ts +567 -0
- package/templates/storefront-services/.env.example +16 -0
- package/templates/storefront-services/AGENTS.md +41 -0
- package/templates/storefront-services/README.md +73 -0
- package/templates/storefront-services/app/about/page.tsx +38 -0
- package/templates/storefront-services/app/cart/page.tsx +9 -0
- package/templates/storefront-services/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/storefront-services/app/categories/[slug]/page.tsx +118 -0
- package/templates/storefront-services/app/checkout/page.tsx +17 -0
- package/templates/storefront-services/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/storefront-services/app/collections/[slug]/page.tsx +118 -0
- package/templates/storefront-services/app/error.tsx +60 -0
- package/templates/storefront-services/app/faq/page.tsx +46 -0
- package/templates/storefront-services/app/globals.css +45 -0
- package/templates/storefront-services/app/layout.tsx +80 -0
- package/templates/storefront-services/app/llms.txt/route.ts +94 -0
- package/templates/storefront-services/app/not-found.tsx +39 -0
- package/templates/storefront-services/app/orders/[id]/page.tsx +21 -0
- package/templates/storefront-services/app/page.tsx +97 -0
- package/templates/storefront-services/app/privacy/page.tsx +44 -0
- package/templates/storefront-services/app/robots.ts +18 -0
- package/templates/storefront-services/app/shop/page.tsx +31 -0
- package/templates/storefront-services/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-services/app/sitemap.ts +62 -0
- package/templates/storefront-services/app/terms/page.tsx +44 -0
- package/templates/storefront-services/components/cart-pill.tsx +32 -0
- package/templates/storefront-services/components/category-grid.tsx +28 -0
- package/templates/storefront-services/components/collection-strip.tsx +45 -0
- package/templates/storefront-services/components/footer.tsx +148 -0
- package/templates/storefront-services/components/header.tsx +43 -0
- package/templates/storefront-services/components/hero.tsx +25 -0
- package/templates/storefront-services/components/nav-link.tsx +20 -0
- package/templates/storefront-services/components/product-modal.tsx +105 -0
- package/templates/storefront-services/components/providers.tsx +31 -0
- package/templates/storefront-services/components/store-product-card.tsx +83 -0
- package/templates/storefront-services/lib/brand.ts +303 -0
- package/templates/storefront-services/lib/cart.ts +27 -0
- package/templates/storefront-services/next.config.ts +40 -0
- package/templates/storefront-services/package.json +28 -0
- package/templates/storefront-services/postcss.config.mjs +7 -0
- package/templates/storefront-services/tsconfig.json +23 -0
- /package/templates/{storefront-retail → storefront-grocery}/components/product-modal.tsx +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { notFound } from "next/navigation";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
6
|
+
import {
|
|
7
|
+
getServerClient,
|
|
8
|
+
tags,
|
|
9
|
+
type Product,
|
|
10
|
+
type ProductWithDetails,
|
|
11
|
+
} from "@cimplify/sdk/server";
|
|
12
|
+
import { ProductDetail } from "./product-detail";
|
|
13
|
+
import { brand } from "@/lib/brand";
|
|
14
|
+
|
|
15
|
+
const SITE_URL =
|
|
16
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
17
|
+
|
|
18
|
+
function productLd(product: ProductWithDetails) {
|
|
19
|
+
const image = product.image_url ?? product.images?.[0];
|
|
20
|
+
const inStock = product.inventory_status?.in_stock !== false;
|
|
21
|
+
return {
|
|
22
|
+
"@context": "https://schema.org",
|
|
23
|
+
"@type": "Product",
|
|
24
|
+
name: product.name,
|
|
25
|
+
description: product.description ?? undefined,
|
|
26
|
+
image: image ? [image] : undefined,
|
|
27
|
+
sku: product.id,
|
|
28
|
+
brand: { "@type": "Brand", name: brand.name },
|
|
29
|
+
offers: {
|
|
30
|
+
"@type": "Offer",
|
|
31
|
+
price: product.default_price,
|
|
32
|
+
priceCurrency: product.currency ?? brand.currency,
|
|
33
|
+
availability: inStock
|
|
34
|
+
? "https://schema.org/InStock"
|
|
35
|
+
: "https://schema.org/OutOfStock",
|
|
36
|
+
url: `${SITE_URL}/products/${product.slug ?? product.id}`,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ProductData {
|
|
42
|
+
product: ProductWithDetails;
|
|
43
|
+
related: Product[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getProduct(slug: string): Promise<ProductData | null> {
|
|
47
|
+
"use cache";
|
|
48
|
+
cacheTag(tags.product(slug), tags.products());
|
|
49
|
+
cacheLife("hours");
|
|
50
|
+
|
|
51
|
+
const client = getServerClient();
|
|
52
|
+
const r = await client.catalogue.getProductBySlug(slug);
|
|
53
|
+
if (!r.ok) return null;
|
|
54
|
+
|
|
55
|
+
const related = r.value.category_id
|
|
56
|
+
? await client.catalogue
|
|
57
|
+
.getCategoryProducts(r.value.category_id)
|
|
58
|
+
.then((res) =>
|
|
59
|
+
res.ok
|
|
60
|
+
? (
|
|
61
|
+
((res.value as { items?: Product[] }).items ??
|
|
62
|
+
(res.value as Product[])) as Product[]
|
|
63
|
+
).filter((p) => p.id !== r.value.id).slice(0, 4)
|
|
64
|
+
: [],
|
|
65
|
+
)
|
|
66
|
+
: [];
|
|
67
|
+
|
|
68
|
+
return { product: r.value, related };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function generateMetadata({
|
|
72
|
+
params,
|
|
73
|
+
}: {
|
|
74
|
+
params: Promise<{ slug: string }>;
|
|
75
|
+
}): Promise<Metadata> {
|
|
76
|
+
const { slug } = await params;
|
|
77
|
+
const data = await getProduct(slug);
|
|
78
|
+
if (!data) return {};
|
|
79
|
+
const { product } = data;
|
|
80
|
+
const image = product.image_url ?? product.images?.[0];
|
|
81
|
+
return {
|
|
82
|
+
title: `${product.name} — ${brand.name}`,
|
|
83
|
+
description: product.description ?? undefined,
|
|
84
|
+
openGraph: {
|
|
85
|
+
title: product.name,
|
|
86
|
+
description: product.description ?? undefined,
|
|
87
|
+
images: image ? [{ url: image }] : undefined,
|
|
88
|
+
type: "website",
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default async function ProductPage({
|
|
94
|
+
params,
|
|
95
|
+
}: {
|
|
96
|
+
params: Promise<{ slug: string }>;
|
|
97
|
+
}) {
|
|
98
|
+
return (
|
|
99
|
+
<Suspense fallback={<ProductSkeleton />}>
|
|
100
|
+
<ProductContent params={params} />
|
|
101
|
+
</Suspense>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function ProductContent({
|
|
106
|
+
params,
|
|
107
|
+
}: {
|
|
108
|
+
params: Promise<{ slug: string }>;
|
|
109
|
+
}) {
|
|
110
|
+
const { slug } = await params;
|
|
111
|
+
const data = await getProduct(slug);
|
|
112
|
+
if (!data) notFound();
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<script
|
|
117
|
+
type="application/ld+json"
|
|
118
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(productLd(data.product)) }}
|
|
119
|
+
/>
|
|
120
|
+
<nav
|
|
121
|
+
aria-label="Breadcrumb"
|
|
122
|
+
className="max-w-7xl mx-auto px-6 sm:px-8 pt-6 text-[12px] font-mono text-muted-foreground flex items-center gap-2"
|
|
123
|
+
>
|
|
124
|
+
<Link href="/" className="hover:text-foreground transition-colors">
|
|
125
|
+
Home
|
|
126
|
+
</Link>
|
|
127
|
+
<span>/</span>
|
|
128
|
+
<Link href="/shop" className="hover:text-foreground transition-colors">
|
|
129
|
+
Shop
|
|
130
|
+
</Link>
|
|
131
|
+
{data.product.category?.slug && (
|
|
132
|
+
<>
|
|
133
|
+
<span>/</span>
|
|
134
|
+
<Link
|
|
135
|
+
href={`/categories/${data.product.category.slug}`}
|
|
136
|
+
className="hover:text-foreground transition-colors"
|
|
137
|
+
>
|
|
138
|
+
{data.product.category.name}
|
|
139
|
+
</Link>
|
|
140
|
+
</>
|
|
141
|
+
)}
|
|
142
|
+
<span>/</span>
|
|
143
|
+
<span className="text-foreground/90 truncate">{data.product.name}</span>
|
|
144
|
+
</nav>
|
|
145
|
+
<ProductDetail product={data.product} related={data.related} />
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function ProductSkeleton() {
|
|
151
|
+
return (
|
|
152
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-10">
|
|
153
|
+
<div className="grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 items-start">
|
|
154
|
+
<div className="aspect-square bg-muted rounded-3xl animate-pulse" />
|
|
155
|
+
<div className="space-y-4">
|
|
156
|
+
<div className="h-3 w-24 bg-muted rounded animate-pulse" />
|
|
157
|
+
<div className="h-10 w-3/4 bg-muted rounded animate-pulse" />
|
|
158
|
+
<div className="h-7 w-32 bg-muted rounded animate-pulse" />
|
|
159
|
+
<div className="h-20 w-full bg-muted rounded animate-pulse mt-6" />
|
|
160
|
+
<div className="h-12 w-full bg-muted rounded animate-pulse mt-4" />
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { ProductPage as SdkProductPage } from "@cimplify/sdk/react";
|
|
6
|
+
import type { Product, ProductWithDetails } from "@cimplify/sdk";
|
|
7
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Client island for the product detail page.
|
|
11
|
+
*
|
|
12
|
+
* - Receives a server-fetched `ProductWithDetails` (no client refetch).
|
|
13
|
+
* - Renders the SDK `<ProductPage>` which picks the right layout
|
|
14
|
+
* (Default / Wholesale / Service / Bundle / Composite) automatically.
|
|
15
|
+
* - On add-to-cart success, routes to `/cart`.
|
|
16
|
+
* - Custom Next.js Image renderer for optimised, lazy-loaded gallery shots.
|
|
17
|
+
* - Renders a "You may also like" rail of in-category products below.
|
|
18
|
+
*/
|
|
19
|
+
export function ProductDetail({
|
|
20
|
+
product,
|
|
21
|
+
related,
|
|
22
|
+
}: {
|
|
23
|
+
product: ProductWithDetails;
|
|
24
|
+
related: Product[];
|
|
25
|
+
}) {
|
|
26
|
+
const router = useRouter();
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<>
|
|
30
|
+
<SdkProductPage
|
|
31
|
+
product={product}
|
|
32
|
+
showRelated={false}
|
|
33
|
+
onAddToCart={() => {
|
|
34
|
+
router.push("/cart");
|
|
35
|
+
}}
|
|
36
|
+
renderImage={({ src, alt, className }) => (
|
|
37
|
+
<Image
|
|
38
|
+
src={src}
|
|
39
|
+
alt={alt}
|
|
40
|
+
width={1200}
|
|
41
|
+
height={1200}
|
|
42
|
+
className={className}
|
|
43
|
+
style={{ width: "100%", height: "auto", objectFit: "cover" }}
|
|
44
|
+
priority
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
className="max-w-7xl mx-auto px-6 sm:px-8 py-8 sm:py-10"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
{related.length > 0 && (
|
|
51
|
+
<section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-16 border-t border-border mt-8">
|
|
52
|
+
<div className="flex items-end justify-between gap-6 mb-8">
|
|
53
|
+
<div>
|
|
54
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
|
|
55
|
+
You may also like
|
|
56
|
+
</p>
|
|
57
|
+
<h2 className="text-[clamp(1.5rem,2.5vw,2rem)] font-bold m-0 -tracking-[0.025em]">
|
|
58
|
+
More from this category.
|
|
59
|
+
</h2>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
63
|
+
{related.map((p) => (
|
|
64
|
+
<StoreProductCard key={p.id} product={p} />
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
)}
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
|
|
3
|
+
const SITE_URL =
|
|
4
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
5
|
+
|
|
6
|
+
export default function robots(): MetadataRoute.Robots {
|
|
7
|
+
return {
|
|
8
|
+
rules: [
|
|
9
|
+
{
|
|
10
|
+
userAgent: "*",
|
|
11
|
+
allow: "/",
|
|
12
|
+
disallow: ["/cart", "/checkout", "/orders/", "/api/"],
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
sitemap: `${SITE_URL}/sitemap.xml`,
|
|
16
|
+
host: SITE_URL,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -3,11 +3,11 @@ import { Suspense } from "react";
|
|
|
3
3
|
import { cacheTag, cacheLife } from "next/cache";
|
|
4
4
|
import { getServerClient, tags } from "@cimplify/sdk/server";
|
|
5
5
|
import { ShopClient } from "./shop-client";
|
|
6
|
+
import { brand } from "@/lib/brand";
|
|
6
7
|
|
|
7
8
|
export const metadata: Metadata = {
|
|
8
|
-
title:
|
|
9
|
-
description:
|
|
10
|
-
"Browse every laptop, phone, audio device, accessory, and gaming product we stock. Authorised dealer pricing, 2-year warranty.",
|
|
9
|
+
title: `Shop — ${brand.name}`,
|
|
10
|
+
description: brand.description,
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
async function getShopData() {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { getServerClient, type Product } from "@cimplify/sdk/server";
|
|
3
|
+
|
|
4
|
+
const SITE_URL =
|
|
5
|
+
process.env.NEXT_PUBLIC_SITE_URL?.trim() || "https://example.com";
|
|
6
|
+
|
|
7
|
+
const STATIC_ROUTES: { path: string; priority: number; changeFrequency: "daily" | "weekly" | "monthly" }[] = [
|
|
8
|
+
{ path: "/", priority: 1.0, changeFrequency: "daily" },
|
|
9
|
+
{ path: "/shop", priority: 0.9, changeFrequency: "daily" },
|
|
10
|
+
{ path: "/about", priority: 0.5, changeFrequency: "monthly" },
|
|
11
|
+
{ path: "/faq", priority: 0.4, changeFrequency: "monthly" },
|
|
12
|
+
{ path: "/terms", priority: 0.2, changeFrequency: "monthly" },
|
|
13
|
+
{ path: "/privacy", priority: 0.2, changeFrequency: "monthly" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
17
|
+
const now = new Date();
|
|
18
|
+
const client = getServerClient();
|
|
19
|
+
|
|
20
|
+
const [productsRes, categoriesRes, collectionsRes] = await Promise.all([
|
|
21
|
+
client.catalogue.getProducts({ limit: 500 }),
|
|
22
|
+
client.catalogue.getCategories(),
|
|
23
|
+
client.catalogue.getCollections(),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const products = productsRes.ok ? productsRes.value.items : [];
|
|
27
|
+
const categories = categoriesRes.ok ? categoriesRes.value : [];
|
|
28
|
+
const collections = collectionsRes.ok ? collectionsRes.value : [];
|
|
29
|
+
|
|
30
|
+
const staticEntries: MetadataRoute.Sitemap = STATIC_ROUTES.map((r) => ({
|
|
31
|
+
url: `${SITE_URL}${r.path}`,
|
|
32
|
+
lastModified: now,
|
|
33
|
+
changeFrequency: r.changeFrequency,
|
|
34
|
+
priority: r.priority,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const productEntries: MetadataRoute.Sitemap = products.map((p: Product) => ({
|
|
38
|
+
url: `${SITE_URL}/products/${p.slug ?? p.id}`,
|
|
39
|
+
lastModified: p.updated_at ? new Date(p.updated_at) : now,
|
|
40
|
+
changeFrequency: "weekly",
|
|
41
|
+
priority: 0.7,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const categoryEntries: MetadataRoute.Sitemap = categories.map((c) => ({
|
|
45
|
+
url: `${SITE_URL}/categories/${c.slug}`,
|
|
46
|
+
lastModified: now,
|
|
47
|
+
changeFrequency: "weekly",
|
|
48
|
+
priority: 0.6,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const collectionEntries: MetadataRoute.Sitemap = collections.map((c) => ({
|
|
52
|
+
url: `${SITE_URL}/collections/${c.slug}`,
|
|
53
|
+
lastModified: now,
|
|
54
|
+
changeFrequency: "weekly",
|
|
55
|
+
priority: 0.6,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
return [...staticEntries, ...categoryEntries, ...collectionEntries, ...productEntries];
|
|
59
|
+
}
|
|
@@ -1,116 +1,43 @@
|
|
|
1
1
|
import type { Metadata } from "next";
|
|
2
|
+
import { brand } from "@/lib/brand";
|
|
2
3
|
|
|
3
4
|
export const metadata: Metadata = {
|
|
4
|
-
title:
|
|
5
|
-
description:
|
|
5
|
+
title: `Terms of Service — ${brand.name}`,
|
|
6
|
+
description: `The rules of the road for ordering from ${brand.name}.`,
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export default function TermsPage() {
|
|
10
|
+
const t = brand.terms;
|
|
9
11
|
return (
|
|
10
12
|
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
11
13
|
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
12
|
-
|
|
14
|
+
{t.eyebrow}
|
|
13
15
|
</p>
|
|
14
16
|
<h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">
|
|
15
|
-
|
|
17
|
+
{t.title}
|
|
16
18
|
</h1>
|
|
17
19
|
<p className="text-sm text-muted-foreground not-prose mb-10">
|
|
18
|
-
Last updated:
|
|
20
|
+
Last updated: {t.lastUpdated}
|
|
19
21
|
</p>
|
|
20
22
|
|
|
21
23
|
<section className="space-y-5 leading-relaxed text-foreground/90">
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
official portal at any time.
|
|
40
|
-
</p>
|
|
41
|
-
|
|
42
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
43
|
-
3. Pricing and payment
|
|
44
|
-
</h2>
|
|
45
|
-
<p>
|
|
46
|
-
Prices are in Ghana Cedis (GH₵) and include VAT where applicable.
|
|
47
|
-
Payment is due at checkout via Mobile Money, card, or instalments via
|
|
48
|
-
our partner bank. Cash on delivery is available for Accra orders only.
|
|
49
|
-
</p>
|
|
50
|
-
|
|
51
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
52
|
-
4. Warranty
|
|
53
|
-
</h2>
|
|
54
|
-
<p>
|
|
55
|
-
Every product carries a two-year warranty: the standard manufacturer
|
|
56
|
-
warranty in year one, plus a Currents-backed extension in year two.
|
|
57
|
-
Coverage includes manufacturing defects and battery degradation below
|
|
58
|
-
80%. Accidental damage, liquid damage, and unauthorised modifications
|
|
59
|
-
are excluded.
|
|
60
|
-
</p>
|
|
61
|
-
|
|
62
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
63
|
-
5. Returns
|
|
64
|
-
</h2>
|
|
65
|
-
<p>
|
|
66
|
-
Sealed products: 14 days, no questions asked. Opened products:
|
|
67
|
-
7 days, subject to a 10% restocking fee for non-defective items.
|
|
68
|
-
Activated SIM-bundled phones cannot be returned once the SIM is
|
|
69
|
-
registered. Custom orders and bulk corporate orders are
|
|
70
|
-
non-refundable except for defects.
|
|
71
|
-
</p>
|
|
72
|
-
|
|
73
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
74
|
-
6. Trade-in
|
|
75
|
-
</h2>
|
|
76
|
-
<p>
|
|
77
|
-
Trade-in quotes are estimates only. Final value is determined after
|
|
78
|
-
inspection by our refurbishment partner. We reserve the right to
|
|
79
|
-
revise the quote, and you may decline the revised offer and receive
|
|
80
|
-
your device back at our cost.
|
|
81
|
-
</p>
|
|
82
|
-
|
|
83
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
84
|
-
7. Liability
|
|
85
|
-
</h2>
|
|
86
|
-
<p>
|
|
87
|
-
To the maximum extent permitted by Ghanaian law, our liability for
|
|
88
|
-
any claim arising from this website or your order is limited to the
|
|
89
|
-
value of the order. We're not liable for indirect or
|
|
90
|
-
consequential losses, including data loss.
|
|
91
|
-
</p>
|
|
92
|
-
|
|
93
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
94
|
-
8. Governing law
|
|
95
|
-
</h2>
|
|
96
|
-
<p>
|
|
97
|
-
These terms are governed by the laws of Ghana. Disputes will be
|
|
98
|
-
resolved in the courts of Greater Accra.
|
|
99
|
-
</p>
|
|
100
|
-
|
|
101
|
-
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
102
|
-
9. Contact
|
|
103
|
-
</h2>
|
|
104
|
-
<p>
|
|
105
|
-
Questions? Email{" "}
|
|
106
|
-
<a
|
|
107
|
-
href="mailto:hello@currentselectronics.test"
|
|
108
|
-
className="text-primary font-semibold hover:underline"
|
|
109
|
-
>
|
|
110
|
-
hello@currentselectronics.test
|
|
111
|
-
</a>
|
|
112
|
-
.
|
|
113
|
-
</p>
|
|
24
|
+
{t.sections.map((s) => (
|
|
25
|
+
<div key={s.heading}>
|
|
26
|
+
<h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">{s.heading}</h2>
|
|
27
|
+
{typeof s.body === "string" ? (
|
|
28
|
+
<p>{s.body}</p>
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<p>{s.body.intro}</p>
|
|
32
|
+
<ul className="list-disc pl-6 space-y-2">
|
|
33
|
+
{s.body.bullets.map((b) => (
|
|
34
|
+
<li key={b}>{b}</li>
|
|
35
|
+
))}
|
|
36
|
+
</ul>
|
|
37
|
+
</>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
114
41
|
</section>
|
|
115
42
|
</article>
|
|
116
43
|
);
|
|
@@ -1,28 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
"Apple",
|
|
3
|
-
"Samsung",
|
|
4
|
-
"Sony",
|
|
5
|
-
"Bose",
|
|
6
|
-
"Logitech",
|
|
7
|
-
"Anker",
|
|
8
|
-
"JBL",
|
|
9
|
-
"Microsoft",
|
|
10
|
-
"Lenovo",
|
|
11
|
-
"DJI",
|
|
12
|
-
];
|
|
1
|
+
import { brand } from "@/lib/brand";
|
|
13
2
|
|
|
14
3
|
/**
|
|
15
4
|
* Brand authority strip. Wordmark-only (avoids licensing issues with real
|
|
16
|
-
* logos),
|
|
5
|
+
* logos), monospaced for the brand-as-typography aesthetic.
|
|
17
6
|
*/
|
|
18
7
|
export function BrandMarquee() {
|
|
8
|
+
const strip = brand.brandStrip;
|
|
9
|
+
if (!strip || strip.brands.length === 0) return null;
|
|
19
10
|
return (
|
|
20
11
|
<section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">
|
|
21
12
|
<p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">
|
|
22
|
-
|
|
13
|
+
{strip.headline}
|
|
23
14
|
</p>
|
|
24
15
|
<div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">
|
|
25
|
-
{
|
|
16
|
+
{strip.brands.map((b) => (
|
|
26
17
|
<span
|
|
27
18
|
key={b}
|
|
28
19
|
className="text-[clamp(1.25rem,2vw,1.75rem)] font-semibold text-muted-foreground hover:text-foreground transition-colors -tracking-[0.025em] opacity-70 hover:opacity-100"
|
|
@@ -14,7 +14,8 @@ interface FeatureHeroProps {
|
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Apple-style split hero. Left: copy + CTAs. Right: large product image.
|
|
17
|
-
* Stacks to image-on-top on mobile.
|
|
17
|
+
* Stacks to image-on-top on mobile. Strings come from `brand.hero` at the
|
|
18
|
+
* call site — this component is otherwise design-only.
|
|
18
19
|
*/
|
|
19
20
|
export function FeatureHero({
|
|
20
21
|
eyebrow,
|