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.
Files changed (170) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +9 -68
  3. package/messages/en.json +32 -0
  4. package/messages/he.json +32 -0
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.claude/commands/design.md +9 -0
  7. package/templates/nextjs/base/AI-GUIDE.md +52 -3
  8. package/templates/nextjs/base/CLAUDE.md +10 -0
  9. package/templates/nextjs/base/package.json.ejs +52 -39
  10. package/templates/nextjs/base/src/app/globals.css +36 -30
  11. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
  12. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
  13. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
  14. package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
  15. package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
  16. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
  17. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
  18. package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
  19. package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
  20. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
  21. package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
  22. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
  23. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
  24. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
  25. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
  26. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
  27. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
  28. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
  29. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
  30. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
  31. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
  32. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
  33. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
  34. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
  35. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
  37. package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
  38. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
  39. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
  40. package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
  41. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
  42. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
  43. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
  44. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
  45. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
  46. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
  47. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
  48. package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
  49. package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
  50. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
  51. package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
  52. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
  53. package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
  54. package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
  55. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
  56. package/templates/nextjs/base/tailwind.config.ts +69 -42
  57. package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
  58. package/templates/nextjs/designs/atelier/globals.css +396 -0
  59. package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
  60. package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
  61. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
  62. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
  63. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
  64. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
  65. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
  66. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
  67. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
  68. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
  69. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
  70. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
  71. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
  72. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
  73. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
  74. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
  75. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
  76. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
  77. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
  78. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
  79. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
  80. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
  81. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
  82. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
  83. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
  84. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
  85. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
  86. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
  87. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
  88. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
  89. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
  90. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
  91. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
  92. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
  93. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
  94. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
  95. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
  96. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
  97. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
  98. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
  99. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
  100. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
  101. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
  102. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
  103. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
  104. package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
  105. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
  106. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
  107. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
  108. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
  109. package/templates/nextjs/themes/luxury/globals.css +405 -399
  110. package/templates/nextjs/themes/minimal/globals.css +36 -30
  111. package/templates/nextjs/themes/playful/globals.css +406 -400
  112. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
  113. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
  114. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
  115. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
  116. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
  117. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
  118. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
  119. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
  120. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
  121. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
  122. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
  123. package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
  124. package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
  125. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
  126. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
  127. package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
  128. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
  129. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
  130. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
  131. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
  132. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
  133. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
  134. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
  135. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
  136. package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
  137. package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
  138. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
  139. package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
  140. package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
  141. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
  142. package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
  143. package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
  144. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
  145. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
  146. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
  147. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
  148. package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
  149. package/templates/nextjs/designs/passepartout/globals.css +0 -412
  150. package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
  151. package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
  152. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
  153. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
  154. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
  155. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
  156. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
  157. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
  158. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
  159. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
  160. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
  161. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
  162. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
  163. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
  164. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
  165. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
  166. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
  167. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
  168. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
  169. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
  170. 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
- /** Chevron down SVG */
16
- function ChevronDown({ className }: { className?: string }) {
17
- return (
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 className={cn('ms-0.5 transition-transform', open && 'rotate-180')} />
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-background border-border absolute start-0 top-full z-50 mt-1 min-w-[180px] overflow-hidden rounded-lg border shadow-lg">
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
- <select
262
- value={brandId}
263
- onChange={(e) => updateParam('brand', e.target.value)}
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
- <option value="">{t('allBrands')}</option>
267
- {brands.map((b) => (
268
- <option key={b.id} value={b.id}>
269
- {b.name}
270
- </option>
271
- ))}
272
- </select>
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
- <select
278
- value={tagId}
279
- onChange={(e) => updateParam('tag', e.target.value)}
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
- <option value="">{t('allTags')}</option>
283
- {tags.map((tg) => (
284
- <option key={tg.id} value={tg.id}>
285
- {tg.name}
286
- </option>
287
- ))}
288
- </select>
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
- <label
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
- <input
307
- type="checkbox"
313
+ <Checkbox
308
314
  checked={isOn}
309
- onChange={(e) => setMetafieldValue(def.key, e.target.checked ? ['true'] : [])}
310
- className="accent-primary h-4 w-4"
315
+ onCheckedChange={(checked) =>
316
+ setMetafieldValue(def.key, checked === true ? ['true'] : [])
317
+ }
311
318
  />
312
319
  {def.name}
313
- </label>
320
+ </Label>
314
321
  );
315
322
  }
