@cimplify/sdk 0.40.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,48 +70,60 @@ async function CollectionContent({
70
70
 
71
71
  const { collection, products } = data;
72
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}
73
+ <>
74
+ <section className="bg-foreground text-background relative overflow-hidden">
75
+ <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />
76
+ <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">
77
+ <nav className="text-[12px] font-mono text-background/60 mb-3 flex items-center gap-2">
78
+ <Link href="/" className="hover:text-background transition-colors">Home</Link>
79
+ <span>/</span>
80
+ <span className="text-background/90">Collections</span>
81
+ <span>/</span>
82
+ <span className="text-background/90">{collection.name}</span>
83
+ </nav>
84
+ <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em]">
85
+ {collection.name}
86
+ </h1>
87
+ {collection.description && (
88
+ <p className="mt-3 max-w-2xl text-base text-background/75">
89
+ {collection.description}
90
+ </p>
91
+ )}
92
+ <p className="mt-4 text-[11px] font-mono uppercase tracking-[0.16em] text-background/60 tabular-nums">
93
+ {products.length} {products.length === 1 ? "product" : "products"}
94
+ </p>
95
+ </div>
96
+ </section>
97
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">
98
+ <ListingClient products={products} />
99
+ {products.length === 0 && (
100
+ <p className="text-center mt-8">
101
+ <Link href="/shop" className="text-primary font-semibold hover:underline">
102
+ ← Browse all products
103
+ </Link>
84
104
  </p>
85
105
  )}
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>
106
+ </section>
107
+ </>
99
108
  );
100
109
  }
101
110
 
102
111
  function CollectionSkeleton() {
103
112
  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>
113
+ <>
114
+ <section className="bg-foreground py-12 sm:py-14">
115
+ <div className="max-w-7xl mx-auto px-6 sm:px-8">
116
+ <div className="h-3 w-32 bg-background/20 rounded mb-3 animate-pulse" />
117
+ <div className="h-12 w-72 bg-background/20 rounded animate-pulse" />
118
+ </div>
119
+ </section>
120
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12">
121
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
122
+ {Array.from({ length: 8 }).map((_, i) => (
123
+ <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
124
+ ))}
125
+ </div>
126
+ </section>
127
+ </>
116
128
  );
117
129
  }
@@ -2,30 +2,44 @@ import type { Metadata } from "next";
2
2
  import { Suspense } from "react";
3
3
  import { cacheTag, cacheLife } from "next/cache";
4
4
  import { getServerClient, tags, type Product } from "@cimplify/sdk/server";
5
- import { Hero } from "@/components/hero";
5
+ import { FeatureHero } from "@/components/feature-hero";
6
+ import { CategoryTiles } from "@/components/category-tiles";
6
7
  import { CollectionStrip } from "@/components/collection-strip";
7
- import { CategoryGrid } from "@/components/category-grid";
8
+ import { PromoBanner } from "@/components/promo-banner";
9
+ import { TrustBar } from "@/components/trust-bar";
10
+ import { BrandMarquee } from "@/components/brand-marquee";
11
+ import { TradeInCta } from "@/components/trade-in-cta";
12
+ import { Newsletter } from "@/components/newsletter";
13
+ import { SectionHeading } from "@/components/section-heading";
14
+ import { StoreProductCard } from "@/components/store-product-card";
8
15
 
9
16
  export const metadata: Metadata = {
10
17
  title: "Currents Electronics — Tech, in stock, in Accra",
11
18
  description:
12
- "The latest laptops, phones, audio, and accessories — curated, in-stock, shipped same-day from Accra.",
19
+ "Authorised dealer for Apple, Samsung, Sony, Bose. Same-day delivery, two-year warranty, instalment payments — across Accra and West Africa.",
13
20
  };
14
21
 
