@cimplify/sdk 0.39.1 → 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 → 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/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,117 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: "Terms of Service — Currents Electronics",
|
|
5
|
+
description: "The rules of the road for ordering from Currents Electronics.",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function TermsPage() {
|
|
9
|
+
return (
|
|
10
|
+
<article className="max-w-3xl mx-auto px-8 py-16 prose prose-lg max-w-none">
|
|
11
|
+
<p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2 not-prose">
|
|
12
|
+
Terms of service
|
|
13
|
+
</p>
|
|
14
|
+
<h1 className="text-[clamp(2.25rem,5vw,3.5rem)] font-bold mb-2 -tracking-[0.025em]">
|
|
15
|
+
Terms of Service
|
|
16
|
+
</h1>
|
|
17
|
+
<p className="text-sm text-muted-foreground not-prose mb-10">
|
|
18
|
+
Last updated: 1 May 2026
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<section className="space-y-5 leading-relaxed text-foreground/90">
|
|
22
|
+
<h2 className="text-2xl font-semibold mt-0 -tracking-[0.02em]">
|
|
23
|
+
1. Who we are
|
|
24
|
+
</h2>
|
|
25
|
+
<p>
|
|
26
|
+
Currents Electronics ("we", "us") is a consumer
|
|
27
|
+
electronics retailer based at Atomic Junction, East Legon, Accra,
|
|
28
|
+
Ghana. By placing an order, you ("you", "customer")
|
|
29
|
+
agree to these terms.
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
<h2 className="text-2xl font-semibold -tracking-[0.02em]">
|
|
33
|
+
2. Authorised dealer status
|
|
34
|
+
</h2>
|
|
35
|
+
<p>
|
|
36
|
+
We are authorised dealers for the brands listed on our About page.
|
|
37
|
+
Every product is sourced direct from the manufacturer or a certified
|
|
38
|
+
distributor. Serial numbers can be verified on the brand's
|
|
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>
|
|
114
|
+
</section>
|
|
115
|
+
</article>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useCartCount } from "@/lib/cart";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cart pill — dynamic island. Reads the live cart count via the SDK and
|
|
8
|
+
* renders a link to `/cart`. Wrap in `<Suspense fallback={<CartPillSkeleton/>}>`
|
|
9
|
+
* so the cached header chrome streams without blocking on the cart fetch.
|
|
10
|
+
*/
|
|
11
|
+
export function CartPill() {
|
|
12
|
+
const { count } = useCartCount();
|
|
13
|
+
return (
|
|
14
|
+
<Link
|
|
15
|
+
href="/cart"
|
|
16
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground text-background text-xs font-semibold tracking-wide transition-transform hover:scale-105"
|
|
17
|
+
>
|
|
18
|
+
Cart · {count}
|
|
19
|
+
</Link>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CartPillSkeleton() {
|
|
24
|
+
return (
|
|
25
|
+
<span
|
|
26
|
+
aria-hidden
|
|
27
|
+
className="inline-flex items-center gap-1.5 px-3.5 py-2 rounded-full bg-foreground/80 text-background text-xs font-semibold tracking-wide"
|
|
28
|
+
>
|
|
29
|
+
Cart · …
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { CategoryGrid as SdkCategoryGrid } from "@cimplify/sdk/react";
|
|
5
|
+
import type { Category } from "@cimplify/sdk";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Homepage category tiles — defers to the SDK's <CategoryGrid/> for layout
|
|
9
|
+
* and accessibility, and routes selections to /categories/:slug.
|
|
10
|
+
*/
|
|
11
|
+
export function CategoryGrid({ categories }: { categories?: Category[] }) {
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
return (
|
|
14
|
+
<section className="max-w-7xl mx-auto px-8 pt-14">
|
|
15
|
+
<h2 className="font-serif text-[28px] font-semibold m-0 mb-5">Browse the menu</h2>
|
|
16
|
+
<SdkCategoryGrid
|
|
17
|
+
categories={categories}
|
|
18
|
+
onSelect={(c) => router.push(`/categories/${c.slug}`)}
|
|
19
|
+
classNames={{
|
|
20
|
+
item: "flex flex-col gap-1 p-6 bg-card border border-border rounded-2xl cursor-pointer text-left transition-all hover:border-primary hover:-translate-y-0.5",
|
|
21
|
+
name: "font-serif text-lg font-semibold text-foreground",
|
|
22
|
+
description: "text-xs text-muted-foreground",
|
|
23
|
+
count: "text-xs text-muted-foreground mt-1",
|
|
24
|
+
}}
|
|
25
|
+
/>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import type { Collection, Product } from "@cimplify/sdk";
|
|
3
|
+
import { StoreProductCard } from "./store-product-card";
|
|
4
|
+
|
|
5
|
+
interface CollectionStripProps {
|
|
6
|
+
collection: Collection;
|
|
7
|
+
products: Product[];
|
|
8
|
+
collectionHref?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Horizontal strip of products under a collection title. Cards come from the
|
|
13
|
+
* SDK so every variant (Food, Bundle, Composite, Service, …) renders
|
|
14
|
+
* correctly; clicks open the shared URL-driven product modal.
|
|
15
|
+
*/
|
|
16
|
+
export function CollectionStrip({ collection, products, collectionHref }: CollectionStripProps) {
|
|
17
|
+
if (products.length === 0) return null;
|
|
18
|
+
return (
|
|
19
|
+
<section className="max-w-7xl mx-auto px-8 pt-12">
|
|
20
|
+
<header className="grid grid-cols-[1fr_auto] items-end gap-4 mb-5">
|
|
21
|
+
<h2 className="font-serif text-[28px] font-semibold m-0">{collection.name}</h2>
|
|
22
|
+
{collectionHref && (
|
|
23
|
+
<Link
|
|
24
|
+
href={collectionHref}
|
|
25
|
+
className="text-[13px] font-semibold text-primary hover:underline"
|
|
26
|
+
>
|
|
27
|
+
See all →
|
|
28
|
+
</Link>
|
|
29
|
+
)}
|
|
30
|
+
{collection.description && (
|
|
31
|
+
<p className="col-span-full m-0 mt-1 text-sm text-muted-foreground">
|
|
32
|
+
{collection.description}
|
|
33
|
+
</p>
|
|
34
|
+
)}
|
|
35
|
+
</header>
|
|
36
|
+
<div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto snap-x snap-mandatory pb-2">
|
|
37
|
+
{products.slice(0, 8).map((p) => (
|
|
38
|
+
<div key={p.id} className="snap-start">
|
|
39
|
+
<StoreProductCard product={p} />
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
</section>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
const SOCIALS: { label: string; href: string; svg: React.ReactNode }[] = [
|
|
4
|
+
{
|
|
5
|
+
label: "Instagram",
|
|
6
|
+
href: "https://instagram.com/currentselectronics",
|
|
7
|
+
svg: (
|
|
8
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" aria-hidden className="w-5 h-5">
|
|
9
|
+
<rect x="3" y="3" width="18" height="18" rx="5" />
|
|
10
|
+
<circle cx="12" cy="12" r="4" />
|
|
11
|
+
<circle cx="17.5" cy="6.5" r="0.75" fill="currentColor" />
|
|
12
|
+
</svg>
|
|
13
|
+
),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "X",
|
|
17
|
+
href: "https://x.com/currentselectronics",
|
|
18
|
+
svg: (
|
|
19
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
|
|
20
|
+
<path d="M18 2h3l-7.5 8.6L22 22h-6.6l-5-6.5L4 22H1l8-9.2L1.4 2H8l4.6 6 5.4-6z" />
|
|
21
|
+
</svg>
|
|
22
|
+
),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: "YouTube",
|
|
26
|
+
href: "https://youtube.com/@currentselectronics",
|
|
27
|
+
svg: (
|
|
28
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
|
|
29
|
+
<path d="M22.5 6.5a2.6 2.6 0 0 0-1.8-1.8C19 4.2 12 4.2 12 4.2s-7 0-8.7.5A2.6 2.6 0 0 0 1.5 6.5C1 8.2 1 12 1 12s0 3.8.5 5.5a2.6 2.6 0 0 0 1.8 1.8C5 19.8 12 19.8 12 19.8s7 0 8.7-.5a2.6 2.6 0 0 0 1.8-1.8c.5-1.7.5-5.5.5-5.5s0-3.8-.5-5.5zM10 15.5v-7l6 3.5-6 3.5z" />
|
|
30
|
+
</svg>
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "WhatsApp",
|
|
35
|
+
href: "https://wa.me/233244000000",
|
|
36
|
+
svg: (
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className="w-5 h-5">
|
|
38
|
+
<path d="M12 2a10 10 0 0 0-8.6 15l-1.4 5 5.2-1.4A10 10 0 1 0 12 2zm5 14.2c-.2.6-1.2 1.2-1.7 1.2-.5.1-1.1.1-1.7-.1-.4-.1-.9-.3-1.5-.6a8.4 8.4 0 0 1-3.7-3.4c-.7-1-1-1.8-1-2.5 0-.7.4-1.1.6-1.3.2-.2.4-.2.5-.2h.4c.1 0 .3 0 .4.3l.6 1.4c.1.2 0 .3 0 .4l-.3.4-.3.3c-.1.1-.2.2-.1.4.2.4.7 1.1 1.4 1.8.9.8 1.7 1.1 1.9 1.2.2.1.3.1.5-.1l.6-.7c.2-.2.3-.2.5-.1l1.4.7c.2.1.3.2.4.3.1.2.1.6 0 1z" />
|
|
39
|
+
</svg>
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const SITEMAP: { title: string; links: { label: string; href: string }[] }[] = [
|
|
45
|
+
{
|
|
46
|
+
title: "Shop",
|
|
47
|
+
links: [
|
|
48
|
+
{ label: "All products", href: "/shop" },
|
|
49
|
+
{ label: "Laptops", href: "/categories/laptops" },
|
|
50
|
+
{ label: "Phones", href: "/categories/phones" },
|
|
51
|
+
{ label: "Audio", href: "/categories/audio" },
|
|
52
|
+
{ label: "Accessories", href: "/categories/accessories" },
|
|
53
|
+
{ label: "Deals", href: "/categories/deals" },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: "Support",
|
|
58
|
+
links: [
|
|
59
|
+
{ label: "Contact", href: "mailto:hello@currentselectronics.test" },
|
|
60
|
+
{ label: "Warranty", href: "/faq#warranty--repairs" },
|
|
61
|
+
{ label: "Returns", href: "/faq#payment--returns" },
|
|
62
|
+
{ label: "Shipping", href: "/faq#ordering--delivery" },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: "Company",
|
|
67
|
+
links: [
|
|
68
|
+
{ label: "About", href: "/about" },
|
|
69
|
+
{ label: "Trade-in", href: "/about" },
|
|
70
|
+
{ label: "Business sales", href: "mailto:business@currentselectronics.test" },
|
|
71
|
+
{ label: "Careers", href: "/about" },
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Legal",
|
|
76
|
+
links: [
|
|
77
|
+
{ label: "Terms of Service", href: "/terms" },
|
|
78
|
+
{ label: "Privacy Policy", href: "/privacy" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
export async function Footer() {
|
|
84
|
+
"use cache";
|
|
85
|
+
const year = new Date().getFullYear();
|
|
86
|
+
return (
|
|
87
|
+
<footer className="mt-16 border-t border-border bg-foreground text-background/80 text-sm">
|
|
88
|
+
<div className="max-w-7xl mx-auto px-6 sm:px-8 pt-12 pb-8">
|
|
89
|
+
<div className="grid gap-10 md:grid-cols-[1.4fr_repeat(4,1fr)]">
|
|
90
|
+
<div>
|
|
91
|
+
<div className="flex items-center gap-2.5 mb-4">
|
|
92
|
+
<span className="grid place-items-center w-8 h-8 rounded-md bg-primary text-primary-foreground text-[13px] font-bold font-mono">
|
|
93
|
+
C
|
|
94
|
+
</span>
|
|
95
|
+
<span className="text-background text-lg font-bold -tracking-[0.025em]">
|
|
96
|
+
Currents Electronics
|
|
97
|
+
</span>
|
|
98
|
+
</div>
|
|
99
|
+
<p className="leading-relaxed mb-4 max-w-sm">
|
|
100
|
+
Authorised dealer for Apple, Samsung, Sony, Bose. Same-day
|
|
101
|
+
delivery in Accra; nationwide via partner courier. Two-year
|
|
102
|
+
warranty on every product.
|
|
103
|
+
</p>
|
|
104
|
+
<address className="not-italic space-y-1">
|
|
105
|
+
<p className="m-0">Atomic Junction, East Legon, Accra</p>
|
|
106
|
+
<p className="m-0">
|
|
107
|
+
<a href="tel:+233244000000" className="hover:text-background transition-colors">
|
|
108
|
+
+233 244 000 000
|
|
109
|
+
</a>
|
|
110
|
+
</p>
|
|
111
|
+
<p className="m-0">
|
|
112
|
+
<a
|
|
113
|
+
href="mailto:hello@currentselectronics.test"
|
|
114
|
+
className="hover:text-background transition-colors"
|
|
115
|
+
>
|
|
116
|
+
hello@currentselectronics.test
|
|
117
|
+
</a>
|
|
118
|
+
</p>
|
|
119
|
+
<p className="m-0 text-xs">Mon–Sat · 9am–8pm</p>
|
|
120
|
+
</address>
|
|
121
|
+
<div className="flex items-center gap-3 mt-5">
|
|
122
|
+
{SOCIALS.map((s) => (
|
|
123
|
+
<a
|
|
124
|
+
key={s.label}
|
|
125
|
+
href={s.href}
|
|
126
|
+
aria-label={s.label}
|
|
127
|
+
target="_blank"
|
|
128
|
+
rel="noopener noreferrer"
|
|
129
|
+
className="inline-flex items-center justify-center w-9 h-9 rounded-md border border-background/20 hover:bg-primary hover:border-primary transition-colors"
|
|
130
|
+
>
|
|
131
|
+
{s.svg}
|
|
132
|
+
</a>
|
|
133
|
+
))}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{SITEMAP.map((section) => (
|
|
137
|
+
<nav key={section.title} aria-labelledby={`footer-${section.title}`}>
|
|
138
|
+
<p
|
|
139
|
+
id={`footer-${section.title}`}
|
|
140
|
+
className="text-background font-mono mb-3 text-[11px] uppercase tracking-[0.12em]"
|
|
141
|
+
>
|
|
142
|
+
{section.title}
|
|
143
|
+
</p>
|
|
144
|
+
<ul className="space-y-2 m-0 p-0 list-none">
|
|
145
|
+
{section.links.map((link) => (
|
|
146
|
+
<li key={link.label}>
|
|
147
|
+
<Link
|
|
148
|
+
href={link.href}
|
|
149
|
+
className="hover:text-background transition-colors"
|
|
150
|
+
>
|
|
151
|
+
{link.label}
|
|
152
|
+
</Link>
|
|
153
|
+
</li>
|
|
154
|
+
))}
|
|
155
|
+
</ul>
|
|
156
|
+
</nav>
|
|
157
|
+
))}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div className="mt-12 pt-6 border-t border-background/15 flex flex-col sm:flex-row items-center justify-between gap-3 text-xs">
|
|
161
|
+
<p className="m-0">© {year} Currents Electronics. All rights reserved.</p>
|
|
162
|
+
<p className="m-0 inline-flex items-center gap-1.5">
|
|
163
|
+
<span className="opacity-70">Powered by</span>
|
|
164
|
+
<a
|
|
165
|
+
href="https://app.cimplify.io"
|
|
166
|
+
target="_blank"
|
|
167
|
+
rel="noopener noreferrer"
|
|
168
|
+
aria-label="Cimplify"
|
|
169
|
+
className="inline-flex items-center gap-1 text-background hover:text-primary transition-colors"
|
|
170
|
+
>
|
|
171
|
+
<span className="font-semibold tracking-tight">Cimplify</span>
|
|
172
|
+
<svg
|
|
173
|
+
viewBox="0 0 12 12"
|
|
174
|
+
aria-hidden
|
|
175
|
+
className="w-3 h-3 opacity-70"
|
|
176
|
+
fill="none"
|
|
177
|
+
stroke="currentColor"
|
|
178
|
+
strokeWidth="1.5"
|
|
179
|
+
>
|
|
180
|
+
<path d="M3 9L9 3M9 3H4M9 3v5" strokeLinecap="round" strokeLinejoin="round" />
|
|
181
|
+
</svg>
|
|
182
|
+
</a>
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
</footer>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
import { NavLink } from "./nav-link";
|
|
4
|
+
import { CartPill, CartPillSkeleton } from "./cart-pill";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Server-rendered header chrome. Brand mark + nav layout streams from the
|
|
8
|
+
* cache; the active-link styling and live cart count are dynamic islands
|
|
9
|
+
* mounted in their own Suspense boundaries so the chrome never blocks.
|
|
10
|
+
*/
|
|
11
|
+
export function Header() {
|
|
12
|
+
return (
|
|
13
|
+
<header className="sticky top-0 z-30 flex items-center justify-between px-6 sm:px-8 py-3.5 border-b border-border bg-background/90 backdrop-blur-md">
|
|
14
|
+
<Link href="/" className="flex items-center gap-2.5 group">
|
|
15
|
+
<span className="grid place-items-center w-8 h-8 rounded-md bg-foreground text-background text-[13px] font-bold font-mono group-hover:bg-primary transition-colors">
|
|
16
|
+
C
|
|
17
|
+
</span>
|
|
18
|
+
<span className="text-[18px] font-bold -tracking-[0.025em]">Currents</span>
|
|
19
|
+
<span className="hidden sm:inline text-[10px] font-mono uppercase tracking-[0.16em] text-muted-foreground border border-border rounded px-1.5 py-0.5">
|
|
20
|
+
ELECTRONICS
|
|
21
|
+
</span>
|
|
22
|
+
</Link>
|
|
23
|
+
<nav className="flex items-center gap-5 sm:gap-6">
|
|
24
|
+
<Suspense fallback={<NavLinkFallback>Shop</NavLinkFallback>}>
|
|
25
|
+
<NavLink href="/shop">Shop</NavLink>
|
|
26
|
+
</Suspense>
|
|
27
|
+
<Suspense fallback={<NavLinkFallback>Deals</NavLinkFallback>}>
|
|
28
|
+
<NavLink href="/categories/deals">Deals</NavLink>
|
|
29
|
+
</Suspense>
|
|
30
|
+
<Suspense fallback={<NavLinkFallback>Support</NavLinkFallback>}>
|
|
31
|
+
<NavLink href="/faq">Support</NavLink>
|
|
32
|
+
</Suspense>
|
|
33
|
+
<Suspense fallback={<CartPillSkeleton />}>
|
|
34
|
+
<CartPill />
|
|
35
|
+
</Suspense>
|
|
36
|
+
</nav>
|
|
37
|
+
</header>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function NavLinkFallback({ children }: { children: React.ReactNode }) {
|
|
42
|
+
return (
|
|
43
|
+
<span className="text-[13px] font-medium tracking-wide text-muted-foreground">
|
|
44
|
+
{children}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
interface HeroProps {
|
|
2
|
+
badge?: string;
|
|
3
|
+
title: string;
|
|
4
|
+
subtitle?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Hero({ badge, title, subtitle }: HeroProps) {
|
|
8
|
+
return (
|
|
9
|
+
<section className="relative px-8 py-20 text-center overflow-hidden bg-gradient-to-br from-foreground via-foreground to-primary text-background">
|
|
10
|
+
<div className="absolute inset-0 opacity-[0.06] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />
|
|
11
|
+
<div className="relative max-w-3xl mx-auto">
|
|
12
|
+
{badge && (
|
|
13
|
+
<span className="inline-block mb-5 px-3.5 py-1.5 rounded-full bg-primary/15 border border-primary/30 text-primary-foreground/90 text-[11px] font-semibold uppercase tracking-[0.16em] font-mono">
|
|
14
|
+
{badge}
|
|
15
|
+
</span>
|
|
16
|
+
)}
|
|
17
|
+
<h1 className="text-[clamp(2.5rem,6vw,4rem)] font-bold m-0 -tracking-[0.03em] leading-[1.05]">
|
|
18
|
+
{title}
|
|
19
|
+
</h1>
|
|
20
|
+
{subtitle && (
|
|
21
|
+
<p className="mx-auto mt-5 max-w-2xl text-base sm:text-lg text-background/80 leading-relaxed">
|
|
22
|
+
{subtitle}
|
|
23
|
+
</p>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { usePathname } from "next/navigation";
|
|
5
|
+
|
|
6
|
+
export function NavLink({ href, children }: { href: string; children: React.ReactNode }) {
|
|
7
|
+
const pathname = usePathname();
|
|
8
|
+
const active = pathname === href;
|
|
9
|
+
return (
|
|
10
|
+
<Link
|
|
11
|
+
href={href}
|
|
12
|
+
className={[
|
|
13
|
+
"text-[13px] font-medium tracking-wide transition-colors",
|
|
14
|
+
active ? "text-primary" : "text-muted-foreground hover:text-foreground",
|
|
15
|
+
].join(" ")}
|
|
16
|
+
>
|
|
17
|
+
{children}
|
|
18
|
+
</Link>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
|
6
|
+
import { ProductSheet, useProduct, useCart } from "@cimplify/sdk/react";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* URL-driven product modal. Reads `?product=<slug>` and renders the SDK's
|
|
10
|
+
* `<ProductSheet/>` — vertical layout with image-on-top, then header, then
|
|
11
|
+
* the variant/add-on/composite/bundle customizer. Closing the modal clears
|
|
12
|
+
* the search param. Deep-linkable and survives reloads.
|
|
13
|
+
*/
|
|
14
|
+
export function ProductModal() {
|
|
15
|
+
const router = useRouter();
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
const searchParams = useSearchParams();
|
|
18
|
+
const slug = searchParams?.get("product") ?? null;
|
|
19
|
+
|
|
20
|
+
const { product } = useProduct(slug ?? "", { enabled: Boolean(slug) });
|
|
21
|
+
const { addToCart } = useCart();
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!slug) return;
|
|
25
|
+
const original = document.body.style.overflow;
|
|
26
|
+
document.body.style.overflow = "hidden";
|
|
27
|
+
return () => {
|
|
28
|
+
document.body.style.overflow = original;
|
|
29
|
+
};
|
|
30
|
+
}, [slug]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!slug) return;
|
|
34
|
+
const onKey = (e: KeyboardEvent) => {
|
|
35
|
+
if (e.key === "Escape") close();
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener("keydown", onKey);
|
|
38
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [slug]);
|
|
41
|
+
|
|
42
|
+
if (!slug) return null;
|
|
43
|
+
|
|
44
|
+
function close() {
|
|
45
|
+
const next = new URLSearchParams(searchParams?.toString() ?? "");
|
|
46
|
+
next.delete("product");
|
|
47
|
+
const qs = next.toString();
|
|
48
|
+
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
role="dialog"
|
|
54
|
+
aria-modal="true"
|
|
55
|
+
onClick={close}
|
|
56
|
+
className="fixed inset-0 z-[100] flex items-end sm:items-center justify-center sm:p-6 bg-foreground/50 backdrop-blur-sm"
|
|
57
|
+
>
|
|
58
|
+
<div
|
|
59
|
+
onClick={(e) => e.stopPropagation()}
|
|
60
|
+
className="relative w-full sm:max-w-lg max-h-[92vh] overflow-auto bg-card sm:rounded-3xl rounded-t-3xl shadow-2xl"
|
|
61
|
+
>
|
|
62
|
+
<button
|
|
63
|
+
onClick={close}
|
|
64
|
+
aria-label="Close product details"
|
|
65
|
+
className="absolute top-4 right-4 z-10 w-9 h-9 rounded-full bg-card border border-border text-sm cursor-pointer transition-colors hover:bg-muted"
|
|
66
|
+
>
|
|
67
|
+
✕
|
|
68
|
+
</button>
|
|
69
|
+
{product ? (
|
|
70
|
+
<ProductSheet
|
|
71
|
+
product={product}
|
|
72
|
+
onClose={close}
|
|
73
|
+
onAddToCart={async (p, qty, options) => {
|
|
74
|
+
await addToCart(p.id, qty, options);
|
|
75
|
+
close();
|
|
76
|
+
router.push("/cart");
|
|
77
|
+
}}
|
|
78
|
+
renderImage={({ src, alt, className }) => (
|
|
79
|
+
<Image
|
|
80
|
+
src={src}
|
|
81
|
+
alt={alt}
|
|
82
|
+
width={1200}
|
|
83
|
+
height={900}
|
|
84
|
+
className={className}
|
|
85
|
+
style={{ width: "100%", height: "auto", objectFit: "cover" }}
|
|
86
|
+
priority
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
classNames={{
|
|
90
|
+
root: "p-6 sm:p-8 gap-4",
|
|
91
|
+
image: "rounded-2xl overflow-hidden -mx-6 sm:-mx-8 -mt-6 sm:-mt-8 mb-2",
|
|
92
|
+
header: "flex items-baseline justify-between gap-4",
|
|
93
|
+
name: "font-serif text-2xl font-semibold m-0",
|
|
94
|
+
price: "text-lg font-semibold text-primary",
|
|
95
|
+
description: "text-sm text-muted-foreground leading-relaxed",
|
|
96
|
+
customizer: "pt-2",
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
) : (
|
|
100
|
+
<div className="p-8 text-center text-muted-foreground">Loading…</div>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, type ReactNode } from "react";
|
|
4
|
+
import { createCimplifyClient } from "@cimplify/sdk";
|
|
5
|
+
import { CimplifyProvider } from "@cimplify/sdk/react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Boots the Cimplify SDK client once on the client-side and exposes it via
|
|
9
|
+
* <CimplifyProvider/>.
|
|
10
|
+
*
|
|
11
|
+
* Base-URL resolution:
|
|
12
|
+
* 1) If NEXT_PUBLIC_CIMPLIFY_API_URL is set, use it verbatim.
|
|
13
|
+
* 2) Otherwise use the current origin so requests flow through the
|
|
14
|
+
* Next.js rewrite in `next.config.ts` to the mock at :8787 (no CORS).
|
|
15
|
+
* 3) Fall back to 127.0.0.1:8787 only during SSR, when there's no window.
|
|
16
|
+
*/
|
|
17
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
18
|
+
const client = useMemo(() => {
|
|
19
|
+
const baseUrl =
|
|
20
|
+
process.env.NEXT_PUBLIC_CIMPLIFY_API_URL?.trim() ||
|
|
21
|
+
(typeof window !== "undefined" ? window.location.origin : "http://127.0.0.1:8787");
|
|
22
|
+
const publicKey = process.env.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY ?? "mock-dev";
|
|
23
|
+
return createCimplifyClient({
|
|
24
|
+
baseUrl,
|
|
25
|
+
publicKey,
|
|
26
|
+
suppressPublicKeyWarning: true,
|
|
27
|
+
});
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
return <CimplifyProvider client={client}>{children}</CimplifyProvider>;
|
|
31
|
+
}
|