create-brainerce-store 1.62.0 → 1.63.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/LICENSE +21 -21
- package/README.md +71 -71
- package/dist/index.js +5 -2
- package/messages/en.json +502 -502
- package/messages/he.json +502 -502
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +60 -60
- package/templates/nextjs/base/.env.local.ejs +24 -24
- package/templates/nextjs/base/.eslintrc.json +57 -57
- package/templates/nextjs/base/.mcp.json +8 -8
- package/templates/nextjs/base/AI-GUIDE.md +174 -174
- package/templates/nextjs/base/TRANSLATIONS.md +200 -200
- package/templates/nextjs/base/package.json.ejs +52 -52
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +262 -262
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
- package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +106 -106
- package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
- package/templates/nextjs/base/src/app/globals.css +36 -36
- package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
- package/templates/nextjs/base/src/app/layout.tsx.ejs +10 -0
- package/templates/nextjs/base/src/app/llms.txt/route.ts +44 -44
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
- package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
- package/templates/nextjs/base/src/app/page.tsx +64 -64
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +175 -175
- package/templates/nextjs/base/src/app/products/page.tsx +19 -19
- package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
- package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
- package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
- package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
- package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
- package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
- package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
- package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +179 -179
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
- package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
- package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
- package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
- package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
- package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
- package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
- package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
- package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
- package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
- package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
- package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
- package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
- package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
- package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
- package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
- package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
- package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
- package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
- package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
- package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
- package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
- package/templates/nextjs/base/src/core/lib/store-info.ts +9 -0
- package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
- package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
- package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
- package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
- package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
- package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
- package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
- package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -20
- package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
- package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
- package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
- package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
- package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
- package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
- package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
- package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
- package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
- package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
- package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
- package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
- package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
- package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
- package/templates/nextjs/base/tailwind.config.ts +69 -69
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +10 -0
- package/templates/nextjs/designs/atelier/globals.css +402 -402
- package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
- package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
- package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
- package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
- package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
- package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
- package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
- package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
- package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
- package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
- package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
- package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
- package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
- package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
- package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
- package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
- package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
- package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
- package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
- package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +27 -27
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -19
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
- package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
- package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
- package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
- package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
- package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Category tiles derived from the live catalog. Uses product categories when
|
|
5
|
-
* present; otherwise derives collections from Hebrew product-name prefixes
|
|
6
|
-
* (טבעת / שרשרת / עגילי / צמיד) so the section is never empty on stores that
|
|
7
|
-
* haven't seeded category data. Each tile shows a real catalog photo.
|
|
8
|
-
*/
|
|
9
|
-
import type { Product } from 'brainerce';
|
|
10
|
-
import { Link } from '@/core/lib/navigation';
|
|
11
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
12
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
13
|
-
import { IconArrowEnd } from '@/ui/shared/icons';
|
|
14
|
-
|
|
15
|
-
const PREFIX_GROUPS = [
|
|
16
|
-
{ prefixes: ['טבעת', 'טבעות', 'ring'], labelKey: 'catRings' },
|
|
17
|
-
{ prefixes: ['שרשרת', 'שרשראות', 'necklace'], labelKey: 'catNecklaces' },
|
|
18
|
-
{ prefixes: ['עגיל', 'earring'], labelKey: 'catEarrings' },
|
|
19
|
-
{ prefixes: ['צמיד', 'bracelet'], labelKey: 'catBracelets' },
|
|
20
|
-
] as const;
|
|
21
|
-
|
|
22
|
-
interface Tile {
|
|
23
|
-
key: string;
|
|
24
|
-
label: string;
|
|
25
|
-
imageUrl: string;
|
|
26
|
-
imageAlt: string;
|
|
27
|
-
href: string;
|
|
28
|
-
count: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function CategoryTiles({ products }: { products: Product[] }) {
|
|
32
|
-
const t = useTranslations('home');
|
|
33
|
-
const tc = useTranslations('common');
|
|
34
|
-
|
|
35
|
-
const tiles: Tile[] = [];
|
|
36
|
-
|
|
37
|
-
// Prefer real category data when the catalog has it.
|
|
38
|
-
const byCategory = new Map<string, { name: string; slug: string | null; items: Product[] }>();
|
|
39
|
-
for (const p of products) {
|
|
40
|
-
for (const cat of p.categories ?? []) {
|
|
41
|
-
const entry = byCategory.get(cat.id) ?? { name: cat.name, slug: cat.slug ?? null, items: [] };
|
|
42
|
-
entry.items.push(p);
|
|
43
|
-
byCategory.set(cat.id, entry);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Tile photo: prefer the LAST matching product's image — the featured grid
|
|
48
|
-
// above leads with the first ones, so tiles avoid repeating the same shots.
|
|
49
|
-
const pickImage = (items: Product[]) =>
|
|
50
|
-
[...items].reverse().find((p) => p.images?.[0]?.url)?.images?.[0];
|
|
51
|
-
|
|
52
|
-
if (byCategory.size >= 2) {
|
|
53
|
-
for (const [id, { name, slug, items }] of Array.from(byCategory.entries()).slice(0, 4)) {
|
|
54
|
-
const img = pickImage(items);
|
|
55
|
-
if (!img?.url) continue;
|
|
56
|
-
// Prefer the real category landing page (SEO-indexed, has its own
|
|
57
|
-
// written description) — this is the only place in the storefront that
|
|
58
|
-
// links to it, so without a real slug it'd be an orphaned page. Fall
|
|
59
|
-
// back to the filtered grid for categories with no slug yet.
|
|
60
|
-
tiles.push({
|
|
61
|
-
key: id,
|
|
62
|
-
label: name,
|
|
63
|
-
imageUrl: img.url,
|
|
64
|
-
imageAlt: img.alt || name,
|
|
65
|
-
href: slug
|
|
66
|
-
? `/category/${encodeURIComponent(slug)}`
|
|
67
|
-
: `/products?category=${encodeURIComponent(id)}`,
|
|
68
|
-
count: items.length,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
} else {
|
|
72
|
-
// Derive from name prefixes — never hardcodes products, only groups them.
|
|
73
|
-
for (const group of PREFIX_GROUPS) {
|
|
74
|
-
const items = products.filter((p) =>
|
|
75
|
-
group.prefixes.some((prefix) => p.name?.toLowerCase().startsWith(prefix))
|
|
76
|
-
);
|
|
77
|
-
const img = pickImage(items);
|
|
78
|
-
if (!img?.url) continue;
|
|
79
|
-
tiles.push({
|
|
80
|
-
key: group.labelKey,
|
|
81
|
-
label: t(group.labelKey),
|
|
82
|
-
imageUrl: img.url,
|
|
83
|
-
imageAlt: img.alt || t(group.labelKey),
|
|
84
|
-
href: `/products?search=${encodeURIComponent(group.prefixes[0])}`,
|
|
85
|
-
count: items.length,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (tiles.length === 0) return null;
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<section id="categories" className="section-pad wash-sand relative overflow-hidden">
|
|
94
|
-
<div aria-hidden="true" className="glow-blob -start-24 top-1/3 h-72 w-72 opacity-70" />
|
|
95
|
-
<div className="container-page relative">
|
|
96
|
-
<div className="mb-8 max-w-xl lg:mb-10">
|
|
97
|
-
<span className="eyebrow">{t('categoriesEyebrow')}</span>
|
|
98
|
-
<h2 className="text-4xl sm:text-5xl">{t('categoriesTitle')}</h2>
|
|
99
|
-
<p className="text-muted-foreground mt-3">{t('categoriesSubtitle')}</p>
|
|
100
|
-
</div>
|
|
101
|
-
|
|
102
|
-
<ul
|
|
103
|
-
className={`grid grid-cols-2 gap-3 sm:gap-5 lg:gap-6 ${
|
|
104
|
-
tiles.length === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
|
|
105
|
-
}`}
|
|
106
|
-
>
|
|
107
|
-
{tiles.map((tile, idx) => (
|
|
108
|
-
<li
|
|
109
|
-
key={tile.key}
|
|
110
|
-
className={
|
|
111
|
-
// Odd tile count: let the last tile span the full row on
|
|
112
|
-
// mobile so the 2-col grid never shows an orphan hole.
|
|
113
|
-
tiles.length % 2 === 1 && idx === tiles.length - 1
|
|
114
|
-
? 'col-span-2 lg:col-span-1'
|
|
115
|
-
: undefined
|
|
116
|
-
}
|
|
117
|
-
>
|
|
118
|
-
<Link href={tile.href} className="card card-hover group block overflow-hidden">
|
|
119
|
-
<span className="bg-secondary relative block aspect-square overflow-hidden">
|
|
120
|
-
<Image
|
|
121
|
-
src={tile.imageUrl}
|
|
122
|
-
alt={tile.imageAlt}
|
|
123
|
-
fill
|
|
124
|
-
sizes="(max-width: 640px) 50vw, 25vw"
|
|
125
|
-
className="img-zoom object-cover"
|
|
126
|
-
/>
|
|
127
|
-
</span>
|
|
128
|
-
<span className="flex items-center justify-between gap-2 px-4 py-3.5">
|
|
129
|
-
<span>
|
|
130
|
-
<span className="text-foreground block text-[15px] font-semibold">
|
|
131
|
-
{tile.label}
|
|
132
|
-
</span>
|
|
133
|
-
<span className="text-muted-foreground block text-xs">
|
|
134
|
-
{tile.count} {tile.count === 1 ? tc('product') : tc('products')}
|
|
135
|
-
</span>
|
|
136
|
-
</span>
|
|
137
|
-
<span
|
|
138
|
-
aria-hidden="true"
|
|
139
|
-
className="bg-secondary text-foreground group-hover:bg-primary group-hover:text-primary-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors"
|
|
140
|
-
>
|
|
141
|
-
<IconArrowEnd size={16} className="rtl-flip" />
|
|
142
|
-
</span>
|
|
143
|
-
</span>
|
|
144
|
-
</Link>
|
|
145
|
-
</li>
|
|
146
|
-
))}
|
|
147
|
-
</ul>
|
|
148
|
-
</div>
|
|
149
|
-
</section>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Category tiles derived from the live catalog. Uses product categories when
|
|
5
|
+
* present; otherwise derives collections from Hebrew product-name prefixes
|
|
6
|
+
* (טבעת / שרשרת / עגילי / צמיד) so the section is never empty on stores that
|
|
7
|
+
* haven't seeded category data. Each tile shows a real catalog photo.
|
|
8
|
+
*/
|
|
9
|
+
import type { Product } from 'brainerce';
|
|
10
|
+
import { Link } from '@/core/lib/navigation';
|
|
11
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
12
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
13
|
+
import { IconArrowEnd } from '@/ui/shared/icons';
|
|
14
|
+
|
|
15
|
+
const PREFIX_GROUPS = [
|
|
16
|
+
{ prefixes: ['טבעת', 'טבעות', 'ring'], labelKey: 'catRings' },
|
|
17
|
+
{ prefixes: ['שרשרת', 'שרשראות', 'necklace'], labelKey: 'catNecklaces' },
|
|
18
|
+
{ prefixes: ['עגיל', 'earring'], labelKey: 'catEarrings' },
|
|
19
|
+
{ prefixes: ['צמיד', 'bracelet'], labelKey: 'catBracelets' },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
interface Tile {
|
|
23
|
+
key: string;
|
|
24
|
+
label: string;
|
|
25
|
+
imageUrl: string;
|
|
26
|
+
imageAlt: string;
|
|
27
|
+
href: string;
|
|
28
|
+
count: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function CategoryTiles({ products }: { products: Product[] }) {
|
|
32
|
+
const t = useTranslations('home');
|
|
33
|
+
const tc = useTranslations('common');
|
|
34
|
+
|
|
35
|
+
const tiles: Tile[] = [];
|
|
36
|
+
|
|
37
|
+
// Prefer real category data when the catalog has it.
|
|
38
|
+
const byCategory = new Map<string, { name: string; slug: string | null; items: Product[] }>();
|
|
39
|
+
for (const p of products) {
|
|
40
|
+
for (const cat of p.categories ?? []) {
|
|
41
|
+
const entry = byCategory.get(cat.id) ?? { name: cat.name, slug: cat.slug ?? null, items: [] };
|
|
42
|
+
entry.items.push(p);
|
|
43
|
+
byCategory.set(cat.id, entry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Tile photo: prefer the LAST matching product's image — the featured grid
|
|
48
|
+
// above leads with the first ones, so tiles avoid repeating the same shots.
|
|
49
|
+
const pickImage = (items: Product[]) =>
|
|
50
|
+
[...items].reverse().find((p) => p.images?.[0]?.url)?.images?.[0];
|
|
51
|
+
|
|
52
|
+
if (byCategory.size >= 2) {
|
|
53
|
+
for (const [id, { name, slug, items }] of Array.from(byCategory.entries()).slice(0, 4)) {
|
|
54
|
+
const img = pickImage(items);
|
|
55
|
+
if (!img?.url) continue;
|
|
56
|
+
// Prefer the real category landing page (SEO-indexed, has its own
|
|
57
|
+
// written description) — this is the only place in the storefront that
|
|
58
|
+
// links to it, so without a real slug it'd be an orphaned page. Fall
|
|
59
|
+
// back to the filtered grid for categories with no slug yet.
|
|
60
|
+
tiles.push({
|
|
61
|
+
key: id,
|
|
62
|
+
label: name,
|
|
63
|
+
imageUrl: img.url,
|
|
64
|
+
imageAlt: img.alt || name,
|
|
65
|
+
href: slug
|
|
66
|
+
? `/category/${encodeURIComponent(slug)}`
|
|
67
|
+
: `/products?category=${encodeURIComponent(id)}`,
|
|
68
|
+
count: items.length,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Derive from name prefixes — never hardcodes products, only groups them.
|
|
73
|
+
for (const group of PREFIX_GROUPS) {
|
|
74
|
+
const items = products.filter((p) =>
|
|
75
|
+
group.prefixes.some((prefix) => p.name?.toLowerCase().startsWith(prefix))
|
|
76
|
+
);
|
|
77
|
+
const img = pickImage(items);
|
|
78
|
+
if (!img?.url) continue;
|
|
79
|
+
tiles.push({
|
|
80
|
+
key: group.labelKey,
|
|
81
|
+
label: t(group.labelKey),
|
|
82
|
+
imageUrl: img.url,
|
|
83
|
+
imageAlt: img.alt || t(group.labelKey),
|
|
84
|
+
href: `/products?search=${encodeURIComponent(group.prefixes[0])}`,
|
|
85
|
+
count: items.length,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (tiles.length === 0) return null;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<section id="categories" className="section-pad wash-sand relative overflow-hidden">
|
|
94
|
+
<div aria-hidden="true" className="glow-blob -start-24 top-1/3 h-72 w-72 opacity-70" />
|
|
95
|
+
<div className="container-page relative">
|
|
96
|
+
<div className="mb-8 max-w-xl lg:mb-10">
|
|
97
|
+
<span className="eyebrow">{t('categoriesEyebrow')}</span>
|
|
98
|
+
<h2 className="text-4xl sm:text-5xl">{t('categoriesTitle')}</h2>
|
|
99
|
+
<p className="text-muted-foreground mt-3">{t('categoriesSubtitle')}</p>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<ul
|
|
103
|
+
className={`grid grid-cols-2 gap-3 sm:gap-5 lg:gap-6 ${
|
|
104
|
+
tiles.length === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-4'
|
|
105
|
+
}`}
|
|
106
|
+
>
|
|
107
|
+
{tiles.map((tile, idx) => (
|
|
108
|
+
<li
|
|
109
|
+
key={tile.key}
|
|
110
|
+
className={
|
|
111
|
+
// Odd tile count: let the last tile span the full row on
|
|
112
|
+
// mobile so the 2-col grid never shows an orphan hole.
|
|
113
|
+
tiles.length % 2 === 1 && idx === tiles.length - 1
|
|
114
|
+
? 'col-span-2 lg:col-span-1'
|
|
115
|
+
: undefined
|
|
116
|
+
}
|
|
117
|
+
>
|
|
118
|
+
<Link href={tile.href} className="card card-hover group block overflow-hidden">
|
|
119
|
+
<span className="bg-secondary relative block aspect-square overflow-hidden">
|
|
120
|
+
<Image
|
|
121
|
+
src={tile.imageUrl}
|
|
122
|
+
alt={tile.imageAlt}
|
|
123
|
+
fill
|
|
124
|
+
sizes="(max-width: 640px) 50vw, 25vw"
|
|
125
|
+
className="img-zoom object-cover"
|
|
126
|
+
/>
|
|
127
|
+
</span>
|
|
128
|
+
<span className="flex items-center justify-between gap-2 px-4 py-3.5">
|
|
129
|
+
<span>
|
|
130
|
+
<span className="text-foreground block text-[15px] font-semibold">
|
|
131
|
+
{tile.label}
|
|
132
|
+
</span>
|
|
133
|
+
<span className="text-muted-foreground block text-xs">
|
|
134
|
+
{tile.count} {tile.count === 1 ? tc('product') : tc('products')}
|
|
135
|
+
</span>
|
|
136
|
+
</span>
|
|
137
|
+
<span
|
|
138
|
+
aria-hidden="true"
|
|
139
|
+
className="bg-secondary text-foreground group-hover:bg-primary group-hover:text-primary-foreground flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors"
|
|
140
|
+
>
|
|
141
|
+
<IconArrowEnd size={16} className="rtl-flip" />
|
|
142
|
+
</span>
|
|
143
|
+
</span>
|
|
144
|
+
</Link>
|
|
145
|
+
</li>
|
|
146
|
+
))}
|
|
147
|
+
</ul>
|
|
148
|
+
</div>
|
|
149
|
+
</section>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { DiscountBanner } from 'brainerce';
|
|
4
|
-
import { IconTag } from '@/ui/shared/icons';
|
|
5
|
-
|
|
6
|
-
/** Store-wide discount banner strip shown at the very top of the homepage. */
|
|
7
|
-
export function DiscountBannerStrip({ banners }: { banners: DiscountBanner[] }) {
|
|
8
|
-
if (banners.length === 0) return null;
|
|
9
|
-
|
|
10
|
-
return (
|
|
11
|
-
<div role="region" aria-label="Promotions" className="border-b bg-secondary">
|
|
12
|
-
<ul className="container-page flex flex-wrap items-center justify-center gap-x-8 gap-y-1 py-2.5">
|
|
13
|
-
{banners.map((banner) => (
|
|
14
|
-
<li
|
|
15
|
-
key={banner.ruleId}
|
|
16
|
-
className="inline-flex items-center gap-2 text-[13px] font-semibold text-foreground"
|
|
17
|
-
>
|
|
18
|
-
<span aria-hidden="true" className="text-primary">
|
|
19
|
-
<IconTag size={16} />
|
|
20
|
-
</span>
|
|
21
|
-
{banner.text}
|
|
22
|
-
</li>
|
|
23
|
-
))}
|
|
24
|
-
</ul>
|
|
25
|
-
</div>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { DiscountBanner } from 'brainerce';
|
|
4
|
+
import { IconTag } from '@/ui/shared/icons';
|
|
5
|
+
|
|
6
|
+
/** Store-wide discount banner strip shown at the very top of the homepage. */
|
|
7
|
+
export function DiscountBannerStrip({ banners }: { banners: DiscountBanner[] }) {
|
|
8
|
+
if (banners.length === 0) return null;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div role="region" aria-label="Promotions" className="border-b bg-secondary">
|
|
12
|
+
<ul className="container-page flex flex-wrap items-center justify-center gap-x-8 gap-y-1 py-2.5">
|
|
13
|
+
{banners.map((banner) => (
|
|
14
|
+
<li
|
|
15
|
+
key={banner.ruleId}
|
|
16
|
+
className="inline-flex items-center gap-2 text-[13px] font-semibold text-foreground"
|
|
17
|
+
>
|
|
18
|
+
<span aria-hidden="true" className="text-primary">
|
|
19
|
+
<IconTag size={16} />
|
|
20
|
+
</span>
|
|
21
|
+
{banner.text}
|
|
22
|
+
</li>
|
|
23
|
+
))}
|
|
24
|
+
</ul>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Editorial "our story" band — lifestyle photography (Pexels hotlinks,
|
|
5
|
-
* HEAD-verified) + brand-story copy from translation keys. Purely template
|
|
6
|
-
* marketing content; no catalog data is hardcoded here. Deliberately
|
|
7
|
-
* vertical-neutral (order fulfillment / packaging imagery, not a specific
|
|
8
|
-
* product category) since this pack scaffolds stores of any kind.
|
|
9
|
-
*/
|
|
10
|
-
import { Link } from '@/core/lib/navigation';
|
|
11
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
12
|
-
import { IconArrowEnd } from '@/ui/shared/icons';
|
|
13
|
-
|
|
14
|
-
// Verified free-stock imagery (images.pexels.com allows hotlinking).
|
|
15
|
-
const IMG_MODEL =
|
|
16
|
-
'https://images.pexels.com/photos/7857523/pexels-photo-7857523.jpeg?auto=compress&cs=tinysrgb&w=1200';
|
|
17
|
-
const IMG_DETAIL =
|
|
18
|
-
'https://images.pexels.com/photos/6224252/pexels-photo-6224252.jpeg?auto=compress&cs=tinysrgb&w=800';
|
|
19
|
-
|
|
20
|
-
export function EditorialBand() {
|
|
21
|
-
const t = useTranslations('home');
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<section className="section-pad">
|
|
25
|
-
<div className="container-page grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
|
|
26
|
-
{/* Image pair */}
|
|
27
|
-
<div className="grid grid-cols-12 gap-4">
|
|
28
|
-
<figure className="border-background col-span-8 -rotate-2 overflow-hidden rounded-3xl border-4 shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.35)]">
|
|
29
|
-
{}
|
|
30
|
-
<img
|
|
31
|
-
src={IMG_MODEL}
|
|
32
|
-
alt={t('storyTitle')}
|
|
33
|
-
loading="lazy"
|
|
34
|
-
className="aspect-[4/5] h-full w-full object-cover"
|
|
35
|
-
/>
|
|
36
|
-
</figure>
|
|
37
|
-
<figure className="border-background col-span-4 -mb-8 rotate-3 self-end overflow-hidden rounded-3xl border-4 shadow-[0_24px_50px_-20px_hsl(var(--primary)/0.35)]">
|
|
38
|
-
{}
|
|
39
|
-
<img
|
|
40
|
-
src={IMG_DETAIL}
|
|
41
|
-
alt={t('storyStat1Value')}
|
|
42
|
-
loading="lazy"
|
|
43
|
-
className="aspect-[3/4] h-full w-full object-cover"
|
|
44
|
-
/>
|
|
45
|
-
</figure>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
{/* Copy */}
|
|
49
|
-
<div className="max-w-xl">
|
|
50
|
-
<span className="eyebrow">{t('storyEyebrow')}</span>
|
|
51
|
-
<h2 className="text-4xl sm:text-5xl">{t('storyTitle')}</h2>
|
|
52
|
-
<p className="text-muted-foreground mt-5 leading-8">{t('storyP1')}</p>
|
|
53
|
-
<p className="text-muted-foreground mt-4 leading-8">{t('storyP2')}</p>
|
|
54
|
-
|
|
55
|
-
{/* Craft highlights */}
|
|
56
|
-
<dl className="mt-8 grid grid-cols-2 gap-6 border-t pt-8">
|
|
57
|
-
<div>
|
|
58
|
-
<dt className="font-display text-foreground text-2xl">{t('storyStat1Value')}</dt>
|
|
59
|
-
<dd className="text-muted-foreground mt-1 text-sm">{t('storyStat1Label')}</dd>
|
|
60
|
-
</div>
|
|
61
|
-
<div>
|
|
62
|
-
<dt className="font-display text-foreground text-2xl">{t('storyStat2Value')}</dt>
|
|
63
|
-
<dd className="text-muted-foreground mt-1 text-sm">{t('storyStat2Label')}</dd>
|
|
64
|
-
</div>
|
|
65
|
-
</dl>
|
|
66
|
-
|
|
67
|
-
<Link href="/products" className="btn-outline btn-md mt-8">
|
|
68
|
-
{t('storyCta')}
|
|
69
|
-
<IconArrowEnd size={18} className="rtl-flip" />
|
|
70
|
-
</Link>
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
</section>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Editorial "our story" band — lifestyle photography (Pexels hotlinks,
|
|
5
|
+
* HEAD-verified) + brand-story copy from translation keys. Purely template
|
|
6
|
+
* marketing content; no catalog data is hardcoded here. Deliberately
|
|
7
|
+
* vertical-neutral (order fulfillment / packaging imagery, not a specific
|
|
8
|
+
* product category) since this pack scaffolds stores of any kind.
|
|
9
|
+
*/
|
|
10
|
+
import { Link } from '@/core/lib/navigation';
|
|
11
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
12
|
+
import { IconArrowEnd } from '@/ui/shared/icons';
|
|
13
|
+
|
|
14
|
+
// Verified free-stock imagery (images.pexels.com allows hotlinking).
|
|
15
|
+
const IMG_MODEL =
|
|
16
|
+
'https://images.pexels.com/photos/7857523/pexels-photo-7857523.jpeg?auto=compress&cs=tinysrgb&w=1200';
|
|
17
|
+
const IMG_DETAIL =
|
|
18
|
+
'https://images.pexels.com/photos/6224252/pexels-photo-6224252.jpeg?auto=compress&cs=tinysrgb&w=800';
|
|
19
|
+
|
|
20
|
+
export function EditorialBand() {
|
|
21
|
+
const t = useTranslations('home');
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<section className="section-pad">
|
|
25
|
+
<div className="container-page grid items-center gap-10 lg:grid-cols-2 lg:gap-16">
|
|
26
|
+
{/* Image pair */}
|
|
27
|
+
<div className="grid grid-cols-12 gap-4">
|
|
28
|
+
<figure className="border-background col-span-8 -rotate-2 overflow-hidden rounded-3xl border-4 shadow-[0_28px_60px_-24px_hsl(var(--primary)/0.35)]">
|
|
29
|
+
{}
|
|
30
|
+
<img
|
|
31
|
+
src={IMG_MODEL}
|
|
32
|
+
alt={t('storyTitle')}
|
|
33
|
+
loading="lazy"
|
|
34
|
+
className="aspect-[4/5] h-full w-full object-cover"
|
|
35
|
+
/>
|
|
36
|
+
</figure>
|
|
37
|
+
<figure className="border-background col-span-4 -mb-8 rotate-3 self-end overflow-hidden rounded-3xl border-4 shadow-[0_24px_50px_-20px_hsl(var(--primary)/0.35)]">
|
|
38
|
+
{}
|
|
39
|
+
<img
|
|
40
|
+
src={IMG_DETAIL}
|
|
41
|
+
alt={t('storyStat1Value')}
|
|
42
|
+
loading="lazy"
|
|
43
|
+
className="aspect-[3/4] h-full w-full object-cover"
|
|
44
|
+
/>
|
|
45
|
+
</figure>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Copy */}
|
|
49
|
+
<div className="max-w-xl">
|
|
50
|
+
<span className="eyebrow">{t('storyEyebrow')}</span>
|
|
51
|
+
<h2 className="text-4xl sm:text-5xl">{t('storyTitle')}</h2>
|
|
52
|
+
<p className="text-muted-foreground mt-5 leading-8">{t('storyP1')}</p>
|
|
53
|
+
<p className="text-muted-foreground mt-4 leading-8">{t('storyP2')}</p>
|
|
54
|
+
|
|
55
|
+
{/* Craft highlights */}
|
|
56
|
+
<dl className="mt-8 grid grid-cols-2 gap-6 border-t pt-8">
|
|
57
|
+
<div>
|
|
58
|
+
<dt className="font-display text-foreground text-2xl">{t('storyStat1Value')}</dt>
|
|
59
|
+
<dd className="text-muted-foreground mt-1 text-sm">{t('storyStat1Label')}</dd>
|
|
60
|
+
</div>
|
|
61
|
+
<div>
|
|
62
|
+
<dt className="font-display text-foreground text-2xl">{t('storyStat2Value')}</dt>
|
|
63
|
+
<dd className="text-muted-foreground mt-1 text-sm">{t('storyStat2Label')}</dd>
|
|
64
|
+
</div>
|
|
65
|
+
</dl>
|
|
66
|
+
|
|
67
|
+
<Link href="/products" className="btn-outline btn-md mt-8">
|
|
68
|
+
{t('storyCta')}
|
|
69
|
+
<IconArrowEnd size={18} className="rtl-flip" />
|
|
70
|
+
</Link>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import type { Product } from 'brainerce';
|
|
4
|
-
import { Link } from '@/core/lib/navigation';
|
|
5
|
-
import { useTranslations } from '@/core/lib/translations';
|
|
6
|
-
import { ProductGrid } from '@/ui/product/product-grid';
|
|
7
|
-
import { IconArrowEnd } from '@/ui/shared/icons';
|
|
8
|
-
|
|
9
|
-
/** Homepage featured-products grid with a section header + view-all link. */
|
|
10
|
-
export function FeaturedProductsSection({ products }: { products: Product[] }) {
|
|
11
|
-
const t = useTranslations('home');
|
|
12
|
-
const tc = useTranslations('common');
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<section className="section-pad">
|
|
16
|
-
<div className="container-page">
|
|
17
|
-
<div className="mb-8 flex flex-wrap items-end justify-between gap-4 lg:mb-10">
|
|
18
|
-
<div className="max-w-xl">
|
|
19
|
-
<span className="eyebrow">{t('featuredEyebrow')}</span>
|
|
20
|
-
<h2 className="text-4xl sm:text-5xl">{t('featuredProducts')}</h2>
|
|
21
|
-
<p className="mt-3 text-muted-foreground">{t('featuredSubtitle')}</p>
|
|
22
|
-
</div>
|
|
23
|
-
<Link
|
|
24
|
-
href="/products"
|
|
25
|
-
className="group inline-flex items-center gap-1.5 text-sm font-semibold text-primary hover:underline"
|
|
26
|
-
>
|
|
27
|
-
{tc('viewAll')}
|
|
28
|
-
<IconArrowEnd size={16} className="rtl-flip" />
|
|
29
|
-
</Link>
|
|
30
|
-
</div>
|
|
31
|
-
|
|
32
|
-
<ProductGrid products={products.slice(0, 8)} />
|
|
33
|
-
</div>
|
|
34
|
-
</section>
|
|
35
|
-
);
|
|
36
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { Product } from 'brainerce';
|
|
4
|
+
import { Link } from '@/core/lib/navigation';
|
|
5
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
6
|
+
import { ProductGrid } from '@/ui/product/product-grid';
|
|
7
|
+
import { IconArrowEnd } from '@/ui/shared/icons';
|
|
8
|
+
|
|
9
|
+
/** Homepage featured-products grid with a section header + view-all link. */
|
|
10
|
+
export function FeaturedProductsSection({ products }: { products: Product[] }) {
|
|
11
|
+
const t = useTranslations('home');
|
|
12
|
+
const tc = useTranslations('common');
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<section className="section-pad">
|
|
16
|
+
<div className="container-page">
|
|
17
|
+
<div className="mb-8 flex flex-wrap items-end justify-between gap-4 lg:mb-10">
|
|
18
|
+
<div className="max-w-xl">
|
|
19
|
+
<span className="eyebrow">{t('featuredEyebrow')}</span>
|
|
20
|
+
<h2 className="text-4xl sm:text-5xl">{t('featuredProducts')}</h2>
|
|
21
|
+
<p className="mt-3 text-muted-foreground">{t('featuredSubtitle')}</p>
|
|
22
|
+
</div>
|
|
23
|
+
<Link
|
|
24
|
+
href="/products"
|
|
25
|
+
className="group inline-flex items-center gap-1.5 text-sm font-semibold text-primary hover:underline"
|
|
26
|
+
>
|
|
27
|
+
{tc('viewAll')}
|
|
28
|
+
<IconArrowEnd size={16} className="rtl-flip" />
|
|
29
|
+
</Link>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<ProductGrid products={products.slice(0, 8)} />
|
|
33
|
+
</div>
|
|
34
|
+
</section>
|
|
35
|
+
);
|
|
36
|
+
}
|