create-brainerce-store 1.51.0 → 1.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +71 -0
  2. package/dist/index.js +24 -80
  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/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
  110. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
  111. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
  112. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
  113. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
  114. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
  115. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
  116. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
  117. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
  118. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
  119. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
  120. package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
  121. package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
  122. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
  123. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
  124. package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
  125. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
  126. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
  127. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
  128. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
  129. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
  130. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
  131. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
  132. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
  133. package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
  134. package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
  135. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
  136. package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
  137. package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
  138. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
  139. package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
  140. package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
  141. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
  142. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
  143. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
  144. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
  145. package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
  146. package/templates/nextjs/designs/passepartout/globals.css +0 -412
  147. package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
  148. package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
  149. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
  150. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
  151. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
  152. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
  153. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
  154. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
  155. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
  156. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
  157. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
  158. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
  159. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
  160. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
  161. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
  162. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
  163. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
  164. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
  165. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
  166. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
  167. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
  168. package/templates/nextjs/themes/luxury/globals.css +0 -399
  169. package/templates/nextjs/themes/luxury/theme.json +0 -23
  170. package/templates/nextjs/themes/minimal/globals.css +0 -30
  171. package/templates/nextjs/themes/minimal/theme.json +0 -23
  172. package/templates/nextjs/themes/playful/globals.css +0 -400
  173. package/templates/nextjs/themes/playful/theme.json +0 -23
