create-brainerce-store 1.51.0 → 1.52.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/README.md +72 -0
- package/dist/index.js +9 -68
- package/messages/en.json +32 -0
- package/messages/he.json +32 -0
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +9 -0
- package/templates/nextjs/base/AI-GUIDE.md +52 -3
- package/templates/nextjs/base/CLAUDE.md +10 -0
- package/templates/nextjs/base/package.json.ejs +52 -39
- package/templates/nextjs/base/src/app/globals.css +36 -30
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
- package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
- package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
- package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
- package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
- package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
- package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
- package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
- package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
- package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
- package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
- package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
- package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
- package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
- package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
- package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
- package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
- package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
- package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
- package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
- package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
- package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
- package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
- package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
- package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
- package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
- package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
- package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
- package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
- package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
- package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
- package/templates/nextjs/base/tailwind.config.ts +69 -42
- package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
- package/templates/nextjs/designs/atelier/globals.css +396 -0
- package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
- package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
- package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
- package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
- package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
- package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
- package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
- package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
- package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
- package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
- package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
- package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
- package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
- package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
- package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
- package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
- package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
- package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
- package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
- package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
- package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
- package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
- package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
- package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
- package/templates/nextjs/themes/luxury/globals.css +405 -399
- package/templates/nextjs/themes/minimal/globals.css +36 -30
- package/templates/nextjs/themes/playful/globals.css +406 -400
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
- package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
- package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
- package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
- package/templates/nextjs/designs/passepartout/globals.css +0 -412
- package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
- package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
- package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
- package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
- package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
- package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
- package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
- package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
- package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
- package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
- package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
- package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
- package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
- package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
- package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
- package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
- package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
- package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
- package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
- package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
- package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
import { ChevronDown } from 'lucide-react';
|
|
4
5
|
import {
|
|
5
6
|
useProductListing,
|
|
6
7
|
sortOptions,
|
|
@@ -9,27 +10,22 @@ import {
|
|
|
9
10
|
} from '@/core/hooks/use-product-listing';
|
|
10
11
|
import { ProductGrid } from '@/ui/product/product-grid';
|
|
11
12
|
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
13
|
+
import { Button } from '@/components/ui/button';
|
|
14
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
15
|
+
import { Label } from '@/components/ui/label';
|
|
16
|
+
import {
|
|
17
|
+
Select,
|
|
18
|
+
SelectContent,
|
|
19
|
+
SelectItem,
|
|
20
|
+
SelectTrigger,
|
|
21
|
+
SelectValue,
|
|
22
|
+
} from '@/components/ui/select';
|
|
12
23
|
import { useTranslations } from '@/core/lib/translations';
|
|
13
24
|
import { cn } from '@/core/lib/utils';
|
|
14
25
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<svg
|
|
19
|
-
className={className}
|
|
20
|
-
width="12"
|
|
21
|
-
height="12"
|
|
22
|
-
viewBox="0 0 12 12"
|
|
23
|
-
fill="none"
|
|
24
|
-
stroke="currentColor"
|
|
25
|
-
strokeWidth="2"
|
|
26
|
-
strokeLinecap="round"
|
|
27
|
-
strokeLinejoin="round"
|
|
28
|
-
>
|
|
29
|
-
<path d="M3 4.5L6 7.5L9 4.5" />
|
|
30
|
-
</svg>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
26
|
+
/** Radix Select items can't carry an empty-string value, so "no filter"
|
|
27
|
+
* round-trips through this sentinel and is written back as ''. */
|
|
28
|
+
const ALL = '__all__';
|
|
33
29
|
|
|
34
30
|
/** Recursive dropdown items for nested categories */
|
|
35
31
|
function CategoryDropdownItems({
|
|
@@ -146,11 +142,14 @@ function CategoryChip({
|
|
|
146
142
|
{'·'} {selectedChildName}
|
|
147
143
|
</span>
|
|
148
144
|
)}
|
|
149
|
-
<ChevronDown
|
|
145
|
+
<ChevronDown
|
|
146
|
+
className={cn('ms-0.5 h-3 w-3 transition-transform', open && 'rotate-180')}
|
|
147
|
+
aria-hidden="true"
|
|
148
|
+
/>
|
|
150
149
|
</button>
|
|
151
150
|
|
|
152
151
|
{open && (
|
|
153
|
-
<div className="bg-
|
|
152
|
+
<div className="bg-popover text-popover-foreground border-border absolute start-0 top-full z-50 mt-1 min-w-[180px] overflow-hidden rounded-lg border shadow-lg">
|
|
154
153
|
{/* "All in [category]" option */}
|
|
155
154
|
<button
|
|
156
155
|
onClick={() => {
|
|
@@ -258,34 +257,42 @@ export function ProductListing() {
|
|
|
258
257
|
<div className="flex flex-wrap items-center gap-3">
|
|
259
258
|
{/* Brand Filter */}
|
|
260
259
|
{brands.length > 0 && (
|
|
261
|
-
<
|
|
262
|
-
value={brandId}
|
|
263
|
-
|
|
264
|
-
className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-9 rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
260
|
+
<Select
|
|
261
|
+
value={brandId || ALL}
|
|
262
|
+
onValueChange={(v) => updateParam('brand', v === ALL ? '' : v)}
|
|
265
263
|
>
|
|
266
|
-
<
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
</
|
|
271
|
-
|
|
272
|
-
|
|
264
|
+
<SelectTrigger className="h-9 w-auto min-w-[8rem] rounded" aria-label={t('allBrands')}>
|
|
265
|
+
<SelectValue />
|
|
266
|
+
</SelectTrigger>
|
|
267
|
+
<SelectContent>
|
|
268
|
+
<SelectItem value={ALL}>{t('allBrands')}</SelectItem>
|
|
269
|
+
{brands.map((b) => (
|
|
270
|
+
<SelectItem key={b.id} value={b.id}>
|
|
271
|
+
{b.name}
|
|
272
|
+
</SelectItem>
|
|
273
|
+
))}
|
|
274
|
+
</SelectContent>
|
|
275
|
+
</Select>
|
|
273
276
|
)}
|
|
274
277
|
|
|
275
278
|
{/* Tag Filter */}
|
|
276
279
|
{tags.length > 0 && (
|
|
277
|
-
<
|
|
278
|
-
value={tagId}
|
|
279
|
-
|
|
280
|
-
className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-9 rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
280
|
+
<Select
|
|
281
|
+
value={tagId || ALL}
|
|
282
|
+
onValueChange={(v) => updateParam('tag', v === ALL ? '' : v)}
|
|
281
283
|
>
|
|
282
|
-
<
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
</
|
|
287
|
-
|
|
288
|
-
|
|
284
|
+
<SelectTrigger className="h-9 w-auto min-w-[8rem] rounded" aria-label={t('allTags')}>
|
|
285
|
+
<SelectValue />
|
|
286
|
+
</SelectTrigger>
|
|
287
|
+
<SelectContent>
|
|
288
|
+
<SelectItem value={ALL}>{t('allTags')}</SelectItem>
|
|
289
|
+
{tags.map((tg) => (
|
|
290
|
+
<SelectItem key={tg.id} value={tg.id}>
|
|
291
|
+
{tg.name}
|
|
292
|
+
</SelectItem>
|
|
293
|
+
))}
|
|
294
|
+
</SelectContent>
|
|
295
|
+
</Select>
|
|
289
296
|
)}
|
|
290
297
|
|
|
291
298
|
{/* Custom-field (metafield) facets — rendered per definition type.
|
|
@@ -299,39 +306,43 @@ export function ProductListing() {
|
|
|
299
306
|
if (def.type === 'BOOLEAN') {
|
|
300
307
|
const isOn = selected.includes('true');
|
|
301
308
|
return (
|
|
302
|
-
<
|
|
309
|
+
<Label
|
|
303
310
|
key={def.id}
|
|
304
|
-
className="border-border text-foreground hover:border-primary inline-flex h-9 cursor-pointer items-center gap-2 rounded border px-3 text-sm transition-colors"
|
|
311
|
+
className="border-border text-foreground hover:border-primary inline-flex h-9 cursor-pointer items-center gap-2 rounded border px-3 text-sm font-normal transition-colors"
|
|
305
312
|
>
|
|
306
|
-
<
|
|
307
|
-
type="checkbox"
|
|
313
|
+
<Checkbox
|
|
308
314
|
checked={isOn}
|
|
309
|
-
|
|
310
|
-
|
|
315
|
+
onCheckedChange={(checked) =>
|
|
316
|
+
setMetafieldValue(def.key, checked === true ? ['true'] : [])
|
|
317
|
+
}
|
|
311
318
|
/>
|
|
312
319
|
{def.name}
|
|
313
|
-
</
|
|
320
|
+
</Label>
|
|
314
321
|
);
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
if (def.type === 'SELECT') {
|
|
318
325
|
return (
|
|
319
|
-
<
|
|
326
|
+
<Select
|
|
320
327
|
key={def.id}
|
|
321
|
-
value={selected[0] ||
|
|
322
|
-
|
|
323
|
-
setMetafieldValue(def.key, e.target.value ? [e.target.value] : [])
|
|
324
|
-
}
|
|
325
|
-
aria-label={def.name}
|
|
326
|
-
className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-9 rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
328
|
+
value={selected[0] || ALL}
|
|
329
|
+
onValueChange={(v) => setMetafieldValue(def.key, v === ALL ? [] : [v])}
|
|
327
330
|
>
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
<SelectTrigger
|
|
332
|
+
className="h-9 w-auto min-w-[8rem] rounded"
|
|
333
|
+
aria-label={def.name}
|
|
334
|
+
>
|
|
335
|
+
<SelectValue />
|
|
336
|
+
</SelectTrigger>
|
|
337
|
+
<SelectContent>
|
|
338
|
+
<SelectItem value={ALL}>{`${tc('all')} ${def.name}`}</SelectItem>
|
|
339
|
+
{(def.enumValues || []).map((v) => (
|
|
340
|
+
<SelectItem key={v.value} value={v.value}>
|
|
341
|
+
{v.label || v.value}
|
|
342
|
+
</SelectItem>
|
|
343
|
+
))}
|
|
344
|
+
</SelectContent>
|
|
345
|
+
</Select>
|
|
335
346
|
);
|
|
336
347
|
}
|
|
337
348
|
|
|
@@ -372,21 +383,27 @@ export function ProductListing() {
|
|
|
372
383
|
|
|
373
384
|
{/* Sort */}
|
|
374
385
|
<div className="flex items-center gap-2 sm:ms-auto">
|
|
375
|
-
<
|
|
386
|
+
<Label
|
|
387
|
+
htmlFor="sort"
|
|
388
|
+
className="text-muted-foreground whitespace-nowrap font-normal"
|
|
389
|
+
>
|
|
376
390
|
{tc('sortBy')}
|
|
377
|
-
</
|
|
378
|
-
<
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
onChange={(e) => updateParam('sort', e.target.value)}
|
|
382
|
-
className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-9 rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
391
|
+
</Label>
|
|
392
|
+
<Select
|
|
393
|
+
value={String(sortIndex)}
|
|
394
|
+
onValueChange={(v) => updateParam('sort', v)}
|
|
383
395
|
>
|
|
384
|
-
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
396
|
+
<SelectTrigger id="sort" className="h-9 w-auto min-w-[8rem] rounded">
|
|
397
|
+
<SelectValue />
|
|
398
|
+
</SelectTrigger>
|
|
399
|
+
<SelectContent>
|
|
400
|
+
{sortOptions.map((opt, idx) => (
|
|
401
|
+
<SelectItem key={idx} value={String(idx)}>
|
|
402
|
+
{t(opt.labelKey)}
|
|
403
|
+
</SelectItem>
|
|
404
|
+
))}
|
|
405
|
+
</SelectContent>
|
|
406
|
+
</Select>
|
|
390
407
|
</div>
|
|
391
408
|
</div>
|
|
392
409
|
</div>
|
|
@@ -403,10 +420,10 @@ export function ProductListing() {
|
|
|
403
420
|
{/* Load More */}
|
|
404
421
|
{page < totalPages && (
|
|
405
422
|
<div className="mt-10 flex justify-center">
|
|
406
|
-
<
|
|
423
|
+
<Button
|
|
407
424
|
onClick={handleLoadMore}
|
|
408
425
|
disabled={loadingMore}
|
|
409
|
-
className="
|
|
426
|
+
className="rounded px-6 py-2.5"
|
|
410
427
|
>
|
|
411
428
|
{loadingMore ? (
|
|
412
429
|
<>
|
|
@@ -419,7 +436,7 @@ export function ProductListing() {
|
|
|
419
436
|
) : (
|
|
420
437
|
t('loadMore')
|
|
421
438
|
)}
|
|
422
|
-
</
|
|
439
|
+
</Button>
|
|
423
440
|
</div>
|
|
424
441
|
)}
|
|
425
442
|
</>
|
|
@@ -1,107 +1,102 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { Link } from '@/core/lib/navigation';
|
|
5
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
6
|
-
import type { ProductRecommendation } from 'brainerce';
|
|
7
|
-
import { PriceDisplay } from '@/ui/product/price-display';
|
|
8
|
-
import { cn } from '@/core/lib/utils';
|
|
9
|
-
|
|
10
|
-
interface RecommendationCardProps {
|
|
11
|
-
item: ProductRecommendation;
|
|
12
|
-
className?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function RecommendationCard({ item, className }: RecommendationCardProps) {
|
|
16
|
-
const firstImage = item.images?.[0];
|
|
17
|
-
const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
|
|
18
|
-
const slug = item.slug || item.id;
|
|
19
|
-
const basePrice = parseFloat(item.basePrice);
|
|
20
|
-
const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
|
|
21
|
-
const isOnSale = salePrice != null && salePrice < basePrice;
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<Link
|
|
25
|
-
href={`/products/${slug}`}
|
|
26
|
-
className={cn(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
items
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
))}
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
);
|
|
107
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Image as ImageIcon } from 'lucide-react';
|
|
4
|
+
import { Link } from '@/core/lib/navigation';
|
|
5
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
6
|
+
import type { ProductRecommendation } from 'brainerce';
|
|
7
|
+
import { PriceDisplay } from '@/ui/product/price-display';
|
|
8
|
+
import { cn } from '@/core/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface RecommendationCardProps {
|
|
11
|
+
item: ProductRecommendation;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function RecommendationCard({ item, className }: RecommendationCardProps) {
|
|
16
|
+
const firstImage = item.images?.[0];
|
|
17
|
+
const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
|
|
18
|
+
const slug = item.slug || item.id;
|
|
19
|
+
const basePrice = parseFloat(item.basePrice);
|
|
20
|
+
const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
|
|
21
|
+
const isOnSale = salePrice != null && salePrice < basePrice;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Link
|
|
25
|
+
href={`/products/${slug}`}
|
|
26
|
+
className={cn(
|
|
27
|
+
// Card-equivalent shell (bg-card + border + rounded-lg) as a link —
|
|
28
|
+
// the whole recommendation tile is clickable.
|
|
29
|
+
'bg-card text-card-foreground group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
>
|
|
33
|
+
<div className="bg-muted relative aspect-square overflow-hidden">
|
|
34
|
+
{imageUrl ? (
|
|
35
|
+
<Image
|
|
36
|
+
src={imageUrl}
|
|
37
|
+
alt={item.name}
|
|
38
|
+
fill
|
|
39
|
+
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
|
|
40
|
+
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
|
|
44
|
+
<ImageIcon className="h-10 w-10" strokeWidth={1.5} aria-hidden="true" />
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
<div className="space-y-1.5 p-3">
|
|
49
|
+
<h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
|
|
50
|
+
{item.name}
|
|
51
|
+
</h3>
|
|
52
|
+
<PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
|
|
53
|
+
</div>
|
|
54
|
+
</Link>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RecommendationSectionProps {
|
|
59
|
+
title: string;
|
|
60
|
+
items: ProductRecommendation[];
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
|
|
65
|
+
if (items.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={cn('', className)}>
|
|
69
|
+
<h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
|
|
70
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
|
71
|
+
{items.map((item) => (
|
|
72
|
+
<RecommendationCard key={item.id} item={item} />
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface CartRecommendationSectionProps {
|
|
80
|
+
title: string;
|
|
81
|
+
items: ProductRecommendation[];
|
|
82
|
+
className?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function CartRecommendationSection({
|
|
86
|
+
title,
|
|
87
|
+
items,
|
|
88
|
+
className,
|
|
89
|
+
}: CartRecommendationSectionProps) {
|
|
90
|
+
if (items.length === 0) return null;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={cn('', className)}>
|
|
94
|
+
<h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
|
|
95
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
96
|
+
{items.map((item) => (
|
|
97
|
+
<RecommendationCard key={item.id} item={item} />
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|