create-brainerce-store 1.54.0 → 1.57.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/index.js +9 -2
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +16 -2
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +175 -157
- package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -0
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +49 -2
- package/templates/nextjs/base/src/core/lib/store-info.ts +3 -0
- package/templates/nextjs/designs/atelier/globals.css +26 -20
- package/templates/nextjs/designs/atelier/messages-patch/en.json +13 -13
- package/templates/nextjs/designs/atelier/messages-patch/he.json +15 -15
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +14 -14
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +5 -5
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +16 -13
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +23 -17
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +6 -11
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -269
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +9 -10
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"home": {
|
|
10
10
|
"heroEyebrow": "New collection · Handcrafted",
|
|
11
|
-
"heroTitle": "
|
|
12
|
-
"heroLead": "
|
|
11
|
+
"heroTitle": "Pieces that tell your story",
|
|
12
|
+
"heroLead": "Thoughtfully designed, handcrafted with love — made to be with you through the small moments and the big ones.",
|
|
13
13
|
"heroCtaSecondary": "Browse categories",
|
|
14
14
|
"heroTrust": "Happy customers nationwide",
|
|
15
15
|
"benefit1Title": "Fast home delivery",
|
|
@@ -17,37 +17,37 @@
|
|
|
17
17
|
"benefit2Title": "Secure payment",
|
|
18
18
|
"benefit2Desc": "Encrypted checkout, highest standard",
|
|
19
19
|
"benefit3Title": "Quality materials",
|
|
20
|
-
"benefit3Desc": "
|
|
20
|
+
"benefit3Desc": "Carefully sourced, built to last",
|
|
21
21
|
"benefit4Title": "Easy returns",
|
|
22
22
|
"benefit4Desc": "30 days to exchange or return, no questions",
|
|
23
23
|
"featuredEyebrow": "Our picks",
|
|
24
24
|
"featuredSubtitle": "The pieces our customers love most — hand-picked from the new collection.",
|
|
25
25
|
"categoriesEyebrow": "Categories",
|
|
26
26
|
"categoriesTitle": "Find the perfect piece",
|
|
27
|
-
"categoriesSubtitle": "From
|
|
27
|
+
"categoriesSubtitle": "From everyday favorites to statement pieces — there's something for everyone.",
|
|
28
28
|
"catRings": "Rings",
|
|
29
29
|
"catNecklaces": "Necklaces",
|
|
30
30
|
"catEarrings": "Earrings",
|
|
31
31
|
"catBracelets": "Bracelets",
|
|
32
32
|
"catCta": "Shop the category",
|
|
33
33
|
"storyEyebrow": "Our studio",
|
|
34
|
-
"storyTitle": "Made
|
|
35
|
-
"storyP1": "Every piece begins as a small sketch on paper — and ends as delicate handwork in our studio. We choose every
|
|
36
|
-
"storyP2": "We believe in
|
|
34
|
+
"storyTitle": "Made with care, made to last",
|
|
35
|
+
"storyP1": "Every piece begins as a small sketch on paper — and ends as delicate handwork in our studio. We choose every material and every detail with care, so the piece that reaches you is exactly as we imagined it.",
|
|
36
|
+
"storyP2": "We believe in pieces that last: real materials, meticulous finish and full warranty on every piece. That is what love for the small details looks like.",
|
|
37
37
|
"storyCta": "Discover the collection",
|
|
38
38
|
"storyStat1Value": "Handmade",
|
|
39
39
|
"storyStat1Label": "Every piece is made in our studio",
|
|
40
|
-
"storyStat2Value": "
|
|
41
|
-
"storyStat2Label": "
|
|
40
|
+
"storyStat2Value": "Hand-finished",
|
|
41
|
+
"storyStat2Label": "Every piece checked before it ships",
|
|
42
42
|
"testimonialsEyebrow": "Customer stories",
|
|
43
43
|
"testimonialsTitle": "They already fell in love",
|
|
44
|
-
"testimonial1Quote": "
|
|
44
|
+
"testimonial1Quote": "It arrived in gorgeous packaging and looks even better than the photos. I got compliments on day one.",
|
|
45
45
|
"testimonial1Name": "Noa L.",
|
|
46
46
|
"testimonial1Meta": "Tel Aviv",
|
|
47
|
-
"testimonial2Quote": "I bought
|
|
47
|
+
"testimonial2Quote": "I bought this as a gift for my mom and she has not put it down since. Amazing quality and truly personal service.",
|
|
48
48
|
"testimonial2Name": "Shira B.",
|
|
49
49
|
"testimonial2Meta": "Haifa",
|
|
50
|
-
"testimonial3Quote": "
|
|
50
|
+
"testimonial3Quote": "It's exactly as described, the work is delicate and precise. You can feel every detail was made with love. Highly recommend.",
|
|
51
51
|
"testimonial3Name": "Dana K.",
|
|
52
52
|
"testimonial3Meta": "Jerusalem",
|
|
53
53
|
"newsletterTitle": "Join our club",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"shippingAtCheckout": "Shipping calculated at checkout"
|
|
89
89
|
},
|
|
90
90
|
"footer": {
|
|
91
|
-
"tagline": "
|
|
91
|
+
"tagline": "{{storeName}} — handcrafted with love in Israel.",
|
|
92
92
|
"followUs": "Follow us",
|
|
93
93
|
"quickLinksTitle": "Quick links",
|
|
94
94
|
"linkProducts": "All products",
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"home": {
|
|
10
10
|
"heroEyebrow": "קולקציה חדשה · עבודת יד",
|
|
11
|
-
"heroTitle": "
|
|
12
|
-
"heroLead": "עיצובים
|
|
11
|
+
"heroTitle": "פריטים שמספרים את הסיפור שלך",
|
|
12
|
+
"heroLead": "עיצובים עדינים, בעבודת יד ובאהבה — נוצרו ללוות אתכן ברגעים הקטנים והגדולים.",
|
|
13
13
|
"heroCtaSecondary": "לצפייה בקטגוריות",
|
|
14
14
|
"heroTrust": "לקוחות מרוצות ברחבי הארץ",
|
|
15
15
|
"benefit1Title": "משלוח מהיר עד הבית",
|
|
@@ -17,37 +17,37 @@
|
|
|
17
17
|
"benefit2Title": "תשלום מאובטח",
|
|
18
18
|
"benefit2Desc": "סליקה מוצפנת בתקן המחמיר ביותר",
|
|
19
19
|
"benefit3Title": "חומרים איכותיים",
|
|
20
|
-
"benefit3Desc": "
|
|
20
|
+
"benefit3Desc": "נבחרים בקפידה, עמידים לאורך זמן",
|
|
21
21
|
"benefit4Title": "החזרה קלה",
|
|
22
22
|
"benefit4Desc": "30 יום להחלפה או החזרה, בלי שאלות",
|
|
23
23
|
"featuredEyebrow": "הנבחרים שלנו",
|
|
24
24
|
"featuredSubtitle": "הפריטים שהלקוחות שלנו הכי אוהבות — נבחרו בקפידה מהקולקציה החדשה.",
|
|
25
25
|
"categoriesEyebrow": "קטגוריות",
|
|
26
|
-
"categoriesTitle": "מצאו את
|
|
27
|
-
"categoriesSubtitle": "
|
|
26
|
+
"categoriesTitle": "מצאו את הפריט המושלם",
|
|
27
|
+
"categoriesSubtitle": "מהפריטים העדינים ועד הבולטים — לכל אחת יש את השפה שלה.",
|
|
28
28
|
"catRings": "טבעות",
|
|
29
29
|
"catNecklaces": "שרשראות",
|
|
30
30
|
"catEarrings": "עגילים",
|
|
31
31
|
"catBracelets": "צמידים",
|
|
32
32
|
"catCta": "לצפייה בקטגוריה",
|
|
33
33
|
"storyEyebrow": "הסטודיו שלנו",
|
|
34
|
-
"storyTitle": "נוצר ביד,
|
|
35
|
-
"storyP1": "כל
|
|
36
|
-
"storyP2": "אנחנו מאמינים
|
|
34
|
+
"storyTitle": "נוצר ביד, נאהב בלב",
|
|
35
|
+
"storyP1": "כל פריט אצלנו מתחיל בסקיצה קטנה על נייר — ונגמר בעבודת יד עדינה בסטודיו. אנחנו בוחרים כל חומר וכל פרט בקפידה, כדי שהפריט שיגיע אליכן יהיה בדיוק כמו שדמיינו אותו.",
|
|
36
|
+
"storyP2": "אנחנו מאמינים בפריטים שנשארים: חומרים אמיתיים, גימור מוקפד ואחריות מלאה על כל פריט. ככה נראית אהבה לפרטים הקטנים.",
|
|
37
37
|
"storyCta": "לגלות את הקולקציה",
|
|
38
38
|
"storyStat1Value": "עבודת יד",
|
|
39
39
|
"storyStat1Label": "כל פריט נוצר בסטודיו שלנו",
|
|
40
|
-
"storyStat2Value": "
|
|
41
|
-
"storyStat2Label": "
|
|
40
|
+
"storyStat2Value": "גימור מוקפד",
|
|
41
|
+
"storyStat2Label": "כל פריט נבדק לפני המשלוח",
|
|
42
42
|
"testimonialsEyebrow": "לקוחות מספרות",
|
|
43
43
|
"testimonialsTitle": "הן כבר התאהבו",
|
|
44
|
-
"testimonial1Quote": "
|
|
44
|
+
"testimonial1Quote": "הפריט הגיע באריזה מהממת ונראה אפילו יותר טוב מבתמונות. קיבלתי מחמאות כבר ביום הראשון.",
|
|
45
45
|
"testimonial1Name": "נועה ל.",
|
|
46
46
|
"testimonial1Meta": "תל אביב",
|
|
47
|
-
"testimonial2Quote": "קניתי
|
|
47
|
+
"testimonial2Quote": "קניתי מתנה לאמא שלי והיא לא מפסיקה להשתמש בו מאז. איכות מדהימה ושירות סופר אישי.",
|
|
48
48
|
"testimonial2Name": "שירה ב.",
|
|
49
49
|
"testimonial2Meta": "חיפה",
|
|
50
|
-
"testimonial3Quote": "
|
|
50
|
+
"testimonial3Quote": "בדיוק כמו שתואר, העבודה עדינה ומוקפדת. מרגישים שכל פרט נעשה באהבה. ממליצה בחום.",
|
|
51
51
|
"testimonial3Name": "דנה כ.",
|
|
52
52
|
"testimonial3Meta": "ירושלים",
|
|
53
53
|
"newsletterTitle": "הצטרפו למועדון שלנו",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"productDetail": {
|
|
69
69
|
"quantityLabel": "כמות",
|
|
70
70
|
"shippingNote": "משלוח מהיר עד הבית · החזרה קלה עד 30 יום",
|
|
71
|
-
"guaranteeNote": "אחריות מלאה על כל
|
|
71
|
+
"guaranteeNote": "אחריות מלאה על כל פריט",
|
|
72
72
|
"pickOne": "בחרו אחד",
|
|
73
73
|
"upTo": "עד {max}",
|
|
74
74
|
"pickExactly": "בחרו בדיוק {min}",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"shippingAtCheckout": "משלוח יחושב בקופה"
|
|
89
89
|
},
|
|
90
90
|
"footer": {
|
|
91
|
-
"tagline": "
|
|
91
|
+
"tagline": "{{storeName}} — עבודת יד, מעוצב באהבה בישראל.",
|
|
92
92
|
"followUs": "עקבו אחרינו",
|
|
93
93
|
"quickLinksTitle": "ניווט מהיר",
|
|
94
94
|
"linkProducts": "כל המוצרים",
|
|
@@ -75,7 +75,7 @@ export function CartDrawer() {
|
|
|
75
75
|
onClick={close}
|
|
76
76
|
aria-hidden="true"
|
|
77
77
|
className={cn(
|
|
78
|
-
'absolute inset-0
|
|
78
|
+
'bg-foreground/35 absolute inset-0 backdrop-blur-[2px] transition-opacity duration-300 motion-reduce:transition-none',
|
|
79
79
|
open ? 'opacity-100' : 'opacity-0'
|
|
80
80
|
)}
|
|
81
81
|
/>
|
|
@@ -86,14 +86,14 @@ export function CartDrawer() {
|
|
|
86
86
|
aria-modal="true"
|
|
87
87
|
aria-label={t('title')}
|
|
88
88
|
className={cn(
|
|
89
|
-
'absolute inset-y-0 end-0 flex w-full max-w-md flex-col
|
|
89
|
+
'bg-background absolute inset-y-0 end-0 flex w-full max-w-md flex-col shadow-2xl transition-transform duration-300 ease-out motion-reduce:transition-none sm:rounded-s-3xl sm:border-s',
|
|
90
90
|
open ? 'translate-x-0' : 'ltr:translate-x-full rtl:-translate-x-full'
|
|
91
91
|
)}
|
|
92
92
|
>
|
|
93
93
|
<header className="flex items-center gap-3 border-b px-5 py-4">
|
|
94
|
-
<h2 className="font-display text-xl font-semibold
|
|
94
|
+
<h2 className="font-display text-foreground text-xl font-semibold">{t('title')}</h2>
|
|
95
95
|
{itemCount > 0 && (
|
|
96
|
-
<span className="flex h-6 min-w-6 items-center justify-center rounded-full
|
|
96
|
+
<span className="bg-primary text-primary-foreground flex h-6 min-w-6 items-center justify-center rounded-full px-1.5 text-xs font-bold">
|
|
97
97
|
{itemCount}
|
|
98
98
|
</span>
|
|
99
99
|
)}
|
|
@@ -102,7 +102,7 @@ export function CartDrawer() {
|
|
|
102
102
|
type="button"
|
|
103
103
|
onClick={close}
|
|
104
104
|
aria-label={tc('close')}
|
|
105
|
-
className="ms-auto flex h-10 w-10 items-center justify-center rounded-full
|
|
105
|
+
className="text-foreground hover:bg-secondary ms-auto flex h-10 w-10 items-center justify-center rounded-full transition-colors"
|
|
106
106
|
>
|
|
107
107
|
<IconX size={20} />
|
|
108
108
|
</button>
|
|
@@ -110,11 +110,11 @@ export function CartDrawer() {
|
|
|
110
110
|
|
|
111
111
|
{items.length === 0 ? (
|
|
112
112
|
<div className="flex flex-1 flex-col items-center justify-center gap-3 px-6 text-center">
|
|
113
|
-
<span className="flex h-16 w-16 items-center justify-center rounded-full
|
|
113
|
+
<span className="bg-secondary text-muted-foreground flex h-16 w-16 items-center justify-center rounded-full">
|
|
114
114
|
<IconBag size={24} />
|
|
115
115
|
</span>
|
|
116
|
-
<p className="font-display text-lg font-semibold
|
|
117
|
-
<p className="max-w-xs text-sm
|
|
116
|
+
<p className="font-display text-foreground text-lg font-semibold">{t('emptyTitle')}</p>
|
|
117
|
+
<p className="text-muted-foreground max-w-xs text-sm">{t('emptySubtitle')}</p>
|
|
118
118
|
<Link href="/products" onClick={close} className="btn-primary btn-sm mt-2">
|
|
119
119
|
{tc('continueShopping')}
|
|
120
120
|
</Link>
|
|
@@ -132,25 +132,25 @@ export function CartDrawer() {
|
|
|
132
132
|
</ul>
|
|
133
133
|
</div>
|
|
134
134
|
|
|
135
|
-
<footer className="
|
|
135
|
+
<footer className="bg-secondary/40 border-t px-5 py-4">
|
|
136
136
|
<div className="flex items-center justify-between text-sm">
|
|
137
137
|
<span className="text-muted-foreground">{tc('subtotal')}</span>
|
|
138
|
-
<strong className="text-
|
|
138
|
+
<strong className="text-foreground text-base">
|
|
139
139
|
{formatPrice(totals.subtotal, { currency }) as string}
|
|
140
140
|
</strong>
|
|
141
141
|
</div>
|
|
142
142
|
{totals.discount > 0 && (
|
|
143
|
-
<div className="mt-1 flex items-center justify-between text-sm
|
|
143
|
+
<div className="text-primary mt-1 flex items-center justify-between text-sm">
|
|
144
144
|
<span>{tc('discount')}</span>
|
|
145
145
|
<span>-{formatPrice(totals.discount, { currency }) as string}</span>
|
|
146
146
|
</div>
|
|
147
147
|
)}
|
|
148
|
-
<p className="mt-1 text-xs
|
|
148
|
+
<p className="text-muted-foreground mt-1 text-xs">{t('shippingAtCheckout')}</p>
|
|
149
149
|
<div className="mt-4 grid gap-2">
|
|
150
|
-
<Link href="/checkout" onClick={close} className="btn-primary w-full">
|
|
150
|
+
<Link href="/checkout" onClick={close} className="btn-primary btn-lg w-full">
|
|
151
151
|
{t('proceedToCheckout')}
|
|
152
152
|
</Link>
|
|
153
|
-
<Link href="/cart" onClick={close} className="btn-outline w-full">
|
|
153
|
+
<Link href="/cart" onClick={close} className="btn-outline btn-lg w-full">
|
|
154
154
|
{t('viewCart')}
|
|
155
155
|
</Link>
|
|
156
156
|
</div>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useTranslations } from '@/core/lib/translations';
|
|
4
|
-
import { IconTruck, IconShield,
|
|
4
|
+
import { IconTruck, IconShield, IconBadgeCheck, IconReturn } from '@/ui/shared/icons';
|
|
5
5
|
|
|
6
6
|
const BENEFITS = [
|
|
7
7
|
{ icon: IconTruck, titleKey: 'benefit1Title', descKey: 'benefit1Desc' },
|
|
8
8
|
{ icon: IconShield, titleKey: 'benefit2Title', descKey: 'benefit2Desc' },
|
|
9
|
-
{ icon:
|
|
9
|
+
{ icon: IconBadgeCheck, titleKey: 'benefit3Title', descKey: 'benefit3Desc' },
|
|
10
10
|
{ icon: IconReturn, titleKey: 'benefit4Title', descKey: 'benefit4Desc' },
|
|
11
11
|
] as const;
|
|
12
12
|
|
|
@@ -19,12 +19,12 @@ export function BenefitsBand() {
|
|
|
19
19
|
<ul className="container-page grid grid-cols-2 gap-x-6 gap-y-8 pb-10 pt-16 lg:grid-cols-4">
|
|
20
20
|
{BENEFITS.map(({ icon: Icon, titleKey, descKey }) => (
|
|
21
21
|
<li key={titleKey} className="flex items-start gap-3.5">
|
|
22
|
-
<span className="flex h-11 w-11 shrink-0 items-center justify-center rounded-full
|
|
22
|
+
<span className="bg-secondary text-primary flex h-11 w-11 shrink-0 items-center justify-center rounded-full">
|
|
23
23
|
<Icon size={24} />
|
|
24
24
|
</span>
|
|
25
25
|
<div>
|
|
26
|
-
<p className="text-sm font-semibold
|
|
27
|
-
<p className="mt-0.5 text-[13px] leading-5
|
|
26
|
+
<p className="text-foreground text-sm font-semibold">{t(titleKey)}</p>
|
|
27
|
+
<p className="text-muted-foreground mt-0.5 text-[13px] leading-5">{t(descKey)}</p>
|
|
28
28
|
</div>
|
|
29
29
|
</li>
|
|
30
30
|
))}
|
|
@@ -35,10 +35,10 @@ export function CategoryTiles({ products }: { products: Product[] }) {
|
|
|
35
35
|
const tiles: Tile[] = [];
|
|
36
36
|
|
|
37
37
|
// Prefer real category data when the catalog has it.
|
|
38
|
-
const byCategory = new Map<string, { name: string; items: Product[] }>();
|
|
38
|
+
const byCategory = new Map<string, { name: string; slug: string | null; items: Product[] }>();
|
|
39
39
|
for (const p of products) {
|
|
40
40
|
for (const cat of p.categories ?? []) {
|
|
41
|
-
const entry = byCategory.get(cat.id) ?? { name: cat.name, items: [] };
|
|
41
|
+
const entry = byCategory.get(cat.id) ?? { name: cat.name, slug: cat.slug ?? null, items: [] };
|
|
42
42
|
entry.items.push(p);
|
|
43
43
|
byCategory.set(cat.id, entry);
|
|
44
44
|
}
|
|
@@ -50,15 +50,21 @@ export function CategoryTiles({ products }: { products: Product[] }) {
|
|
|
50
50
|
[...items].reverse().find((p) => p.images?.[0]?.url)?.images?.[0];
|
|
51
51
|
|
|
52
52
|
if (byCategory.size >= 2) {
|
|
53
|
-
for (const [id, { name, items }] of Array.from(byCategory.entries()).slice(0, 4)) {
|
|
53
|
+
for (const [id, { name, slug, items }] of Array.from(byCategory.entries()).slice(0, 4)) {
|
|
54
54
|
const img = pickImage(items);
|
|
55
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.
|
|
56
60
|
tiles.push({
|
|
57
61
|
key: id,
|
|
58
62
|
label: name,
|
|
59
63
|
imageUrl: img.url,
|
|
60
64
|
imageAlt: img.alt || name,
|
|
61
|
-
href:
|
|
65
|
+
href: slug
|
|
66
|
+
? `/category/${encodeURIComponent(slug)}`
|
|
67
|
+
: `/products?category=${encodeURIComponent(id)}`,
|
|
62
68
|
count: items.length,
|
|
63
69
|
});
|
|
64
70
|
}
|
|
@@ -90,7 +96,7 @@ export function CategoryTiles({ products }: { products: Product[] }) {
|
|
|
90
96
|
<div className="mb-8 max-w-xl lg:mb-10">
|
|
91
97
|
<span className="eyebrow">{t('categoriesEyebrow')}</span>
|
|
92
98
|
<h2 className="text-4xl sm:text-5xl">{t('categoriesTitle')}</h2>
|
|
93
|
-
<p className="
|
|
99
|
+
<p className="text-muted-foreground mt-3">{t('categoriesSubtitle')}</p>
|
|
94
100
|
</div>
|
|
95
101
|
|
|
96
102
|
<ul
|
|
@@ -109,11 +115,8 @@ export function CategoryTiles({ products }: { products: Product[] }) {
|
|
|
109
115
|
: undefined
|
|
110
116
|
}
|
|
111
117
|
>
|
|
112
|
-
<Link
|
|
113
|
-
|
|
114
|
-
className="group card card-hover block overflow-hidden"
|
|
115
|
-
>
|
|
116
|
-
<span className="relative block aspect-square overflow-hidden bg-secondary">
|
|
118
|
+
<Link href={tile.href} className="card card-hover group block overflow-hidden">
|
|
119
|
+
<span className="bg-secondary relative block aspect-square overflow-hidden">
|
|
117
120
|
<Image
|
|
118
121
|
src={tile.imageUrl}
|
|
119
122
|
alt={tile.imageAlt}
|
|
@@ -124,16 +127,16 @@ export function CategoryTiles({ products }: { products: Product[] }) {
|
|
|
124
127
|
</span>
|
|
125
128
|
<span className="flex items-center justify-between gap-2 px-4 py-3.5">
|
|
126
129
|
<span>
|
|
127
|
-
<span className="block text-[15px] font-semibold
|
|
130
|
+
<span className="text-foreground block text-[15px] font-semibold">
|
|
128
131
|
{tile.label}
|
|
129
132
|
</span>
|
|
130
|
-
<span className="block text-xs
|
|
133
|
+
<span className="text-muted-foreground block text-xs">
|
|
131
134
|
{tile.count} {tile.count === 1 ? tc('product') : tc('products')}
|
|
132
135
|
</span>
|
|
133
136
|
</span>
|
|
134
137
|
<span
|
|
135
138
|
aria-hidden="true"
|
|
136
|
-
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full
|
|
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"
|
|
137
140
|
>
|
|
138
141
|
<IconArrowEnd size={16} className="rtl-flip" />
|
|
139
142
|
</span>
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
IconMinus,
|
|
23
23
|
IconPlus,
|
|
24
24
|
IconTruck,
|
|
25
|
-
|
|
25
|
+
IconBadgeCheck,
|
|
26
26
|
IconDownload,
|
|
27
27
|
} from '@/ui/shared/icons';
|
|
28
28
|
|
|
@@ -159,7 +159,10 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
159
159
|
{/* Image Gallery */}
|
|
160
160
|
<figure className="lg:sticky lg:top-24 lg:self-start">
|
|
161
161
|
{/* `relative` + `aspect-square` are layout-critical for next/image fill */}
|
|
162
|
-
<div
|
|
162
|
+
<div
|
|
163
|
+
data-pdp-gallery
|
|
164
|
+
className="bg-secondary relative aspect-square overflow-hidden rounded-3xl border shadow-[0_28px_60px_-30px_hsl(var(--primary)/0.3)]"
|
|
165
|
+
>
|
|
163
166
|
{mainImageUrl ? (
|
|
164
167
|
<Image
|
|
165
168
|
src={mainImageUrl}
|
|
@@ -184,10 +187,10 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
184
187
|
onClick={() => setSelectedImageIndex(idx)}
|
|
185
188
|
aria-pressed={selectedImageIndex === idx}
|
|
186
189
|
className={cn(
|
|
187
|
-
'relative h-[4.5rem] w-[4.5rem] overflow-hidden rounded-lg border-2
|
|
190
|
+
'bg-secondary relative h-[4.5rem] w-[4.5rem] overflow-hidden rounded-lg border-2 transition-colors',
|
|
188
191
|
selectedImageIndex === idx
|
|
189
192
|
? 'border-primary'
|
|
190
|
-
: 'border-
|
|
193
|
+
: 'hover:border-border border-transparent'
|
|
191
194
|
)}
|
|
192
195
|
>
|
|
193
196
|
<Image
|
|
@@ -220,9 +223,9 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
220
223
|
{/* Brand */}
|
|
221
224
|
{(product as { brands?: Array<{ id: string; name: string }> }).brands &&
|
|
222
225
|
(product as { brands: Array<{ id: string; name: string }> }).brands.length > 0 && (
|
|
223
|
-
<p className="mb-1 text-sm
|
|
226
|
+
<p className="text-muted-foreground mb-1 text-sm">
|
|
224
227
|
{t('by')}{' '}
|
|
225
|
-
<span className="font-medium
|
|
228
|
+
<span className="text-foreground font-medium">
|
|
226
229
|
{(product as { brands: Array<{ id: string; name: string }> }).brands
|
|
227
230
|
.map((b) => b.name)
|
|
228
231
|
.join(', ')}
|
|
@@ -237,7 +240,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
237
240
|
{(product as unknown as { tags?: Array<{ id: string; name: string }> }).tags &&
|
|
238
241
|
(product as unknown as { tags: Array<{ id: string; name: string }> }).tags.length >
|
|
239
242
|
0 && (
|
|
240
|
-
<ul className="mt-2 flex flex-wrap gap-1.5 text-xs
|
|
243
|
+
<ul className="text-muted-foreground mt-2 flex flex-wrap gap-1.5 text-xs">
|
|
241
244
|
{(product as unknown as { tags: Array<{ id: string; name: string }> }).tags.map(
|
|
242
245
|
(tag) => (
|
|
243
246
|
<li key={tag.id}>#{tag.name}</li>
|
|
@@ -265,11 +268,11 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
265
268
|
|
|
266
269
|
{/* Downloadable files info */}
|
|
267
270
|
{product.isDownloadable && product.downloads && product.downloads.length > 0 && (
|
|
268
|
-
<section className="mt-6 rounded-xl border
|
|
271
|
+
<section className="bg-secondary/60 mt-6 rounded-xl border p-4">
|
|
269
272
|
<h2 className="text-sm font-semibold">
|
|
270
273
|
{t('filesIncluded').replace('{count}', String(product.downloads.length))}
|
|
271
274
|
</h2>
|
|
272
|
-
<ul className="mt-2 space-y-1.5 text-sm
|
|
275
|
+
<ul className="text-muted-foreground mt-2 space-y-1.5 text-sm">
|
|
273
276
|
{product.downloads.map((file: DownloadFile) => (
|
|
274
277
|
<li key={file.id} className="flex items-center gap-2">
|
|
275
278
|
<IconDownload size={16} />
|
|
@@ -322,7 +325,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
322
325
|
/>
|
|
323
326
|
))}
|
|
324
327
|
{modifierError && (
|
|
325
|
-
<p role="alert" className="text-sm font-medium
|
|
328
|
+
<p role="alert" className="text-destructive text-sm font-medium">
|
|
326
329
|
{modifierError}
|
|
327
330
|
</p>
|
|
328
331
|
)}
|
|
@@ -340,7 +343,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
340
343
|
type="button"
|
|
341
344
|
onClick={() => setQuantity((q) => Math.max(1, q - 1))}
|
|
342
345
|
aria-label={t('decreaseQuantity')}
|
|
343
|
-
className="flex h-full w-11 items-center justify-center rounded-s-full
|
|
346
|
+
className="text-foreground hover:bg-secondary flex h-full w-11 items-center justify-center rounded-s-full transition-colors disabled:opacity-40"
|
|
344
347
|
disabled={quantity <= 1}
|
|
345
348
|
>
|
|
346
349
|
<IconMinus size={16} />
|
|
@@ -352,7 +355,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
352
355
|
type="button"
|
|
353
356
|
onClick={() => setQuantity((q) => q + 1)}
|
|
354
357
|
aria-label={t('increaseQuantity')}
|
|
355
|
-
className="flex h-full w-11 items-center justify-center rounded-e-full
|
|
358
|
+
className="text-foreground hover:bg-secondary flex h-full w-11 items-center justify-center rounded-e-full transition-colors"
|
|
356
359
|
>
|
|
357
360
|
<IconPlus size={16} />
|
|
358
361
|
</button>
|
|
@@ -360,7 +363,10 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
360
363
|
|
|
361
364
|
<button
|
|
362
365
|
type="button"
|
|
363
|
-
onClick={() => {
|
|
366
|
+
onClick={() => {
|
|
367
|
+
flyToCart(firstVisible('[data-pdp-gallery]'));
|
|
368
|
+
handleAddToCart();
|
|
369
|
+
}}
|
|
364
370
|
disabled={!canPurchase || addingToCart}
|
|
365
371
|
className={cn(
|
|
366
372
|
'btn-primary btn-lg flex-1 sm:min-w-64 sm:flex-none',
|
|
@@ -379,7 +385,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
379
385
|
</div>
|
|
380
386
|
|
|
381
387
|
{/* Reassurance row */}
|
|
382
|
-
<ul className="mt-6 space-y-2 border-t pt-5 text-sm
|
|
388
|
+
<ul className="text-muted-foreground mt-6 space-y-2 border-t pt-5 text-sm">
|
|
383
389
|
<li className="flex items-center gap-2.5">
|
|
384
390
|
<span className="text-primary">
|
|
385
391
|
<IconTruck size={20} />
|
|
@@ -388,7 +394,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
388
394
|
</li>
|
|
389
395
|
<li className="flex items-center gap-2.5">
|
|
390
396
|
<span className="text-primary">
|
|
391
|
-
<
|
|
397
|
+
<IconBadgeCheck size={20} />
|
|
392
398
|
</span>
|
|
393
399
|
{t('guaranteeNote')}
|
|
394
400
|
</li>
|
|
@@ -396,7 +402,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
396
402
|
|
|
397
403
|
{/* Download after purchase note */}
|
|
398
404
|
{product.isDownloadable && (
|
|
399
|
-
<p className="mt-4 text-sm
|
|
405
|
+
<p className="text-muted-foreground mt-4 text-sm">{t('downloadAfterPurchase')}</p>
|
|
400
406
|
)}
|
|
401
407
|
|
|
402
408
|
{/* Description */}
|
|
@@ -424,7 +430,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
|
|
|
424
430
|
<tr key={field.id} className="odd:bg-secondary/50">
|
|
425
431
|
<th
|
|
426
432
|
scope="row"
|
|
427
|
-
className="w-1/3 px-4 py-3 text-start font-medium
|
|
433
|
+
className="text-muted-foreground w-1/3 px-4 py-3 text-start font-medium"
|
|
428
434
|
>
|
|
429
435
|
{field.definitionName}
|
|
430
436
|
</th>
|
|
@@ -5,7 +5,7 @@ import { Link } from '@/core/lib/navigation';
|
|
|
5
5
|
import { useTranslations } from '@/core/lib/translations';
|
|
6
6
|
import { ProductCard } from '@/ui/product/product-card';
|
|
7
7
|
import { cn } from '@/core/lib/utils';
|
|
8
|
-
import {
|
|
8
|
+
import { IconSparkle } from '@/ui/shared/icons';
|
|
9
9
|
|
|
10
10
|
interface ProductGridProps {
|
|
11
11
|
products: Product[];
|
|
@@ -20,11 +20,11 @@ export function ProductGrid({ products, className }: ProductGridProps) {
|
|
|
20
20
|
if (products.length === 0) {
|
|
21
21
|
return (
|
|
22
22
|
<div className="card mx-auto flex max-w-md flex-col items-center gap-3 px-8 py-14 text-center">
|
|
23
|
-
<span className="flex h-14 w-14 items-center justify-center rounded-full
|
|
24
|
-
<
|
|
23
|
+
<span className="bg-secondary text-primary flex h-14 w-14 items-center justify-center rounded-full">
|
|
24
|
+
<IconSparkle size={28} />
|
|
25
25
|
</span>
|
|
26
|
-
<p className="font-display text-
|
|
27
|
-
<p className="text-
|
|
26
|
+
<p className="font-display text-foreground text-xl">{t('emptyTitle')}</p>
|
|
27
|
+
<p className="text-muted-foreground text-sm">{t('emptyBody')}</p>
|
|
28
28
|
<Link href="/products" className="btn-soft btn-sm mt-2">
|
|
29
29
|
{tc('viewAll')}
|
|
30
30
|
</Link>
|
|
@@ -33,12 +33,7 @@ export function ProductGrid({ products, className }: ProductGridProps) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
|
-
<div
|
|
37
|
-
className={cn(
|
|
38
|
-
'grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-4 lg:gap-6',
|
|
39
|
-
className
|
|
40
|
-
)}
|
|
41
|
-
>
|
|
36
|
+
<div className={cn('grid grid-cols-2 gap-3 sm:gap-5 lg:grid-cols-4 lg:gap-6', className)}>
|
|
42
37
|
{products.map((product) => (
|
|
43
38
|
<ProductCard key={product.id} product={product} />
|
|
44
39
|
))}
|