316
323
 
317
324
  if (def.type === 'SELECT') {
318
325
  return (
319
- <select
326
+ <Select
320
327
  key={def.id}
321
- value={selected[0] || ''}
322
- onChange={(e) =>
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
- <option value="">{`${tc('all')} ${def.name}`}</option>
329
- {(def.enumValues || []).map((v) => (
330
- <option key={v.value} value={v.value}>
331
- {v.label || v.value}
332
- </option>
333
- ))}
334
- </select>
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
- <label htmlFor="sort" className="text-muted-foreground whitespace-nowrap text-sm">
386
+ <Label
387
+ htmlFor="sort"
388
+ className="text-muted-foreground whitespace-nowrap font-normal"
389
+ >
376
390
  {tc('sortBy')}
377
- </label>
378
- <select
379
- id="sort"
380
- value={sortIndex}
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
- {sortOptions.map((opt, idx) => (
385
- <option key={idx} value={idx}>
386
- {t(opt.labelKey)}
387
- </option>
388
- ))}
389
- </select>
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
- <button
423
+ <Button
407
424
  onClick={handleLoadMore}
408
425
  disabled={loadingMore}
409
- className="bg-primary text-primary-foreground inline-flex items-center gap-2 rounded px-6 py-2.5 font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
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
- </button>
439
+ </Button>
423
440
  </div>
424
441
  )}
425
442
  </>
@@ -1,107 +1,102 @@
1
- 'use client';
2
-
3
- import { useEffect, useState } from '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
- 'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
28
- className
29
- )}
30
- >
31
- <div className="bg-muted relative aspect-square overflow-hidden">
32
- {imageUrl ? (
33
- <Image
34
- src={imageUrl}
35
- alt={item.name}
36
- fill
37
- sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
38
- className="object-cover transition-transform duration-300 group-hover:scale-105"
39
- />
40
- ) : (
41
- <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
42
- <svg className="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
43
- <path
44
- strokeLinecap="round"
45
- strokeLinejoin="round"
46
- strokeWidth={1.5}
47
- d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
48
- />
49
- </svg>
50
- </div>
51
- )}
52
- </div>
53
- <div className="space-y-1.5 p-3">
54
- <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
55
- {item.name}
56
- </h3>
57
- <PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
58
- </div>
59
- </Link>
60
- );
61
- }
62
-
63
- interface RecommendationSectionProps {
64
- title: string;
65
- items: ProductRecommendation[];
66
- className?: string;
67
- }
68
-
69
- export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
70
- if (items.length === 0) return null;
71
-
72
- return (
73
- <div className={cn('', className)}>
74
- <h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
75
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
76
- {items.map((item) => (
77
- <RecommendationCard key={item.id} item={item} />
78
- ))}
79
- </div>
80
- </div>
81
- );
82
- }
83
-
84
- interface CartRecommendationSectionProps {
85
- title: string;
86
- items: ProductRecommendation[];
87
- className?: string;
88
- }
89
-
90
- export function CartRecommendationSection({
91
- title,
92
- items,
93
- className,
94
- }: CartRecommendationSectionProps) {
95
- if (items.length === 0) return null;
96
-
97
- return (
98
- <div className={cn('', className)}>
99
- <h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
100
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
101
- {items.map((item) => (
102
- <RecommendationCard key={item.id} item={item} />
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
+ }