@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.
- package/dist/dispatcher.mjs +39 -5
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/templates/{storefront → storefront-bakery}/README.md +8 -1
- package/templates/storefront-bakery/app/about/page.tsx +61 -0
- package/templates/storefront-bakery/app/categories/[slug]/listing-client.tsx +19 -0
- package/templates/{storefront → storefront-bakery}/app/categories/[slug]/page.tsx +42 -10
- package/templates/storefront-bakery/app/collections/[slug]/listing-client.tsx +20 -0
- package/templates/{storefront → storefront-bakery}/app/collections/[slug]/page.tsx +42 -10
- package/templates/storefront-bakery/app/faq/page.tsx +122 -0
- package/templates/{storefront → storefront-bakery}/app/page.tsx +39 -6
- package/templates/storefront-bakery/app/privacy/page.tsx +88 -0
- package/templates/storefront-bakery/app/terms/page.tsx +102 -0
- package/templates/storefront-bakery/components/cart-pill.tsx +32 -0
- package/templates/storefront-bakery/components/footer.tsx +190 -0
- package/templates/storefront-bakery/components/header.tsx +51 -0
- package/templates/storefront-bakery/components/nav-link.tsx +20 -0
- package/templates/{storefront → storefront-bakery}/components/store-product-card.tsx +9 -25
- package/templates/{storefront → storefront-bakery}/next.config.ts +3 -4
- 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 +117 -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 +117 -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 +97 -0
- package/templates/storefront-retail/app/privacy/page.tsx +101 -0
- package/templates/storefront-retail/app/shop/page.tsx +30 -0
- package/templates/storefront-retail/app/shop/shop-client.tsx +27 -0
- package/templates/storefront-retail/app/terms/page.tsx +117 -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/collection-strip.tsx +45 -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/product-modal.tsx +105 -0
- package/templates/storefront-retail/components/providers.tsx +31 -0
- package/templates/storefront-retail/components/store-product-card.tsx +83 -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/components/footer.tsx +0 -19
- package/templates/storefront/components/header.tsx +0 -41
- package/templates/storefront/lib/server-client.ts +0 -27
- /package/templates/{storefront → storefront-bakery}/.env.example +0 -0
- /package/templates/{storefront → storefront-bakery}/app/cart/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/checkout/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/shop/page.tsx +0 -0
- /package/templates/{storefront → storefront-bakery}/app/shop/shop-client.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/hero.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}/lib/cart.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,77 @@
|
|
|
1
|
+
# __STOREFRONT_NAME__
|
|
2
|
+
|
|
3
|
+
A Cimplify storefront scaffolded from the **retail** template — Next.js 16 (App Router), React 19, Tailwind v4. Cool navy + electric blue palette, Inter + JetBrains Mono typography, modern electronics-store aesthetic.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
bun dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Two things start in parallel:
|
|
13
|
+
|
|
14
|
+
- `bunx @cimplify/sdk --seed retail` — the Cimplify mock API on `http://127.0.0.1:8787`, seeded with Currents Electronics.
|
|
15
|
+
- `next dev` — this storefront on `http://localhost:3000`.
|
|
16
|
+
|
|
17
|
+
Open the storefront in your browser. Edit `app/page.tsx` to start customising.
|
|
18
|
+
|
|
19
|
+
## Structure
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
app/
|
|
23
|
+
layout.tsx # root layout, fonts, providers, header/footer/modal
|
|
24
|
+
page.tsx # home
|
|
25
|
+
shop/page.tsx # full catalogue (SDK <CataloguePage/>)
|
|
26
|
+
collections/[slug]/ # collection landing
|
|
27
|
+
categories/[slug]/ # category landing
|
|
28
|
+
cart/page.tsx # SDK <CartPage/>
|
|
29
|
+
checkout/page.tsx # SDK <CheckoutPage/>
|
|
30
|
+
orders/[id]/page.tsx # post-checkout thank-you
|
|
31
|
+
about, faq, terms, privacy
|
|
32
|
+
globals.css # Tailwind import + theme tokens
|
|
33
|
+
components/
|
|
34
|
+
providers.tsx # CimplifyProvider client wrapper
|
|
35
|
+
header.tsx, footer.tsx, hero.tsx
|
|
36
|
+
store-product-card.tsx # SDK <ProductCard/> wired to URL-driven modal
|
|
37
|
+
product-modal.tsx # ?product=<slug> deep-linkable modal
|
|
38
|
+
collection-strip.tsx # horizontal product strip
|
|
39
|
+
category-grid.tsx # SDK <CategoryGrid/> with router navigation
|
|
40
|
+
lib/
|
|
41
|
+
cart.ts # useCartCount() for the header pill
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Switch the seed
|
|
45
|
+
|
|
46
|
+
This template is wired to the `retail` seed. To preview a different industry without re-scaffolding:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bunx @cimplify/sdk --seed restaurant # Mama's Kitchen
|
|
50
|
+
bunx @cimplify/sdk --seed services # Serene Spa
|
|
51
|
+
bunx @cimplify/sdk --seed grocery # FreshMart
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Update `NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID` in `.env.local` to match the seeded business.
|
|
55
|
+
|
|
56
|
+
For a fresh scaffold with another design altogether:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bunx @cimplify/sdk init my-store --template bakery # warm food/pastry
|
|
60
|
+
bunx @cimplify/sdk init my-store --template restaurant # coming soon
|
|
61
|
+
bunx @cimplify/sdk init my-store --template services # coming soon
|
|
62
|
+
bunx @cimplify/sdk init my-store --template grocery # coming soon
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Go live
|
|
66
|
+
|
|
67
|
+
```diff
|
|
68
|
+
# .env.local
|
|
69
|
+
- NEXT_PUBLIC_CIMPLIFY_API_URL=http://127.0.0.1:8787
|
|
70
|
+
+ NEXT_PUBLIC_CIMPLIFY_API_URL=https://api.cimplify.io
|
|
71
|
+
- NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=mock-dev
|
|
72
|
+
+ NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY=<your tenant key>
|
|
73
|
+
- NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=bus_currents_electronics
|
|
74
|
+
+ NEXT_PUBLIC_CIMPLIFY_BUSINESS_ID=<your business id>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Deploy to Vercel with one click — `next.config.ts` already whitelists the SDK image hosts under `images.remotePatterns`.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: "About — Currents Electronics",
|
|
5
|
+
description:
|
|
6
|
+
"Authorised dealer for Apple, Samsung, Sony, Bose. Serving Accra and West Africa with same-day delivery and a two-year warranty since 2017.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function AboutPage() {
|
|
10
|
+
return (
|
|
11
|
+
<article className="max-w-3xl mx-auto px-8 py-16">
|
|
12
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
|
|
13
|
+
About Currents
|
|
14
|
+
</p>
|
|
15
|
+
<h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-6 -tracking-[0.025em] leading-tight">
|
|
16
|
+
Tech that lasts.<br />Service that doesn't flake.
|
|
17
|
+
</h1>
|
|
18
|
+
<div className="prose prose-lg max-w-none space-y-5 text-foreground/90 leading-relaxed">
|
|
19
|
+
<p>
|
|
20
|
+
We started Currents in 2017 because the best phones, laptops, and
|
|
21
|
+
headphones were either three months out of stock in Accra, or
|
|
22
|
+
obviously fake. Either way, you ended up calling someone's
|
|
23
|
+
cousin in London. That's a bad way to buy a thousand-dollar
|
|
24
|
+
device.
|
|
25
|
+
</p>
|
|
26
|
+
<p>
|
|
27
|
+
Today we're an authorised dealer for Apple, Samsung, Sony, Bose,
|
|
28
|
+
Logitech, and Anker. Every product is sourced direct from the
|
|
29
|
+
manufacturer or their certified distributor — no parallel imports,
|
|
30
|
+
no refurbished-as-new, no "dual SIM Asia spec" on a US iPhone.
|
|
31
|
+
What you order is what we open in front of you on delivery.
|
|
32
|
+
</p>
|
|
33
|
+
<p>
|
|
34
|
+
We back everything with a two-year warranty (one year manufacturer +
|
|
35
|
+
one year Currents), free same-day Accra delivery, and a 14-day
|
|
36
|
+
no-questions-asked return on anything sealed.
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">
|
|
40
|
+
Trade-in
|
|
41
|
+
</h2>
|
|
42
|
+
<p>
|
|
43
|
+
Got an iPhone, Galaxy, or MacBook you no longer use? Bring it in or
|
|
44
|
+
ship it to us — we'll quote you within 48 hours and credit the
|
|
45
|
+
value toward your next purchase. We work with refurbishers across the
|
|
46
|
+
continent so your old kit stays in service for years more.
|
|
47
|
+
</p>
|
|
48
|
+
|
|
49
|
+
<h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">
|
|
50
|
+
Business sales
|
|
51
|
+
</h2>
|
|
52
|
+
<p>
|
|
53
|
+
Equipping a team or kitting out a co-working space? We do volume
|
|
54
|
+
quotes, delivery scheduling, asset tagging, and on-site setup. Email{" "}
|
|
55
|
+
<a
|
|
56
|
+
href="mailto:business@currentselectronics.test"
|
|
57
|
+
className="text-primary font-semibold hover:underline"
|
|
58
|
+
>
|
|
59
|
+
business@currentselectronics.test
|
|
60
|
+
</a>
|
|
61
|
+
.
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<h2 className="text-2xl font-semibold mt-10 mb-3 -tracking-[0.02em]">
|
|
65
|
+
Visit
|
|
66
|
+
</h2>
|
|
67
|
+
<p>Atomic Junction, East Legon, Accra · Mon–Sat, 9am–8pm.</p>
|
|
68
|
+
</div>
|
|
69
|
+
</article>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { CartPage as SdkCartPage } from "@cimplify/sdk/react";
|
|
5
|
+
|
|
6
|
+
export default function CartPage() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
return <SdkCartPage onCheckout={() => router.push("/checkout")} />;
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ProductGrid } from "@cimplify/sdk/react";
|
|
4
|
+
import type { Product } from "@cimplify/sdk";
|
|
5
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Client island for the category listing. Receives server-fetched products
|
|
9
|
+
* as props (serializable) and owns the `renderCard` function.
|
|
10
|
+
*/
|
|
11
|
+
export function ListingClient({ products }: { products: Product[] }) {
|
|
12
|
+
return (
|
|
13
|
+
<ProductGrid
|
|
14
|
+
products={products}
|
|
15
|
+
emptyMessage="No products in this category yet."
|
|
16
|
+
renderCard={(p) => <StoreProductCard product={p} />}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
6
|
+
import {
|
|
7
|
+
getServerClient,
|
|
8
|
+
tags,
|
|
9
|
+
type Category,
|
|
10
|
+
type Product,
|
|
11
|
+
} from "@cimplify/sdk/server";
|
|
12
|
+
import { ListingClient } from "./listing-client";
|
|
13
|
+
|
|
14
|
+
interface CategoryData {
|
|
15
|
+
category: Category;
|
|
16
|
+
products: Product[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getCategory(slug: string): Promise<CategoryData | null> {
|
|
20
|
+
"use cache";
|
|
21
|
+
cacheTag(tags.categories());
|
|
22
|
+
cacheLife("hours");
|
|
23
|
+
|
|
24
|
+
const client = getServerClient();
|
|
25
|
+
const catRes = await client.catalogue.getCategoryBySlug(slug);
|
|
26
|
+
if (!catRes.ok) return null;
|
|
27
|
+
|
|
28
|
+
cacheTag(tags.category(catRes.value.id), tags.categoryProducts(catRes.value.id));
|
|
29
|
+
const r = await client.catalogue.getCategoryProducts(catRes.value.id);
|
|
30
|
+
const products = r.ok
|
|
31
|
+
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
32
|
+
: [];
|
|
33
|
+
return { category: catRes.value, products };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function generateMetadata({
|
|
37
|
+
params,
|
|
38
|
+
}: {
|
|
39
|
+
params: Promise<{ slug: string }>;
|
|
40
|
+
}): Promise<Metadata> {
|
|
41
|
+
const { slug } = await params;
|
|
42
|
+
const data = await getCategory(slug);
|
|
43
|
+
if (!data) return {};
|
|
44
|
+
return {
|
|
45
|
+
title: `${data.category.name} — Akua's Bakery`,
|
|
46
|
+
description: data.category.description ?? undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default async function CategoryPage({
|
|
51
|
+
params,
|
|
52
|
+
}: {
|
|
53
|
+
params: Promise<{ slug: string }>;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<Suspense fallback={<CategorySkeleton />}>
|
|
57
|
+
<CategoryContent params={params} />
|
|
58
|
+
</Suspense>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function CategoryContent({
|
|
63
|
+
params,
|
|
64
|
+
}: {
|
|
65
|
+
params: Promise<{ slug: string }>;
|
|
66
|
+
}) {
|
|
67
|
+
const { slug } = await params;
|
|
68
|
+
const data = await getCategory(slug);
|
|
69
|
+
if (!data) notFound();
|
|
70
|
+
|
|
71
|
+
const { category, products } = data;
|
|
72
|
+
return (
|
|
73
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
74
|
+
<header className="mb-8 text-center">
|
|
75
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
76
|
+
Category
|
|
77
|
+
</p>
|
|
78
|
+
<h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">
|
|
79
|
+
{category.name}
|
|
80
|
+
</h1>
|
|
81
|
+
{category.description && (
|
|
82
|
+
<p className="mx-auto mb-2 max-w-xl text-muted-foreground">
|
|
83
|
+
{category.description}
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
<p className="text-sm text-muted-foreground">
|
|
87
|
+
{products.length} item{products.length === 1 ? "" : "s"}
|
|
88
|
+
</p>
|
|
89
|
+
</header>
|
|
90
|
+
<ListingClient products={products} />
|
|
91
|
+
{products.length === 0 && (
|
|
92
|
+
<p className="text-center mt-8">
|
|
93
|
+
<Link href="/" className="text-primary font-semibold">
|
|
94
|
+
← Back home
|
|
95
|
+
</Link>
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function CategorySkeleton() {
|
|
103
|
+
return (
|
|
104
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
105
|
+
<header className="mb-8 text-center">
|
|
106
|
+
<div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />
|
|
107
|
+
<div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />
|
|
108
|
+
<div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />
|
|
109
|
+
</header>
|
|
110
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
111
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
112
|
+
<div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { CheckoutPage as SdkCheckoutPage } from "@cimplify/sdk/react";
|
|
5
|
+
|
|
6
|
+
export default function CheckoutPage() {
|
|
7
|
+
const router = useRouter();
|
|
8
|
+
return (
|
|
9
|
+
<SdkCheckoutPage
|
|
10
|
+
onComplete={(result) => {
|
|
11
|
+
if (result.success && result.order) {
|
|
12
|
+
router.push(`/orders/${result.order.id}`);
|
|
13
|
+
}
|
|
14
|
+
}}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ProductGrid } from "@cimplify/sdk/react";
|
|
4
|
+
import type { Product } from "@cimplify/sdk";
|
|
5
|
+
import { StoreProductCard } from "@/components/store-product-card";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Client island for the collection listing. Receives server-fetched
|
|
9
|
+
* products as props (serializable) and owns the `renderCard` function
|
|
10
|
+
* (which can't cross the server/client boundary).
|
|
11
|
+
*/
|
|
12
|
+
export function ListingClient({ products }: { products: Product[] }) {
|
|
13
|
+
return (
|
|
14
|
+
<ProductGrid
|
|
15
|
+
products={products}
|
|
16
|
+
emptyMessage="No products in this collection yet."
|
|
17
|
+
renderCard={(p) => <StoreProductCard product={p} />}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { notFound } from "next/navigation";
|
|
5
|
+
import { cacheTag, cacheLife } from "next/cache";
|
|
6
|
+
import {
|
|
7
|
+
getServerClient,
|
|
8
|
+
tags,
|
|
9
|
+
type Collection,
|
|
10
|
+
type Product,
|
|
11
|
+
} from "@cimplify/sdk/server";
|
|
12
|
+
import { ListingClient } from "./listing-client";
|
|
13
|
+
|
|
14
|
+
interface CollectionData {
|
|
15
|
+
collection: Collection;
|
|
16
|
+
products: Product[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getCollection(slug: string): Promise<CollectionData | null> {
|
|
20
|
+
"use cache";
|
|
21
|
+
cacheTag(tags.collections());
|
|
22
|
+
cacheLife("hours");
|
|
23
|
+
|
|
24
|
+
const client = getServerClient();
|
|
25
|
+
const colRes = await client.catalogue.getCollectionBySlug(slug);
|
|
26
|
+
if (!colRes.ok) return null;
|
|
27
|
+
|
|
28
|
+
cacheTag(tags.collection(colRes.value.id), tags.collectionProducts(colRes.value.id));
|
|
29
|
+
const r = await client.catalogue.getCollectionProducts(colRes.value.id);
|
|
30
|
+
const products = r.ok
|
|
31
|
+
? ((r.value as { items?: Product[] }).items ?? (r.value as Product[]))
|
|
32
|
+
: [];
|
|
33
|
+
return { collection: colRes.value, products };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function generateMetadata({
|
|
37
|
+
params,
|
|
38
|
+
}: {
|
|
39
|
+
params: Promise<{ slug: string }>;
|
|
40
|
+
}): Promise<Metadata> {
|
|
41
|
+
const { slug } = await params;
|
|
42
|
+
const data = await getCollection(slug);
|
|
43
|
+
if (!data) return {};
|
|
44
|
+
return {
|
|
45
|
+
title: `${data.collection.name} — Akua's Bakery`,
|
|
46
|
+
description: data.collection.description ?? undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default async function CollectionPage({
|
|
51
|
+
params,
|
|
52
|
+
}: {
|
|
53
|
+
params: Promise<{ slug: string }>;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<Suspense fallback={<CollectionSkeleton />}>
|
|
57
|
+
<CollectionContent params={params} />
|
|
58
|
+
</Suspense>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function CollectionContent({
|
|
63
|
+
params,
|
|
64
|
+
}: {
|
|
65
|
+
params: Promise<{ slug: string }>;
|
|
66
|
+
}) {
|
|
67
|
+
const { slug } = await params;
|
|
68
|
+
const data = await getCollection(slug);
|
|
69
|
+
if (!data) notFound();
|
|
70
|
+
|
|
71
|
+
const { collection, products } = data;
|
|
72
|
+
return (
|
|
73
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
74
|
+
<header className="mb-8 text-center">
|
|
75
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
76
|
+
Collection
|
|
77
|
+
</p>
|
|
78
|
+
<h1 className="font-serif text-[clamp(2rem,4vw,2.75rem)] font-semibold mb-2">
|
|
79
|
+
{collection.name}
|
|
80
|
+
</h1>
|
|
81
|
+
{collection.description && (
|
|
82
|
+
<p className="mx-auto mb-2 max-w-xl text-muted-foreground">
|
|
83
|
+
{collection.description}
|
|
84
|
+
</p>
|
|
85
|
+
)}
|
|
86
|
+
<p className="text-sm text-muted-foreground">
|
|
87
|
+
{products.length} item{products.length === 1 ? "" : "s"}
|
|
88
|
+
</p>
|
|
89
|
+
</header>
|
|
90
|
+
<ListingClient products={products} />
|
|
91
|
+
{products.length === 0 && (
|
|
92
|
+
<p className="text-center mt-8">
|
|
93
|
+
<Link href="/" className="text-primary font-semibold">
|
|
94
|
+
← Back home
|
|
95
|
+
</Link>
|
|
96
|
+
</p>
|
|
97
|
+
)}
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function CollectionSkeleton() {
|
|
103
|
+
return (
|
|
104
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
105
|
+
<header className="mb-8 text-center">
|
|
106
|
+
<div className="mx-auto h-3 w-20 bg-muted rounded mb-2 animate-pulse" />
|
|
107
|
+
<div className="mx-auto h-10 w-64 bg-muted rounded mb-2 animate-pulse" />
|
|
108
|
+
<div className="mx-auto h-4 w-80 bg-muted rounded animate-pulse" />
|
|
109
|
+
</header>
|
|
110
|
+
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
111
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
112
|
+
<div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: "Support — Currents Electronics",
|
|
5
|
+
description:
|
|
6
|
+
"Shipping, warranty, repairs, returns, payment — answers to the questions we hear most often.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
interface QA {
|
|
10
|
+
q: string;
|
|
11
|
+
a: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SECTIONS: { title: string; items: QA[] }[] = [
|
|
15
|
+
{
|
|
16
|
+
title: "Ordering & delivery",
|
|
17
|
+
items: [
|
|
18
|
+
{
|
|
19
|
+
q: "How fast is shipping?",
|
|
20
|
+
a: "Free same-day delivery within Greater Accra for orders placed before 4pm. Outside Accra, we use a partner courier — usually 1–3 business days, with tracking sent by SMS.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
q: "Can I pick up in store?",
|
|
24
|
+
a: "Yes. Choose 'Pickup' at checkout and we'll have your order boxed at our Atomic Junction store within 30 minutes. Bring a photo ID matching the order name.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
q: "Do you ship internationally?",
|
|
28
|
+
a: "We ship across West Africa via DHL — rates and ETAs are calculated at checkout once you enter your destination.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
q: "How do I track my order?",
|
|
32
|
+
a: "You'll get an SMS and email with a tracking link within 30 minutes of dispatch. Same-day Accra orders include the rider's phone number.",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: "Warranty & repairs",
|
|
38
|
+
items: [
|
|
39
|
+
{
|
|
40
|
+
q: "What does the two-year warranty cover?",
|
|
41
|
+
a: "Manufacturing defects, electrical failures, and battery degradation below 80% within the first 24 months. Accidental damage, water damage, and software issues caused by user installs are excluded.",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
q: "Where do I send a faulty device?",
|
|
45
|
+
a: "Drop it at our Atomic Junction store, or request a free pickup from your address by emailing support@currentselectronics.test. Loaner devices are available for warranty claims while we diagnose.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
q: "Can you fix devices bought elsewhere?",
|
|
49
|
+
a: "We offer paid repairs on Apple and Samsung devices regardless of where they were purchased. Expect a quote within 48 hours of inspection.",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: "Trade-in & financing",
|
|
55
|
+
items: [
|
|
56
|
+
{
|
|
57
|
+
q: "How does trade-in work?",
|
|
58
|
+
a: "Tell us the model and condition of your device, get an instant estimate, and ship it free or drop it at our store. Once we verify, we credit your account or pay out via Mobile Money — your choice.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
q: "Do you offer instalment payments?",
|
|
62
|
+
a: "Yes. Pay in 3 (interest-free) or 6/12 monthly instalments via our partner Fidelity Bank. Available at checkout for orders over GH₵2,000.",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
q: "Are there bulk discounts for businesses?",
|
|
66
|
+
a: "Yes. Email business@currentselectronics.test with your shopping list and team size — most orders over 5 units qualify for a discount, plus free on-site setup.",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
title: "Payment & returns",
|
|
72
|
+
items: [
|
|
73
|
+
{
|
|
74
|
+
q: "What payment methods do you accept?",
|
|
75
|
+
a: "Mobile Money (MTN, Telecel, AirtelTigo), Visa/Mastercard, instalments via Fidelity Bank, and cash on delivery for Accra orders.",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
q: "What's your return policy?",
|
|
79
|
+
a: "14 days, no questions asked, on any sealed product. For opened items, returns are accepted within 7 days subject to a 10% restocking fee. We can't accept returns on activated SIM-bundled phones once the SIM is registered.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
q: "Are products genuine?",
|
|
83
|
+
a: "Yes. We're an authorised dealer for every brand we stock. Every product ships with the manufacturer's serial number registered to your name — verify it on the brand's official portal anytime.",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
export default function FaqPage() {
|
|
90
|
+
return (
|
|
91
|
+
<article className="max-w-3xl mx-auto px-8 py-16">
|
|
92
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
|
|
93
|
+
Support
|
|
94
|
+
</p>
|
|
95
|
+
<h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-10 -tracking-[0.025em]">
|
|
96
|
+
Questions, answered.
|
|
97
|
+
</h1>
|
|
98
|
+
<div className="space-y-12">
|
|
99
|
+
{SECTIONS.map((section) => (
|
|
100
|
+
<section key={section.title}>
|
|
101
|
+
<h2 className="text-2xl font-semibold mb-5 -tracking-[0.02em]">
|
|
102
|
+
{section.title}
|
|
103
|
+
</h2>
|
|
104
|
+
<dl className="space-y-6">
|
|
105
|
+
{section.items.map((item) => (
|
|
106
|
+
<div key={item.q}>
|
|
107
|
+
<dt className="font-semibold text-foreground mb-1.5">{item.q}</dt>
|
|
108
|
+
<dd className="text-muted-foreground leading-relaxed">{item.a}</dd>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</dl>
|
|
112
|
+
</section>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
<p className="mt-12 pt-8 border-t border-border text-sm text-muted-foreground">
|
|
116
|
+
Still stuck? Email{" "}
|
|
117
|
+
<a
|
|
118
|
+
href="mailto:support@currentselectronics.test"
|
|
119
|
+
className="text-primary font-semibold hover:underline"
|
|
120
|
+
>
|
|
121
|
+
support@currentselectronics.test
|
|
122
|
+
</a>{" "}
|
|
123
|
+
and a real human will reply within 24 hours.
|
|
124
|
+
</p>
|
|
125
|
+
</article>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* Scan the SDK's bundled JS so utility classes used inside its components
|
|
4
|
+
land in the compiled stylesheet alongside our own. */
|
|
5
|
+
@source "../node_modules/@cimplify/sdk/dist";
|
|
6
|
+
|
|
7
|
+
/* Currents Electronics palette — cool navy chrome, electric blue accent.
|
|
8
|
+
Modern retail aesthetic; tweak these tokens to retheme everything. */
|
|
9
|
+
@theme {
|
|
10
|
+
--color-background: oklch(0.99 0.005 240);
|
|
11
|
+
--color-foreground: oklch(0.2 0.03 250);
|
|
12
|
+
--color-card: oklch(1 0 0);
|
|
13
|
+
--color-card-foreground: oklch(0.2 0.03 250);
|
|
14
|
+
--color-popover: oklch(1 0 0);
|
|
15
|
+
--color-popover-foreground: oklch(0.2 0.03 250);
|
|
16
|
+
--color-primary: oklch(0.55 0.22 255);
|
|
17
|
+
--color-primary-foreground: oklch(0.99 0 0);
|
|
18
|
+
--color-secondary: oklch(0.95 0.02 250);
|
|
19
|
+
--color-secondary-foreground: oklch(0.25 0.05 250);
|
|
20
|
+
--color-muted: oklch(0.96 0.01 250);
|
|
21
|
+
--color-muted-foreground: oklch(0.5 0.03 250);
|
|
22
|
+
--color-accent: oklch(0.92 0.07 240);
|
|
23
|
+
--color-accent-foreground: oklch(0.25 0.1 250);
|
|
24
|
+
--color-destructive: oklch(0.58 0.24 27);
|
|
25
|
+
--color-destructive-foreground: oklch(0.99 0 0);
|
|
26
|
+
--color-border: oklch(0.92 0.01 250);
|
|
27
|
+
--color-input: oklch(0.94 0.01 250);
|
|
28
|
+
--color-ring: oklch(0.55 0.22 255);
|
|
29
|
+
--radius: 0.5rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@layer base {
|
|
33
|
+
html, body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 0;
|
|
36
|
+
background: var(--color-background);
|
|
37
|
+
color: var(--color-foreground);
|
|
38
|
+
-webkit-font-smoothing: antialiased;
|
|
39
|
+
-moz-osx-font-smoothing: grayscale;
|
|
40
|
+
}
|
|
41
|
+
a { color: inherit; text-decoration: none; }
|
|
42
|
+
h1, h2, h3, h4 {
|
|
43
|
+
font-family: var(--font-sans);
|
|
44
|
+
letter-spacing: -0.02em;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter, JetBrains_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
import { Providers } from "@/components/providers";
|
|
5
|
+
import { Header } from "@/components/header";
|
|
6
|
+
import { Footer } from "@/components/footer";
|
|
7
|
+
import { ProductModal } from "@/components/product-modal";
|
|
8
|
+
import { Suspense } from "react";
|
|
9
|
+
|
|
10
|
+
const inter = Inter({
|
|
11
|
+
subsets: ["latin"],
|
|
12
|
+
variable: "--font-sans",
|
|
13
|
+
display: "swap",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const mono = JetBrains_Mono({
|
|
17
|
+
subsets: ["latin"],
|
|
18
|
+
variable: "--font-mono",
|
|
19
|
+
display: "swap",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const metadata: Metadata = {
|
|
23
|
+
title: "Currents Electronics",
|
|
24
|
+
description:
|
|
25
|
+
"The latest laptops, phones, audio, and accessories — curated, in-stock, shipped from Accra to Africa.",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
29
|
+
return (
|
|
30
|
+
<html lang="en" className={`${inter.variable} ${mono.variable}`}>
|
|
31
|
+
<body
|
|
32
|
+
suppressHydrationWarning
|
|
33
|
+
className="min-h-screen flex flex-col bg-background text-foreground font-sans"
|
|
34
|
+
>
|
|
35
|
+
<Providers>
|
|
36
|
+
<Header />
|
|
37
|
+
<main className="flex-1 pb-12 w-full">{children}</main>
|
|
38
|
+
<Footer />
|
|
39
|
+
<Suspense fallback={null}>
|
|
40
|
+
<ProductModal />
|
|
41
|
+
</Suspense>
|
|
42
|
+
</Providers>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
);
|
|
46
|
+
}
|