@@ -73,7 +73,7 @@ export function AnnouncementBar({ announcements }: AnnouncementBarProps) {
73
73
 
74
74
  return (
75
75
  <div role="region" aria-label="Site announcements">
76
- {/* DESIGN ME — announcement bar(s) at the very top of every page; data.severity + message + optional CTA come from Content<'ANNOUNCEMENT'>[] pre-fetched in the layout */}
76
+ {/* DESIGN ME — announcement bar(s) at the very top of every page; data.severity + message + optional CTA come from Content<'ANNOUNCEMENT'>[] pre-fetched in the layout. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
77
77
  {visible.map((announcement) => (
78
78
  <div key={announcement.id} data-severity={announcement.data.severity} className="flex items-center gap-3">
79
79
  <span>
@@ -40,7 +40,7 @@ export function FaqSection({ faq }: FaqSectionProps) {
40
40
  if (!faq || items.length === 0) {
41
41
  return (
42
42
  <section>
43
- {/* DESIGN ME — FAQ empty state: heading + contact CTA; fed by Content<'FAQ'> pre-fetched in the /faq route */}
43
+ {/* DESIGN ME — FAQ empty state: heading + contact CTA; fed by Content<'FAQ'> pre-fetched in the /faq route. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
44
44
  <h1>{heading}</h1>
45
45
  <p>
46
46
  <strong>{t('faqEmptyTitle')}</strong>
@@ -54,7 +54,7 @@ export function FaqSection({ faq }: FaqSectionProps) {
54
54
 
55
55
  return (
56
56
  <section>
57
- {/* DESIGN ME — FAQ accordion: question/answer pairs from Content<'FAQ'> pre-fetched in the /faq route; answers are sanitized merchant HTML */}
57
+ {/* DESIGN ME — FAQ accordion: question/answer pairs from Content<'FAQ'> pre-fetched in the /faq route; answers are sanitized merchant HTML. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
58
58
  <h1>{heading}</h1>
59
59
  <ul>
60
60
  {items.map((item, idx) => (
@@ -24,7 +24,7 @@ export function HeaderAccount() {
24
24
 
25
25
  return (
26
26
  <Link href={href} aria-label={label}>
27
- {/* DESIGN ME — header account link; auth state via useAuth(), label via the `nav` translation namespace */}
27
+ {/* DESIGN ME — header account link; auth state via useAuth(), label via the `nav` translation namespace. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
28
28
  {label}
29
29
  </Link>
30
30
  );
@@ -35,6 +35,8 @@ const defaultLocale = '<%= defaultLocale %>';
35
35
  * DESIGN ME — locale switcher in the header; reads supported locales from
36
36
  * `@/i18n` and swaps the URL prefix. Keep the aria-label and the redirect
37
37
  * logic exactly as-is.
38
+ *
39
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
38
40
  */
39
41
  export function LanguageSwitcher() {
40
42
  const pathname = usePathname();
@@ -14,6 +14,8 @@
14
14
  * DESIGN ME — merchant-editable HTML block; fed by
15
15
  * `client.content.richText.get(key, locale)` pre-fetched in the route.
16
16
  * Style the injected HTML via the wrapper `className`.
17
+ *
18
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
17
19
  */
18
20
  import * as React from 'react';
19
21
  import type { Content } from 'brainerce';
@@ -33,7 +33,7 @@ export function SiteFooter({ footer, storeName }: SiteFooterProps) {
33
33
  if (!data || (columns.length === 0 && !data.copyright && social.length === 0)) {
34
34
  return (
35
35
  <footer>
36
- {/* DESIGN ME — footer (fallback branch): copyright only; Content<'FOOTER'> pre-fetched in the layout */}
36
+ {/* DESIGN ME — footer (fallback branch): copyright only; Content<'FOOTER'> pre-fetched in the layout. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
37
37
  <p>
38
38
  © {year} {brandLabel}. All rights reserved.
39
39
  </p>
@@ -43,7 +43,7 @@ export function SiteFooter({ footer, storeName }: SiteFooterProps) {
43
43
 
44
44
  return (
45
45
  <footer>
46
- {/* DESIGN ME — footer: brand block, merchant link columns and social links (Content<'FOOTER'> pre-fetched in the layout), copyright bar */}
46
+ {/* DESIGN ME — footer: brand block, merchant link columns and social links (Content<'FOOTER'> pre-fetched in the layout), copyright bar. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
47
47
  <div>
48
48
  <a href="/">{brandLabel}</a>
49
49
  {social.length > 0 ? (
@@ -41,7 +41,7 @@ export function SiteHeader({ header, storeName }: SiteHeaderProps) {
41
41
  if (!header) {
42
42
  return (
43
43
  <header>
44
- {/* DESIGN ME — site header (fallback branch, no merchant nav yet): brand link + account + cart; data comes pre-fetched from the layout via the Content API */}
44
+ {/* DESIGN ME — site header (fallback branch, no merchant nav yet): brand link + account + cart; data comes pre-fetched from the layout via the Content API. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
45
45
  <div className="flex items-center justify-between gap-4">
46
46
  <a href="/" aria-label={brandLabel}>
47
47
  {brandLabel}
@@ -62,7 +62,7 @@ export function SiteHeader({ header, storeName }: SiteHeaderProps) {
62
62
 
63
63
  return (
64
64
  <header>
65
- {/* DESIGN ME — site header: merchant-configured logo/nav/CTA (Content<'HEADER'> pre-fetched in the layout) + account + cart links. Keep every link working. */}
65
+ {/* DESIGN ME — site header: merchant-configured logo/nav/CTA (Content<'HEADER'> pre-fetched in the layout) + account + cart links. Keep every link working. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
66
66
  <div className="flex items-center justify-between gap-4">
67
67
  <a href="/" className="flex items-center gap-2" aria-label={brandLabel}>
68
68
  {logo ? (
@@ -24,6 +24,8 @@ interface CustomizationFieldsProps {
24
24
  * (engraving text, gift date, uploaded artwork, …). Field definitions,
25
25
  * values and validation errors come from useProductPage(); every input type
26
26
  * below must stay functional (including the image/gallery uploads).
27
+ *
28
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
27
29
  */
28
30
  export function CustomizationFields({
29
31
  fields,
@@ -9,6 +9,8 @@ interface DiscountBadgeProps {
9
9
  /**
10
10
  * DESIGN ME — discount-rule badge shown on product cards; text comes from
11
11
  * the product's `discount.badgeText` (merchant-configured rule).
12
+ *
13
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
12
14
  */
13
15
  export function DiscountBadge({ discount, className }: DiscountBadgeProps) {
14
16
  if (!discount) return null;
@@ -56,6 +56,8 @@ function ProductThumb({
56
56
  * current product + selectable cross-sells + combined total + add-all button;
57
57
  * items come from useProductPage().recommendations.crossSells, the feature
58
58
  * gate from useStoreInfo().upsell.
59
+ *
60
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
59
61
  */
60
62
  export function FrequentlyBoughtTogether({
61
63
  items,
@@ -19,6 +19,8 @@
19
19
  * The price-delta side: this component does NOT compute totals. The server
20
20
  * runs the free-allocation policy and returns the line's `unitPrice` /
21
21
  * `modifiers[]` / `modifiersTotal` snapshot — render those after add-to-cart.
22
+ *
23
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
22
24
  */
23
25
 
24
26
  import type { Modifier, ModifierGroup } from 'brainerce';
@@ -16,6 +16,8 @@ interface PriceDisplayProps {
16
16
  * DESIGN ME — price renderer used by cards, product page and recommendations;
17
17
  * computes sale state + discount % from props (currency via useCurrency()).
18
18
  * Never hardcode currency symbols — formatPrice handles them.
19
+ *
20
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
19
21
  */
20
22
  export function PriceDisplay({
21
23
  price,
@@ -130,7 +130,7 @@ export function ProductCard({ product, className }: ProductCardProps) {
130
130
 
131
131
  return (
132
132
  <article className={cn(className)}>
133
- {/* DESIGN ME — product card (grid/listing unit): image, badges, name, price, stock, add-to-cart; product data arrives as a prop from useHomeData()/useProductListing() */}
133
+ {/* DESIGN ME — product card (grid/listing unit): image, badges, name, price, stock, add-to-cart; product data arrives as a prop from useHomeData()/useProductListing(). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
134
134
  <figure>
135
135
  <Link href={`/products/${slug}`}>
136
136
  {/* `relative` + `aspect-square` are layout-critical for next/image fill */}
@@ -108,7 +108,7 @@ export function ProductClientSection({ product: initialProduct }: ProductClientS
108
108
 
109
109
  return (
110
110
  <article>
111
- {/* DESIGN ME — product page (PDP): gallery, buy box (price/stock/variants/customization/modifiers/quantity/add-to-cart), description, specs, cross-sells; everything comes from useProductPage(initialProduct) */}
111
+ {/* DESIGN ME — product page (PDP): gallery, buy box (price/stock/variants/customization/modifiers/quantity/add-to-cart), description, specs, cross-sells; everything comes from useProductPage(initialProduct). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
112
112
  <div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
113
113
  {/* Image Gallery */}
114
114
  <figure>
@@ -18,7 +18,7 @@ export function ProductGrid({ products, className }: ProductGridProps) {
18
18
 
19
19
  return (
20
20
  <div className={cn('grid grid-cols-2 gap-4 lg:grid-cols-4', className)}>
21
- {/* DESIGN ME — product grid: lays out ProductCard units; products arrive as a prop from the listing/home hooks */}
21
+ {/* DESIGN ME — product grid: lays out ProductCard units; products arrive as a prop from the listing/home hooks. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
22
22
  {products.map((product) => (
23
23
  <ProductCard key={product.id} product={product} />
24
24
  ))}
@@ -65,7 +65,7 @@ export function ProductListing() {
65
65
 
66
66
  return (
67
67
  <section>
68
- {/* DESIGN ME — product listing page: header, category/brand/tag/custom-field filters, sort, product grid, load-more; everything comes from useProductListing() */}
68
+ {/* DESIGN ME — product listing page: header, category/brand/tag/custom-field filters, sort, product grid, load-more; everything comes from useProductListing(). Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
69
69
  <header>
70
70
  <h1>{searchQuery ? `${t('searchPrefix')} "${searchQuery}"` : t('allProducts')}</h1>
71
71
  {!loading && (
@@ -49,6 +49,8 @@ interface RecommendationSectionProps {
49
49
  /**
50
50
  * DESIGN ME — recommendation strip on the product page (upsells / related);
51
51
  * items come from useProductPage().recommendations, title is passed in.
52
+ *
53
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
52
54
  */
53
55
  export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
54
56
  if (items.length === 0) return null;
@@ -74,6 +76,8 @@ interface CartRecommendationSectionProps {
74
76
  /**
75
77
  * DESIGN ME — cross-sell strip on the cart page ("you might also need");
76
78
  * items come from useCartPage().cartRecs.
79
+ *
80
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
77
81
  */
78
82
  export function CartRecommendationSection({
79
83
  title,
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useState } from 'react';
4
4
  import { getClient } from '@/core/lib/brainerce';
5
5
  import { checkAuthStatus } from '@/core/lib/auth';
6
+ import { useTranslations } from '@/core/lib/translations';
6
7
  import type { MyProductReview, ProductReview } from 'brainerce';
7
8
 
8
9
  interface ReviewFormProps {
@@ -26,9 +27,12 @@ type Stage =
26
27
  *
27
28
  * DESIGN ME — review submit/edit form; auth via checkAuthStatus(), eligibility
28
29
  * + existing review via getClient().getMyProductReview(productId).
30
+ *
31
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
29
32
  */
30
33
  export function ReviewForm({ productId }: ReviewFormProps) {
31
34
  const [stage, setStage] = useState<Stage>({ kind: 'loading' });
35
+ const t = useTranslations('reviews');
32
36
 
33
37
  useEffect(() => {
34
38
  let cancelled = false;
@@ -62,13 +66,13 @@ export function ReviewForm({ productId }: ReviewFormProps) {
62
66
  }, [productId]);
63
67
 
64
68
  if (stage.kind === 'loading') {
65
- return <p role="status">Loading…</p>;
69
+ return <p role="status">{t('loading')}</p>;
66
70
  }
67
71
 
68
72
  if (stage.kind === 'signed_out') {
69
73
  return (
70
74
  <p>
71
- <a href="/account/login">Sign in</a> to leave a review for this product.
75
+ <a href="/account/login">{t('signIn')}</a> {t('signInSuffix')}
72
76
  </p>
73
77
  );
74
78
  }
@@ -91,16 +95,17 @@ export function ReviewForm({ productId }: ReviewFormProps) {
91
95
  }
92
96
 
93
97
  function NotEligibleMessage({ reason }: { reason: MyProductReview['reason'] }) {
98
+ const t = useTranslations('reviews');
94
99
  let message: string;
95
100
  switch (reason) {
96
101
  case 'no_eligible_order':
97
- message = 'Only customers who purchased this product can leave a review.';
102
+ message = t('onlyPurchasers');
98
103
  break;
99
104
  case 'reviews_disabled':
100
- message = 'Reviews are not enabled for this store.';
105
+ message = t('reviewsDisabled');
101
106
  break;
102
107
  default:
103
- message = 'You cannot review this product right now.';
108
+ message = t('cannotReview');
104
109
  }
105
110
  return <p>{message}</p>;
106
111
  }
@@ -119,10 +124,11 @@ function EditReviewBlock({
119
124
  onDeleted: () => void;
120
125
  }) {
121
126
  const [deleting, setDeleting] = useState(false);
127
+ const t = useTranslations('reviews');
122
128
  const [deleteError, setDeleteError] = useState<string | null>(null);
123
129
 
124
130
  async function handleDelete() {
125
- if (!window.confirm('Delete your review for this product?')) return;
131
+ if (!window.confirm(t('confirmDelete'))) return;
126
132
  setDeleting(true);
127
133
  setDeleteError(null);
128
134
  try {
@@ -130,7 +136,7 @@ function EditReviewBlock({
130
136
  onDeleted();
131
137
  window.location.reload();
132
138
  } catch {
133
- setDeleteError('Could not delete your review. Please try again.');
139
+ setDeleteError(t('deleteFailed'));
134
140
  } finally {
135
141
  setDeleting(false);
136
142
  }
@@ -145,9 +151,9 @@ function EditReviewBlock({
145
151
  initialBody={initial.body ?? ''}
146
152
  />
147
153
  <div className="flex items-center gap-2">
148
- <p>Want to start over? You can delete your review.</p>
154
+ <p>{t('startOver')}</p>
149
155
  <button type="button" onClick={handleDelete} disabled={deleting}>
150
- {deleting ? 'Deleting…' : 'Delete review'}
156
+ {deleting ? t('deleting') : t('deleteReview')}
151
157
  </button>
152
158
  </div>
153
159
  {deleteError && <p role="alert">{deleteError}</p>}
@@ -172,6 +178,7 @@ function ReviewEditor({
172
178
  initialBody: string;
173
179
  }) {
174
180
  const [rating, setRating] = useState(initialRating);
181
+ const t = useTranslations('reviews');
175
182
  const [body, setBody] = useState(initialBody);
176
183
  const [submitting, setSubmitting] = useState(false);
177
184
  const [error, setError] = useState<string | null>(null);
@@ -194,17 +201,13 @@ function ReviewEditor({
194
201
  } catch (err) {
195
202
  const status = (err as { status?: number })?.status;
196
203
  if (status === 409) {
197
- setError("You've already submitted a review for this product.");
204
+ setError(t('alreadySubmitted'));
198
205
  } else if (status === 403) {
199
- setError('Only customers who purchased this product can leave a review.');
206
+ setError(t('onlyPurchasers'));
200
207
  } else if (status === 429) {
201
- setError('Too many submissions. Please try again in a few minutes.');
208
+ setError(t('tooMany'));
202
209
  } else {
203
- setError(
204
- mode === 'edit'
205
- ? 'Could not update your review. Please try again.'
206
- : 'Could not submit your review. Please try again.'
207
- );
210
+ setError(mode === 'edit' ? t('updateFailed') : t('submitFailed'));
208
211
  }
209
212
  } finally {
210
213
  setSubmitting(false);
@@ -212,22 +215,16 @@ function ReviewEditor({
212
215
  }
213
216
 
214
217
  if (success) {
215
- return (
216
- <p role="status">
217
- {mode === 'edit'
218
- ? 'Thanks! Your review has been updated.'
219
- : 'Thanks! Your review has been submitted.'}
220
- </p>
221
- );
218
+ return <p role="status">{mode === 'edit' ? t('thanksUpdated') : t('thanksSubmitted')}</p>;
222
219
  }
223
220
 
224
221
  return (
225
222
  <form onSubmit={handleSubmit}>
226
- <h3>{mode === 'edit' ? 'Edit your review' : 'Write a review'}</h3>
223
+ <h3>{mode === 'edit' ? t('editYourReview') : t('writeReview')}</h3>
227
224
 
228
225
  <fieldset>
229
- <legend>Your rating</legend>
230
- <div className="inline-flex gap-1" role="radiogroup" aria-label="Star rating">
226
+ <legend>{t('yourRating')}</legend>
227
+ <div className="inline-flex gap-1" role="radiogroup" aria-label={t('starRating')}>
231
228
  {[1, 2, 3, 4, 5].map((n) => (
232
229
  <button
233
230
  key={n}
@@ -243,7 +240,7 @@ function ReviewEditor({
243
240
  </fieldset>
244
241
 
245
242
  <label className="block">
246
- Your review <span>(optional)</span>
243
+ {t('yourReview')} <span>({t('optionalField')})</span>
247
244
  <textarea
248
245
  value={body}
249
246
  onChange={(event) => setBody(event.target.value)}
@@ -258,11 +255,11 @@ function ReviewEditor({
258
255
  <button type="submit" disabled={submitting}>
259
256
  {submitting
260
257
  ? mode === 'edit'
261
- ? 'Saving…'
262
- : 'Submitting…'
258
+ ? t('saving')
259
+ : t('submittingBtn')
263
260
  : mode === 'edit'
264
- ? 'Save changes'
265
- : 'Submit review'}
261
+ ? t('saveChanges')
262
+ : t('submitReview')}
266
263
  </button>
267
264
  </form>
268
265
  );
@@ -1,10 +1,23 @@
1
1
  import type { ProductReview } from 'brainerce';
2
2
  import { getServerClient } from '@/core/lib/brainerce';
3
+ <% if (i18nEnabled) { %>import { getMessages, defaultLocale } from '@/i18n';<% } else { %>import { defaultLocale, messages } from '@/i18n';<% } %>
3
4
  import { ReviewForm } from './review-form';
4
5
 
6
+ // Server component — it renders before any client i18n context exists, so the
7
+ // wrapper strings (title / empty state / verified badge) are read straight
8
+ // from the `reviews` namespace in messages/, keyed by the store locale.
9
+ type ReviewStrings = Record<string, string>;
10
+
11
+ <% if (i18nEnabled) { %>async function getReviewStrings(locale: string): Promise<ReviewStrings> {
12
+ return ((await getMessages(locale)).reviews ?? {}) as ReviewStrings;
13
+ }<% } else { %>async function getReviewStrings(_locale: string): Promise<ReviewStrings> {
14
+ return messages.reviews as ReviewStrings;
15
+ }<% } %>
16
+
5
17
  interface ReviewsSectionProps {
6
18
  productId: string;
7
19
  initialReviews?: ProductReview[];
20
+ locale?: string;
8
21
  /** Where to send users back to after submit (defaults to the current product URL). */
9
22
  returnUrl?: string;
10
23
  }
@@ -16,7 +29,12 @@ interface ReviewsSectionProps {
16
29
  * the initial HTML payload (good for SEO; JSON-LD on the same page already
17
30
  * carries `aggregateRating` when reviewCount > 0).
18
31
  */
19
- export async function ReviewsSection({ productId, initialReviews }: ReviewsSectionProps) {
32
+ export async function ReviewsSection({
33
+ productId,
34
+ initialReviews,
35
+ locale = defaultLocale,
36
+ }: ReviewsSectionProps) {
37
+ const t = await getReviewStrings(locale);
20
38
  let reviews: ProductReview[] = initialReviews ?? [];
21
39
 
22
40
  if (!initialReviews) {
@@ -24,22 +42,22 @@ export async function ReviewsSection({ productId, initialReviews }: ReviewsSecti
24
42
  const result = await getServerClient().listProductReviews(productId, { page: 1, limit: 20 });
25
43
  reviews = result.data;
26
44
  } catch {
27
- // Reviews disabled at store level or network error — render the form anyway.
45
+ // Review listing disabled at store level or network error — render the form anyway.
28
46
  reviews = [];
29
47
  }
30
48
  }
31
49
 
32
50
  return (
33
51
  <section aria-labelledby="reviews-heading">
34
- {/* DESIGN ME — product reviews: server-fetched review list (SDK listProductReviews) + eligibility-aware submit form */}
35
- <h2 id="reviews-heading">Reviews</h2>
52
+ {/* DESIGN ME — product reviews: server-fetched review list (SDK listProductReviews) + eligibility-aware submit form. Compose with the shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons. */}
53
+ <h2 id="reviews-heading">{t.title}</h2>
36
54
 
37
55
  {reviews.length === 0 ? (
38
- <p>No reviews yet — be the first to share your experience.</p>
56
+ <p>{t.noReviews}</p>
39
57
  ) : (
40
58
  <ul>
41
59
  {reviews.map((review) => (
42
- <ReviewCard key={review.id} review={review} />
60
+ <ReviewCard key={review.id} review={review} verifiedLabel={t.verifiedPurchase} />
43
61
  ))}
44
62
  </ul>
45
63
  )}
@@ -49,14 +67,14 @@ export async function ReviewsSection({ productId, initialReviews }: ReviewsSecti
49
67
  );
50
68
  }
51
69
 
52
- function ReviewCard({ review }: { review: ProductReview }) {
70
+ function ReviewCard({ review, verifiedLabel }: { review: ProductReview; verifiedLabel: string }) {
53
71
  return (
54
72
  <li>
55
73
  <article>
56
74
  <header className="flex flex-wrap items-center gap-2">
57
75
  <Stars rating={review.rating} />
58
76
  <span>{review.authorName}</span>
59
- {review.verifiedPurchase && <span>Verified purchase</span>}
77
+ {review.verifiedPurchase && <span>{verifiedLabel}</span>}
60
78
  <time dateTime={review.createdAt}>{new Date(review.createdAt).toLocaleDateString()}</time>
61
79
  </header>
62
80
  {review.body && <p className="whitespace-pre-line break-words">{review.body}</p>}
@@ -14,6 +14,8 @@ interface StockBadgeProps {
14
14
  * DESIGN ME — stock status badge (in stock / low stock / out of stock);
15
15
  * derives the label from the product/variant `inventory` prop. Keep the
16
16
  * label logic; add your own tones per state.
17
+ *
18
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
17
19
  */
18
20
  export function StockBadge({ inventory, lowStockThreshold = 5, className }: StockBadgeProps) {
19
21
  const t = useTranslations('productDetail');
@@ -34,6 +34,8 @@ interface AttributeGroup {
34
34
  * (`displayType`, `swatchColor`, `swatchColor2`, `swatchImageUrl`) is
35
35
  * computed per value — the skeleton renders plain buttons, use the swatch
36
36
  * data to build color/image swatches in your design.
37
+ *
38
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
37
39
  */
38
40
  export function VariantSelector({
39
41
  product,
@@ -23,6 +23,8 @@ function isOptimizedHost(src: ImageProps['src']): boolean {
23
23
  *
24
24
  * DESIGN ME — shared image wrapper; render ALL catalog images through this
25
25
  * component (not raw next/image) so unknown hosts never crash the page.
26
+ *
27
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
26
28
  */
27
29
  export function CdnImage({ unoptimized, ...props }: ImageProps) {
28
30
  return <Image {...props} unoptimized={unoptimized ?? !isOptimizedHost(props.src)} />;
@@ -13,6 +13,8 @@ interface LoadingSpinnerProps {
13
13
  * text placeholder; replace with your own spinner/skeleton. Keep the
14
14
  * `role="status"` + sr-only label, and keep the `size` prop (callers across
15
15
  * the app pass sm/md/lg).
16
+ *
17
+ * Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
16
18
  */
17
19
  export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
18
20
  const t = useTranslations('common');
@@ -1,85 +0,0 @@
1
- import type { ProductReview } from 'brainerce';
2
- import { getServerClient } from '@/core/lib/brainerce';
3
- import { ReviewForm } from './review-form';
4
-
5
- interface ReviewsSectionProps {
6
- productId: string;
7
- initialReviews?: ProductReview[];
8
- /** Where to send users back to after submit (defaults to the current product URL). */
9
- returnUrl?: string;
10
- }
11
-
12
- /**
13
- * Renders the visible reviews for a product plus a submit form.
14
- *
15
- * Server component — fetches with the SDK on the server so reviews are in
16
- * the initial HTML payload (good for SEO; JSON-LD on the same page already
17
- * carries `aggregateRating` when reviewCount > 0).
18
- */
19
- export async function ReviewsSection({ productId, initialReviews }: ReviewsSectionProps) {
20
- let reviews: ProductReview[] = initialReviews ?? [];
21
-
22
- if (!initialReviews) {
23
- try {
24
- const result = await getServerClient().listProductReviews(productId, { page: 1, limit: 20 });
25
- reviews = result.data;
26
- } catch {
27
- // Reviews disabled at store level or network error — render the form anyway.
28
- reviews = [];
29
- }
30
- }
31
-
32
- return (
33
- <section className="mt-8" aria-labelledby="reviews-heading">
34
- <h2 id="reviews-heading" className="mb-4 text-xl font-semibold">
35
- Reviews
36
- </h2>
37
-
38
- {reviews.length === 0 ? (
39
- <p className="text-muted-foreground mb-6 text-sm">
40
- No reviews yet — be the first to share your experience.
41
- </p>
42
- ) : (
43
- <ul className="mb-6 space-y-4">
44
- {reviews.map((review) => (
45
- <ReviewCard key={review.id} review={review} />
46
- ))}
47
- </ul>
48
- )}
49
-
50
- <ReviewForm productId={productId} />
51
- </section>
52
- );
53
- }
54
-
55
- function ReviewCard({ review }: { review: ProductReview }) {
56
- return (
57
- <li className="rounded-md border p-4">
58
- <header className="flex flex-wrap items-center gap-2">
59
- <Stars rating={review.rating} />
60
- <span className="text-sm font-medium">{review.authorName}</span>
61
- {review.verifiedPurchase && (
62
- <span className="rounded bg-emerald-50 px-2 py-0.5 text-xs text-emerald-700">
63
- Verified purchase
64
- </span>
65
- )}
66
- <time className="text-muted-foreground ms-auto text-xs">
67
- {new Date(review.createdAt).toLocaleDateString()}
68
- </time>
69
- </header>
70
- {review.body && <p className="mt-2 whitespace-pre-line break-words text-sm">{review.body}</p>}
71
- </li>
72
- );
73
- }
74
-
75
- function Stars({ rating }: { rating: number }) {
76
- return (
77
- <span aria-label={`${rating} of 5 stars`} className="inline-flex">
78
- {[1, 2, 3, 4, 5].map((n) => (
79
- <span key={n} className={n <= rating ? 'text-yellow-500' : 'text-gray-300'}>
80
- ★
81
- </span>
82
- ))}
83
- </span>
84
- );
85
- }