22
+ interface CollectionWithProducts {
23
+ collection: { id: string; slug: string; name: string; description?: string };
24
+ products: Product[];
25
+ }
26
+
15
27
  async function getHomeData() {
16
28
  "use cache";
17
29
  cacheTag(tags.collections(), tags.categories(), tags.products());
18
30
  cacheLife("hours");
19
31
 
20
32
  const client = getServerClient();
21
- const [colRes, catRes] = await Promise.all([
33
+ const [colRes, catRes, productsRes] = await Promise.all([
22
34
  client.catalogue.getCollections(),
23
35
  client.catalogue.getCategories(),
36
+ client.catalogue.getProducts({ limit: 12 }),
24
37
  ]);
25
38
  const collections = colRes.ok ? colRes.value : [];
26
39
  const categories = catRes.ok ? catRes.value : [];
40
+ const allProducts = productsRes.ok ? productsRes.value.items : [];
27
41
 
28
- const collectionsWithProducts = await Promise.all(
42
+ const collectionsWithProducts: CollectionWithProducts[] = await Promise.all(
29
43
  collections.map(async (col) => {
30
44
  const r = await client.catalogue.getCollectionProducts(col.id);
31
45
  const items = r.ok
@@ -38,19 +52,121 @@ async function getHomeData() {
38
52
  return {
39
53
  collections: collectionsWithProducts.filter((x) => x.products.length > 0),
40
54
  categories,
55
+ featured: allProducts.slice(0, 4),
56
+ newArrivals: allProducts.slice(4, 12),
41
57
  };
42
58
  }
43
59
 
60
+ const TRUST_ITEMS = [
61
+ {
62
+ label: "Delivery",
63
+ value: "Same-day in Accra",
64
+ description: "Orders before 4pm, free over GH₵500.",
65
+ icon: (
66
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>
67
+ <path d="M3 7h12v8H3zM15 10h4l3 3v2h-7z" strokeLinejoin="round" />
68
+ <circle cx="7" cy="17" r="1.5" />
69
+ <circle cx="18" cy="17" r="1.5" />
70
+ </svg>
71
+ ),
72
+ },
73
+ {
74
+ label: "Warranty",
75
+ value: "2 years on everything",
76
+ description: "1yr manufacturer + 1yr Currents.",
77
+ icon: (
78
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>
79
+ <path d="M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6l8-3z" strokeLinejoin="round" />
80
+ <path d="M9 12l2 2 4-4" strokeLinecap="round" strokeLinejoin="round" />
81
+ </svg>
82
+ ),
83
+ },
84
+ {
85
+ label: "Payment",
86
+ value: "Pay in 3, interest-free",
87
+ description: "Or 6/12 months via Fidelity Bank.",
88
+ icon: (
89
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>
90
+ <rect x="3" y="6" width="18" height="12" rx="2" />
91
+ <line x1="3" y1="10" x2="21" y2="10" />
92
+ </svg>
93
+ ),
94
+ },
95
+ {
96
+ label: "Genuine",
97
+ value: "Authorised dealer",
98
+ description: "Direct from Apple, Samsung, Sony, Bose.",
99
+ icon: (
100
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-5 h-5" aria-hidden>
101
+ <circle cx="12" cy="12" r="9" />
102
+ <path d="M8 12l3 3 5-6" strokeLinecap="round" strokeLinejoin="round" />
103
+ </svg>
104
+ ),
105
+ },
106
+ ];
107
+
108
+ const HERO_IMAGE =
109
+ "https://images.unsplash.com/photo-1592899677977-9c10ca588bbd?w=1200&h=1200&fit=crop&auto=format&q=85";
110
+
44
111
  export default async function HomePage() {
45
- const { collections, categories } = await getHomeData();
112
+ const { collections, categories, featured, newArrivals } = await getHomeData();
113
+ const heroProduct = featured[0];
46
114
 
47
115
  return (
48
116
  <>
49
- <Hero
50
- badge="LAPTOPS · PHONES · AUDIO · ACCESSORIES"
51
- title="The tech you want, in stock today."
52
- subtitle="Authorised dealer for Apple, Samsung, Sony, Bose. Same-day delivery in Accra. Two-year warranty on every product."
117
+ <FeatureHero
118
+ eyebrow="Featured · in stock"
119
+ badge="Just landed"
120
+ title={
121
+ <>
122
+ Built for the way<br />
123
+ you actually work.
124
+ </>
125
+ }
126
+ description={
127
+ heroProduct?.description ??
128
+ "Apple, Samsung, Sony, Bose — direct from the source, with a two-year warranty and same-day Accra delivery on every order."
129
+ }
130
+ primaryCta={{
131
+ label: heroProduct ? `Shop ${heroProduct.name}` : "Shop the menu",
132
+ href: heroProduct ? `?product=${heroProduct.slug ?? heroProduct.id}` : "/shop",
133
+ }}
134
+ secondaryCta={{ label: "See deals", href: "/categories/deals" }}
135
+ imageUrl={heroProduct?.image_url ?? heroProduct?.images?.[0] ?? HERO_IMAGE}
136
+ imageAlt={heroProduct?.name ?? "Featured product"}
137
+ />
138
+
139
+ <TrustBar items={TRUST_ITEMS} />
140
+
141
+ <Suspense fallback={<CategoryTilesSkeleton />}>
142
+ <CategoryTiles categories={categories} />
143
+ </Suspense>
144
+
145
+ <PromoBanner
146
+ badge="Limited · ends Sunday"
147
+ title="Trade in your old phone, get up to GH₵3,500 off."
148
+ body="Send us your last-gen iPhone or Galaxy. We'll quote within 48 hours and credit the value toward your next upgrade."
149
+ cta={{ label: "Start trade-in", href: "/about" }}
53
150
  />
151
+
152
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
153
+ <SectionHeading
154
+ eyebrow="Just dropped"
155
+ title="New this week."
156
+ description="Hand-picked launches and freshly restocked staples."
157
+ link={{ label: "Browse all", href: "/shop" }}
158
+ />
159
+ <Suspense fallback={<GridSkeleton count={4} />}>
160
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
161
+ {newArrivals.slice(0, 4).map((p) => (
162
+ <StoreProductCard key={p.id} product={p} />
163
+ ))}
164
+ </div>
165
+ </Suspense>
166
+ </section>
167
+
168
+ <BrandMarquee />
169
+
54
170
  {collections.map(({ collection, products }) => (
55
171
  <Suspense
56
172
  key={collection.id}
@@ -63,33 +179,59 @@ export default async function HomePage() {
63
179
  />
64
180
  </Suspense>
65
181
  ))}
66
- <Suspense fallback={<CategoryGridSkeleton />}>
67
- <CategoryGrid categories={categories} />
68
- </Suspense>
182
+
183
+ <TradeInCta />
184
+
185
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
186
+ <SectionHeading
187
+ eyebrow="Best sellers"
188
+ title="What everyone&apos;s buying."
189
+ link={{ label: "See more", href: "/shop" }}
190
+ />
191
+ <Suspense fallback={<GridSkeleton count={4} />}>
192
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
193
+ {newArrivals.slice(4, 8).map((p) => (
194
+ <StoreProductCard key={p.id} product={p} />
195
+ ))}
196
+ </div>
197
+ </Suspense>
198
+ </section>
199
+
200
+ <Newsletter />
69
201
  </>
70
202
  );
71
203
  }
72
204
 
73
- function StripSkeleton({ title }: { title: string }) {
205
+ function CategoryTilesSkeleton() {
74
206
  return (
75
- <section className="max-w-7xl mx-auto px-8 pt-12">
76
- <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>
77
- <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">
207
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
208
+ <div className="h-8 w-64 bg-muted rounded mb-8 animate-pulse" />
209
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
78
210
  {Array.from({ length: 5 }).map((_, i) => (
79
- <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />
211
+ <div key={i} className="h-40 bg-muted rounded-2xl animate-pulse" />
80
212
  ))}
81
213
  </div>
82
214
  </section>
83
215
  );
84
216
  }
85
217
 
86
- function CategoryGridSkeleton() {
218
+ function GridSkeleton({ count }: { count: number }) {
219
+ return (
220
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
221
+ {Array.from({ length: count }).map((_, i) => (
222
+ <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
223
+ ))}
224
+ </div>
225
+ );
226
+ }
227
+
228
+ function StripSkeleton({ title }: { title: string }) {
87
229
  return (
88
- <section className="max-w-7xl mx-auto px-8 pt-14">
89
- <div className="h-8 w-48 bg-muted rounded mb-5 animate-pulse" />
90
- <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
91
- {Array.from({ length: 6 }).map((_, i) => (
92
- <div key={i} className="h-32 bg-muted rounded-2xl animate-pulse" />
230
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-12">
231
+ <h2 className="text-[26px] font-semibold m-0 mb-5 -tracking-[0.02em]">{title}</h2>
232
+ <div className="grid grid-flow-col auto-cols-[minmax(220px,1fr)] gap-4 overflow-x-auto pb-2">
233
+ {Array.from({ length: 5 }).map((_, i) => (
234
+ <div key={i} className="aspect-square bg-muted rounded-2xl animate-pulse" />
93
235
  ))}
94
236
  </div>
95
237
  </section>
@@ -1,11 +1,13 @@
1
1
  import type { Metadata } from "next";
2
+ import { Suspense } from "react";
2
3
  import { cacheTag, cacheLife } from "next/cache";
3
4
  import { getServerClient, tags } from "@cimplify/sdk/server";
4
5
  import { ShopClient } from "./shop-client";
5
6
 
6
7
  export const metadata: Metadata = {
7
- title: "Shop — Akua's Bakery",
8
- description: "Browse the full catalogue: breads, pastries, cakes, drinks, merch, and more.",
8
+ title: "Shop — Currents Electronics",
9
+ description:
10
+ "Browse every laptop, phone, audio device, accessory, and gaming product we stock. Authorised dealer pricing, 2-year warranty.",
9
11
  };
10
12
 
11
13
  async function getShopData() {
@@ -26,5 +28,36 @@ async function getShopData() {
26
28
 
27
29
  export default async function ShopPage() {
28
30
  const { products, categories } = await getShopData();
29
- return <ShopClient products={products} categories={categories} />;
31
+ return (
32
+ <>
33
+ <section className="bg-foreground text-background relative overflow-hidden">
34
+ <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:32px_32px]" />
35
+ <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-12 sm:py-14">
36
+ <p className="text-[11px] font-mono uppercase tracking-[0.2em] text-background/60 mb-2">
37
+ Catalogue · {products.length} products
38
+ </p>
39
+ <h1 className="text-[clamp(2rem,4vw,3rem)] font-bold m-0 -tracking-[0.025em] leading-[1.05]">
40
+ Every product we stock.
41
+ </h1>
42
+ <p className="mt-3 max-w-xl text-base text-background/75">
43
+ Filter, sort, search. Authorised dealer pricing, two-year warranty,
44
+ same-day Accra delivery on every order.
45
+ </p>
46
+ </div>
47
+ </section>
48
+ <Suspense
49
+ fallback={
50
+ <div className="max-w-7xl mx-auto px-6 sm:px-8 py-10">
51
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3 sm:gap-4">
52
+ {Array.from({ length: 8 }).map((_, i) => (
53
+ <div key={i} className="aspect-[4/3] bg-muted rounded-2xl animate-pulse" />
54
+ ))}
55
+ </div>
56
+ </div>
57
+ }
58
+ >
59
+ <ShopClient products={products} categories={categories} />
60
+ </Suspense>
61
+ </>
62
+ );
30
63
  }
@@ -8,6 +8,10 @@ import { StoreProductCard } from "@/components/store-product-card";
8
8
  * Client island for the shop page. Server-side fetches all products and
9
9
  * categories (cached via `'use cache'` in `app/shop/page.tsx`), then hands
10
10
  * them to `<CataloguePage>` which owns the interactive filter / sort state.
11
+ *
12
+ * The page hero (in `app/shop/page.tsx`) already provides the title; we
13
+ * pass an empty `title` and override the SDK heading via className so the
14
+ * page reads as one continuous design.
11
15
  */
12
16
  export function ShopClient({
13
17
  products,
@@ -18,10 +22,11 @@ export function ShopClient({
18
22
  }) {
19
23
  return (
20
24
  <CataloguePage
21
- title="The Menu"
25
+ title="All products"
22
26
  products={products}
23
27
  categories={categories}
24
28
  renderCard={(p) => <StoreProductCard product={p} />}
29
+ className="max-w-7xl mx-auto px-6 sm:px-8 py-10 sm:py-12"
25
30
  />
26
31
  );
27
32
  }
@@ -0,0 +1,36 @@
1
+ const BRANDS = [
2
+ "Apple",
3
+ "Samsung",
4
+ "Sony",
5
+ "Bose",
6
+ "Logitech",
7
+ "Anker",
8
+ "JBL",
9
+ "Microsoft",
10
+ "Lenovo",
11
+ "DJI",
12
+ ];
13
+
14
+ /**
15
+ * Brand authority strip. Wordmark-only (avoids licensing issues with real
16
+ * logos), JetBrains Mono for the brand-as-typography aesthetic.
17
+ */
18
+ export function BrandMarquee() {
19
+ return (
20
+ <section className="border-y border-border bg-background py-8 sm:py-10 overflow-hidden">
21
+ <p className="text-center text-[11px] font-mono uppercase tracking-[0.2em] text-muted-foreground mb-6">
22
+ Authorised dealer · genuine stock · 2-year warranty
23
+ </p>
24
+ <div className="flex items-center justify-around gap-10 sm:gap-14 flex-wrap px-6">
25
+ {BRANDS.map((b) => (
26
+ <span
27
+ key={b}
28
+ 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"
29
+ >
30
+ {b}
31
+ </span>
32
+ ))}
33
+ </div>
34
+ </section>
35
+ );
36
+ }
@@ -0,0 +1,104 @@
1
+ import Link from "next/link";
2
+ import Image from "next/image";
3
+ import type { Category } from "@cimplify/sdk";
4
+
5
+ interface CategoryTilesProps {
6
+ categories: Category[];
7
+ }
8
+
9
+ const ICONS: Record<string, React.ReactNode> = {
10
+ phones: (
11
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>
12
+ <rect x="6" y="2" width="12" height="20" rx="2.5" />
13
+ <line x1="12" y1="18" x2="12" y2="18" strokeWidth="2.5" strokeLinecap="round" />
14
+ </svg>
15
+ ),
16
+ laptops: (
17
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>
18
+ <rect x="3" y="4" width="18" height="12" rx="1.5" />
19
+ <line x1="2" y1="20" x2="22" y2="20" strokeLinecap="round" />
20
+ </svg>
21
+ ),
22
+ audio: (
23
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>
24
+ <path d="M3 14v-2a9 9 0 0 1 18 0v2" />
25
+ <rect x="2" y="14" width="5" height="6" rx="1.5" />
26
+ <rect x="17" y="14" width="5" height="6" rx="1.5" />
27
+ </svg>
28
+ ),
29
+ accessories: (
30
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>
31
+ <rect x="2" y="6" width="14" height="10" rx="2" />
32
+ <path d="M16 12h4l2 2v0l-2 2h-4" />
33
+ </svg>
34
+ ),
35
+ gaming: (
36
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>
37
+ <path d="M5 7h14a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1l-2-3H8l-2 3H5a3 3 0 0 1-3-3v-4a3 3 0 0 1 3-3z" />
38
+ <line x1="9" y1="11" x2="9" y2="13" strokeLinecap="round" />
39
+ <line x1="8" y1="12" x2="10" y2="12" strokeLinecap="round" />
40
+ <circle cx="15" cy="11" r="0.75" fill="currentColor" />
41
+ <circle cx="17" cy="13" r="0.75" fill="currentColor" />
42
+ </svg>
43
+ ),
44
+ };
45
+
46
+ export function CategoryTiles({ categories }: CategoryTilesProps) {
47
+ if (categories.length === 0) return null;
48
+ return (
49
+ <section className="max-w-7xl mx-auto px-6 sm:px-8 py-14 sm:py-20">
50
+ <div className="flex items-end justify-between gap-6 mb-8">
51
+ <div>
52
+ <p className="text-[11px] font-mono uppercase tracking-[0.16em] text-primary mb-2">
53
+ Shop the catalogue
54
+ </p>
55
+ <h2 className="text-[clamp(1.75rem,3vw,2.25rem)] font-bold -tracking-[0.025em]">
56
+ Pick your category.
57
+ </h2>
58
+ </div>
59
+ <Link
60
+ href="/shop"
61
+ className="text-sm font-semibold text-primary hover:underline whitespace-nowrap hidden sm:inline-flex items-center gap-1"
62
+ >
63
+ See everything
64
+ <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
65
+ <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />
66
+ </svg>
67
+ </Link>
68
+ </div>
69
+ <div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3 sm:gap-4">
70
+ {categories.map((c, i) => (
71
+ <Link
72
+ key={c.id}
73
+ href={`/categories/${c.slug}`}
74
+ className="group relative overflow-hidden rounded-2xl bg-card border border-border p-5 hover:border-primary hover:-translate-y-0.5 transition-all"
75
+ >
76
+ <div className="flex items-start justify-between mb-12">
77
+ <div className="grid place-items-center w-10 h-10 rounded-lg bg-primary/10 text-primary">
78
+ {ICONS[c.slug] ?? (
79
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="w-7 h-7" aria-hidden>
80
+ <rect x="3" y="3" width="18" height="18" rx="2" />
81
+ </svg>
82
+ )}
83
+ </div>
84
+ <span className="text-[10px] font-mono text-muted-foreground tabular-nums">
85
+ {String(i + 1).padStart(2, "0")}
86
+ </span>
87
+ </div>
88
+ <p className="text-base font-semibold mb-1 -tracking-[0.015em]">{c.name}</p>
89
+ {c.product_count != null && (
90
+ <p className="text-xs text-muted-foreground">
91
+ {c.product_count} {c.product_count === 1 ? "product" : "products"}
92
+ </p>
93
+ )}
94
+ <span className="absolute right-4 bottom-4 grid place-items-center w-7 h-7 rounded-full bg-foreground text-background opacity-0 group-hover:opacity-100 transition-opacity">
95
+ <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
96
+ <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />
97
+ </svg>
98
+ </span>
99
+ </Link>
100
+ ))}
101
+ </div>
102
+ </section>
103
+ );
104
+ }
@@ -0,0 +1,83 @@
1
+ import Link from "next/link";
2
+ import Image from "next/image";
3
+
4
+ interface FeatureHeroProps {
5
+ eyebrow: string;
6
+ title: React.ReactNode;
7
+ description: string;
8
+ primaryCta: { label: string; href: string };
9
+ secondaryCta?: { label: string; href: string };
10
+ imageUrl: string;
11
+ imageAlt: string;
12
+ badge?: string;
13
+ }
14
+
15
+ /**
16
+ * Apple-style split hero. Left: copy + CTAs. Right: large product image.
17
+ * Stacks to image-on-top on mobile.
18
+ */
19
+ export function FeatureHero({
20
+ eyebrow,
21
+ title,
22
+ description,
23
+ primaryCta,
24
+ secondaryCta,
25
+ imageUrl,
26
+ imageAlt,
27
+ badge,
28
+ }: FeatureHeroProps) {
29
+ return (
30
+ <section className="relative overflow-hidden bg-foreground text-background">
31
+ <div className="absolute inset-0 opacity-[0.04] pointer-events-none [background-image:radial-gradient(circle_at_2px_2px,white_1px,transparent_0)] [background-size:40px_40px]" />
32
+ <div className="relative max-w-7xl mx-auto px-6 sm:px-8 py-16 sm:py-20 lg:py-24 grid grid-cols-1 lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">
33
+ <div>
34
+ {badge && (
35
+ <span className="inline-flex items-center gap-2 mb-5 px-3 py-1.5 rounded-full bg-primary/15 border border-primary/40 text-primary-foreground/95 text-[11px] font-mono uppercase tracking-[0.16em]">
36
+ <span className="grid place-items-center w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
37
+ {badge}
38
+ </span>
39
+ )}
40
+ <p className="text-[12px] font-mono uppercase tracking-[0.2em] text-background/60 mb-3">
41
+ {eyebrow}
42
+ </p>
43
+ <h1 className="text-[clamp(2.5rem,6vw,4.75rem)] font-bold m-0 mb-5 -tracking-[0.035em] leading-[1.02]">
44
+ {title}
45
+ </h1>
46
+ <p className="text-base sm:text-lg text-background/75 leading-relaxed max-w-xl">
47
+ {description}
48
+ </p>
49
+ <div className="flex flex-wrap items-center gap-3 mt-7">
50
+ <Link
51
+ href={primaryCta.href}
52
+ className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md bg-primary text-primary-foreground font-semibold text-sm hover:bg-primary/90 transition-colors"
53
+ >
54
+ {primaryCta.label}
55
+ <svg viewBox="0 0 12 12" className="w-3 h-3" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
56
+ <path d="M3 6h7m0 0L7 3m3 3L7 9" strokeLinecap="round" strokeLinejoin="round" />
57
+ </svg>
58
+ </Link>
59
+ {secondaryCta && (
60
+ <Link
61
+ href={secondaryCta.href}
62
+ className="inline-flex items-center gap-2 px-5 py-2.5 rounded-md border border-background/25 text-background hover:bg-background/10 transition-colors text-sm font-medium"
63
+ >
64
+ {secondaryCta.label}
65
+ </Link>
66
+ )}
67
+ </div>
68
+ </div>
69
+ <div className="relative aspect-[4/3] lg:aspect-square w-full rounded-2xl overflow-hidden bg-background/5 ring-1 ring-background/10">
70
+ <Image
71
+ src={imageUrl}
72
+ alt={imageAlt}
73
+ fill
74
+ sizes="(min-width: 1024px) 50vw, 100vw"
75
+ className="object-cover"
76
+ priority
77
+ />
78
+ <div className="absolute inset-0 bg-gradient-to-tr from-foreground/40 via-transparent to-transparent pointer-events-none" />
79
+ </div>
80
+ </div>
81
+ </section>
82
+ );
83
+ }