create-brainerce-store 1.50.1 → 1.51.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 (189) hide show
  1. package/dist/index.js +171 -66
  2. package/messages/en.json +4 -1
  3. package/messages/he.json +4 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.claude/commands/design.md +51 -0
  6. package/templates/nextjs/base/.env.local.ejs +5 -0
  7. package/templates/nextjs/base/.eslintrc.json +57 -3
  8. package/templates/nextjs/base/AGENTS.md +41 -0
  9. package/templates/nextjs/base/AI-GUIDE.md +94 -0
  10. package/templates/nextjs/base/CLAUDE.md +42 -0
  11. package/templates/nextjs/base/TRANSLATIONS.md +1 -1
  12. package/templates/nextjs/base/next.config.ts +18 -10
  13. package/templates/nextjs/base/package.json.ejs +5 -1
  14. package/templates/nextjs/base/src/app/account/page.tsx +5 -5
  15. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +1 -1
  16. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +2 -2
  17. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +1 -1
  18. package/templates/nextjs/base/src/app/auth/callback/page.tsx +4 -4
  19. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +4 -4
  20. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +2 -2
  21. package/templates/nextjs/base/src/app/cart/page.tsx +2 -173
  22. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -11
  23. package/templates/nextjs/base/src/app/contact/page.tsx +3 -3
  24. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +2 -2
  25. package/templates/nextjs/base/src/app/forgot-password/page.tsx +4 -4
  26. package/templates/nextjs/base/src/app/layout.tsx.ejs +16 -14
  27. package/templates/nextjs/base/src/app/login/page.tsx +4 -4
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +19 -12
  29. package/templates/nextjs/base/src/app/page.tsx +4 -4
  30. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +3 -3
  31. package/templates/nextjs/base/src/app/payment-complete/page.tsx +1 -1
  32. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +6 -6
  33. package/templates/nextjs/base/src/app/products/page.tsx +4 -623
  34. package/templates/nextjs/base/src/app/register/page.tsx +4 -4
  35. package/templates/nextjs/base/src/app/reset-password/page.tsx +5 -5
  36. package/templates/nextjs/base/src/app/sitemap.ts +1 -1
  37. package/templates/nextjs/base/src/app/verify-email/page.tsx +5 -5
  38. package/templates/nextjs/base/src/components/account/address-book.tsx +3 -3
  39. package/templates/nextjs/base/src/components/account/order-customizations.tsx +3 -3
  40. package/templates/nextjs/base/src/components/account/order-history.tsx +13 -5
  41. package/templates/nextjs/base/src/components/account/order-payment-block.tsx +2 -2
  42. package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +2 -2
  43. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +2 -2
  44. package/templates/nextjs/base/src/components/account/profile-section.tsx +3 -3
  45. package/templates/nextjs/base/src/components/auth/login-form.tsx +4 -4
  46. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +4 -4
  47. package/templates/nextjs/base/src/components/auth/register-form.tsx +4 -4
  48. package/templates/nextjs/base/src/components/brainerce-bot.tsx +2 -2
  49. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +24 -3
  50. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +2 -2
  51. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +2 -2
  52. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +4 -4
  53. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +6 -6
  54. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +3 -3
  55. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +3 -3
  56. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +3 -3
  57. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +2 -2
  58. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +3 -3
  59. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -0
  60. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -0
  61. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -0
  62. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +278 -0
  63. package/templates/nextjs/base/src/{hooks → core/hooks}/use-search.ts +1 -1
  64. package/templates/nextjs/base/src/{lib → core/lib}/brainerce.ts.ejs +1 -1
  65. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -0
  66. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -0
  67. package/templates/nextjs/base/src/{lib → core/lib}/resolve-currency.ts +1 -1
  68. package/templates/nextjs/base/src/{lib → core/lib}/safe-redirect.ts +1 -1
  69. package/templates/nextjs/base/src/{lib → core/lib}/sanitize-html.ts +12 -0
  70. package/templates/nextjs/base/src/{lib → core/lib}/sanitize.ts.ejs +1 -1
  71. package/templates/nextjs/base/src/{lib → core/lib}/use-currency.ts +1 -1
  72. package/templates/nextjs/base/src/{providers → core/providers}/store-provider.tsx.ejs +4 -4
  73. package/templates/nextjs/base/src/middleware.ts.ejs +2 -2
  74. package/templates/nextjs/base/src/{components → ui}/cart/cart-bundle-offer.tsx +5 -5
  75. package/templates/nextjs/base/src/{components → ui}/cart/cart-item.tsx +6 -6
  76. package/templates/nextjs/base/src/{components → ui}/cart/cart-nudges.tsx +1 -1
  77. package/templates/nextjs/base/src/{components → ui}/cart/cart-summary.tsx +4 -4
  78. package/templates/nextjs/base/src/{components → ui}/cart/cart-upgrade-banner.tsx +5 -5
  79. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +149 -0
  80. package/templates/nextjs/base/src/{components → ui}/cart/coupon-input.tsx +4 -4
  81. package/templates/nextjs/base/src/{components → ui}/cart/free-shipping-bar.tsx +4 -4
  82. package/templates/nextjs/base/src/{components → ui}/cart/reservation-countdown.tsx +2 -2
  83. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -0
  84. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -0
  85. package/templates/nextjs/base/src/ui/home/hero-section.tsx +29 -0
  86. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -0
  87. package/templates/nextjs/base/src/{components/content → ui/layout}/faq-section.tsx.ejs +2 -2
  88. package/templates/nextjs/base/src/{components/content → ui/layout}/header-account.tsx +3 -3
  89. package/templates/nextjs/base/src/{components/content → ui/layout}/rich-text-block.tsx.ejs +1 -1
  90. package/templates/nextjs/base/src/{components/content → ui/layout}/site-header.tsx.ejs +1 -1
  91. package/templates/nextjs/base/src/{components/products → ui/product}/customization-fields.tsx +18 -51
  92. package/templates/nextjs/base/src/{components/products → ui/product}/discount-badge.tsx +1 -1
  93. package/templates/nextjs/base/src/{components/products → ui/product}/frequently-bought-together.tsx +6 -6
  94. package/templates/nextjs/base/src/{components/products → ui/product}/modifier-group-selector.tsx +10 -77
  95. package/templates/nextjs/base/src/{components/shared → ui/product}/price-display.tsx +2 -2
  96. package/templates/nextjs/base/src/{components/products → ui/product}/product-card.tsx +10 -10
  97. package/templates/nextjs/base/src/{app/products/[slug] → ui/product}/product-client-section.tsx +45 -206
  98. package/templates/nextjs/base/src/{components/products → ui/product}/product-grid.tsx +3 -3
  99. package/templates/nextjs/base/src/ui/product/product-listing.tsx +429 -0
  100. package/templates/nextjs/base/src/{components/products → ui/product}/recommendation-section.tsx +4 -4
  101. package/templates/nextjs/base/src/{components/reviews → ui/product}/review-form.tsx +2 -2
  102. package/templates/nextjs/base/src/{components/reviews → ui/product}/reviews-section.tsx +1 -1
  103. package/templates/nextjs/base/src/{components/products → ui/product}/stock-badge.tsx +2 -2
  104. package/templates/nextjs/base/src/{components/products → ui/product}/variant-selector.tsx +3 -3
  105. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -0
  106. package/templates/nextjs/base/src/{components → ui}/shared/loading-spinner.tsx +2 -2
  107. package/templates/nextjs/base/tailwind.config.ts +12 -0
  108. package/templates/nextjs/designs/passepartout/app-overlay/layout.tsx.ejs +253 -0
  109. package/templates/nextjs/designs/passepartout/globals.css +412 -0
  110. package/templates/nextjs/designs/passepartout/messages-patch/en.json +87 -0
  111. package/templates/nextjs/designs/passepartout/messages-patch/he.json +87 -0
  112. package/templates/nextjs/designs/passepartout/ui/cart/cart-bundle-offer.tsx +111 -0
  113. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +39 -0
  114. package/templates/nextjs/designs/passepartout/ui/cart/cart-upgrade-banner.tsx +141 -0
  115. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +149 -0
  116. package/templates/nextjs/designs/passepartout/ui/cart/free-shipping-bar.tsx +62 -0
  117. package/templates/nextjs/designs/passepartout/ui/cart/reservation-countdown.tsx +103 -0
  118. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +401 -0
  119. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +139 -0
  120. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +45 -0
  121. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +103 -0
  122. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +159 -0
  123. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +155 -0
  124. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +60 -0
  125. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +34 -0
  126. package/templates/nextjs/designs/passepartout/ui/product/discount-badge.tsx +22 -0
  127. package/templates/nextjs/designs/passepartout/ui/product/frequently-bought-together.tsx +205 -0
  128. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +111 -0
  129. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +265 -0
  130. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +667 -0
  131. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +35 -0
  132. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +427 -0
  133. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +113 -0
  134. package/templates/nextjs/designs/passepartout/ui/product/review-form.tsx +294 -0
  135. package/templates/nextjs/designs/passepartout/ui/product/reviews-section.tsx.ejs +96 -0
  136. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +52 -0
  137. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +307 -0
  138. package/templates/nextjs/designs/passepartout/ui/shared/fly-to-cart.ts +85 -0
  139. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +33 -0
  140. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +87 -0
  141. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +121 -0
  142. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +25 -0
  143. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +97 -0
  144. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +103 -0
  145. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -0
  146. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +117 -0
  147. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +52 -0
  148. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +79 -0
  149. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -0
  150. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -0
  151. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -0
  152. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -0
  153. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -0
  154. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -0
  155. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -0
  156. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +74 -0
  157. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +34 -0
  158. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -0
  159. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -0
  160. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +443 -0
  161. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +17 -0
  162. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +166 -0
  163. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +163 -0
  164. package/templates/nextjs/ui-canvas/product/price-display.tsx +50 -0
  165. package/templates/nextjs/ui-canvas/product/product-card.tsx +197 -0
  166. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -0
  167. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -0
  168. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -0
  169. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +95 -0
  170. package/templates/nextjs/ui-canvas/product/review-form.tsx +269 -0
  171. package/templates/nextjs/ui-canvas/product/reviews-section.tsx +78 -0
  172. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +43 -0
  173. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +210 -0
  174. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +29 -0
  175. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +25 -0
  176. package/templates/nextjs/base/src/app/home-client.tsx +0 -98
  177. package/templates/nextjs/base/src/components/products/allergen-chips.tsx +0 -78
  178. /package/templates/nextjs/base/src/{lib → core/lib}/auth.ts +0 -0
  179. /package/templates/nextjs/base/src/{lib → core/lib}/csrf.ts +0 -0
  180. /package/templates/nextjs/base/src/{lib → core/lib}/navigation.tsx.ejs +0 -0
  181. /package/templates/nextjs/base/src/{lib → core/lib}/nonce.ts +0 -0
  182. /package/templates/nextjs/base/src/{lib → core/lib}/seo.ts +0 -0
  183. /package/templates/nextjs/base/src/{lib → core/lib}/store-info.ts +0 -0
  184. /package/templates/nextjs/base/src/{lib → core/lib}/translations.ts.ejs +0 -0
  185. /package/templates/nextjs/base/src/{lib → core/lib}/utils.ts +0 -0
  186. /package/templates/nextjs/base/src/{lib → core/lib}/validation.ts +0 -0
  187. /package/templates/nextjs/base/src/{components/content → ui/layout}/announcement-bar.tsx.ejs +0 -0
  188. /package/templates/nextjs/base/src/{components → ui}/layout/language-switcher.tsx.ejs +0 -0
  189. /package/templates/nextjs/base/src/{components/content → ui/layout}/site-footer.tsx.ejs +0 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Pure product-option logic (zero markup) shared by `core/hooks/use-product-page`
3
+ * and the `ui/product` option components. Moved verbatim from
4
+ * `customization-fields.tsx` / `modifier-group-selector.tsx` so that core
5
+ * never depends on `ui/` — the UI components re-export these for
6
+ * backward compatibility.
7
+ */
8
+
9
+ import type { ModifierGroup, ModifierSelection, ProductCustomizationField } from 'brainerce';
10
+
11
+ export type CustomizationValues = Record<string, unknown>;
12
+
13
+ export function validateCustomization(
14
+ fields: ProductCustomizationField[],
15
+ values: CustomizationValues
16
+ ): Record<string, string> {
17
+ const errors: Record<string, string> = {};
18
+ for (const field of fields) {
19
+ const value = values[field.key];
20
+ const empty =
21
+ value === undefined ||
22
+ value === null ||
23
+ value === '' ||
24
+ (Array.isArray(value) && value.length === 0);
25
+
26
+ if (field.required && empty) {
27
+ errors[field.key] = 'required';
28
+ continue;
29
+ }
30
+ if (empty) continue;
31
+
32
+ if (field.type === 'SELECT') {
33
+ const allowed = (field.enumValues ?? []).map((o) => o.value);
34
+ if (typeof value !== 'string' || !allowed.includes(value)) {
35
+ errors[field.key] = 'invalidOption';
36
+ }
37
+ }
38
+ if (field.type === 'MULTI_SELECT') {
39
+ const allowed = (field.enumValues ?? []).map((o) => o.value);
40
+ if (
41
+ !Array.isArray(value) ||
42
+ !(value as unknown[]).every((v) => typeof v === 'string' && allowed.includes(v as string))
43
+ ) {
44
+ errors[field.key] = 'invalidOption';
45
+ }
46
+ }
47
+ }
48
+ return errors;
49
+ }
50
+
51
+ /**
52
+ * Lightweight client-side validation that mirrors the server's checks. Use
53
+ * this to gate the add-to-cart button — but always treat the server's 400
54
+ * (`MODIFIER_VALIDATION_FAILED`) as the authoritative answer.
55
+ */
56
+ export function validateSelections(
57
+ groups: ModifierGroup[],
58
+ selections: Record<string, string[]>
59
+ ): string | null {
60
+ for (const group of groups) {
61
+ if (group.max === 0) continue; // disabled-for-variant — skip
62
+ const picks = selections[group.id] ?? [];
63
+ if (group.required && picks.length === 0) {
64
+ return `${group.name} is required`;
65
+ }
66
+ if (picks.length < group.min) {
67
+ return `Pick at least ${group.min} from ${group.name}`;
68
+ }
69
+ if (group.max != null && picks.length > group.max) {
70
+ return `Pick at most ${group.max} from ${group.name}`;
71
+ }
72
+ if (group.selectionType === 'SINGLE' && picks.length > 1) {
73
+ return `Only one option allowed in ${group.name}`;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * Adapter: convert the local Record<groupId, modifierIds> shape used by the
81
+ * selector component into the wire-format `ModifierSelection[]` array the SDK
82
+ * expects on `addToCart` / `updateCartItem`.
83
+ */
84
+ export function toModifierSelections(
85
+ groups: ModifierGroup[],
86
+ selections: Record<string, string[]>
87
+ ): ModifierSelection[] {
88
+ const out: ModifierSelection[] = [];
89
+ for (const group of groups) {
90
+ if (group.max === 0) continue;
91
+ const picks = selections[group.id] ?? [];
92
+ if (picks.length === 0) continue;
93
+ out.push({ modifierGroupId: group.id, modifierIds: picks });
94
+ }
95
+ return out;
96
+ }
97
+
98
+ /**
99
+ * Build the initial selection map from the group's defaults — call once on
100
+ * first render for each group. Honors `defaultModifierIds` first (per-attach
101
+ * defaults), falling back to per-modifier `isDefault` flags. Sold-out
102
+ * modifiers are filtered out so the user doesn't start with an invalid pick.
103
+ */
104
+ export function buildInitialSelections(groups: ModifierGroup[]): Record<string, string[]> {
105
+ const out: Record<string, string[]> = {};
106
+ for (const group of groups) {
107
+ if (group.max === 0) continue;
108
+ const fromDefaults = group.defaultModifierIds ?? [];
109
+ const fromIsDefault = group.modifiers
110
+ .filter((m) => m.isDefault && m.available)
111
+ .map((m) => m.id);
112
+ const merged =
113
+ fromDefaults.length > 0
114
+ ? fromDefaults.filter((id) => group.modifiers.some((m) => m.id === id && m.available))
115
+ : fromIsDefault;
116
+ // Cap by max for SINGLE groups — defaults can't violate selection rules.
117
+ const capped = group.selectionType === 'SINGLE' ? merged.slice(0, 1) : merged;
118
+ if (capped.length > 0) out[group.id] = capped;
119
+ }
120
+ return out;
121
+ }
@@ -1,4 +1,4 @@
1
- import type { PublicStoreInfo } from '@/lib/store-info';
1
+ import type { PublicStoreInfo } from '@/core/lib/store-info';
2
2
 
3
3
  /**
4
4
  * Server-side twin of `useCurrency()` — call from Server Components,
@@ -1,7 +1,7 @@
1
1
  // Re-export the platform-maintained payment URL allowlist from the SDK.
2
2
  //
3
3
  // Why a re-export and not a direct import everywhere: keeping a stable
4
- // `@/lib/safe-redirect` import path means future SDK API changes (e.g. an
4
+ // `@/core/lib/safe-redirect` import path means future SDK API changes (e.g. an
5
5
  // `extraHosts` rename) only need editing here, and merchants don't have to
6
6
  // touch every component that does payment redirects. It also leaves a clear
7
7
  // place to plug in store-specific extras (custom self-hosted PSP) without
@@ -71,6 +71,9 @@ export function sanitizeProductHtml(html: string): string {
71
71
  'colgroup',
72
72
  'col',
73
73
  'iframe',
74
+ // Self-hosted product videos (uploaded via the dashboard media library).
75
+ 'video',
76
+ 'source',
74
77
  ],
75
78
  ALLOWED_ATTR: [
76
79
  'href',
@@ -87,6 +90,15 @@ export function sanitizeProductHtml(html: string): string {
87
90
  'allowfullscreen',
88
91
  'frameborder',
89
92
  'loading',
93
+ 'title',
94
+ // <video> / <source> playback attributes.
95
+ 'controls',
96
+ 'poster',
97
+ 'preload',
98
+ 'muted',
99
+ 'loop',
100
+ 'playsinline',
101
+ 'type',
90
102
  ],
91
103
  ALLOW_DATA_ATTR: false,
92
104
  });
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import DOMPurify from 'isomorphic-dompurify';
16
16
 
17
- const DEFAULT_CONFIG: DOMPurify.Config = {
17
+ const DEFAULT_CONFIG = {
18
18
  // Allow iframes for embedded videos/maps the merchant may include.
19
19
  ADD_TAGS: ['iframe'],
20
20
  ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'target', 'rel'],
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useStoreInfo } from '@/providers/store-provider';
3
+ import { useStoreInfo } from '@/core/providers/store-provider';
4
4
 
5
5
  /**
6
6
  * Single source of truth for "which currency code do I format prices with?"
@@ -3,11 +3,11 @@
3
3
  import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
4
4
  import type { Cart, CustomerProfile } from 'brainerce';
5
5
  import { getCartTotals } from 'brainerce';
6
- import { getClient, initClient, setStoredCartId } from '@/lib/brainerce';
7
- import { pickPublicStoreInfo, type PublicStoreInfo } from '@/lib/store-info';
8
- import { checkAuthStatus, proxyLogout } from '@/lib/auth';
6
+ import { getClient, initClient, setStoredCartId } from '@/core/lib/brainerce';
7
+ import { pickPublicStoreInfo, type PublicStoreInfo } from '@/core/lib/store-info';
8
+ import { checkAuthStatus, proxyLogout } from '@/core/lib/auth';
9
9
  <% if (i18nEnabled) { %>
10
- import { MessagesContext } from '@/lib/translations';
10
+ import { MessagesContext } from '@/core/lib/translations';
11
11
  import { getMessages, defaultLocale } from '@/i18n';
12
12
  <% } %>
13
13
 
@@ -43,7 +43,7 @@ function buildCsp(nonce: string): string {
43
43
  "style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
44
44
  "img-src 'self' data: blob: https:",
45
45
  "font-src 'self' data:",
46
- "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://*.brainerce.com",
46
+ "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://*.brainerce.com",
47
47
  `connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il`,
48
48
  "worker-src 'self' blob:",
49
49
  // 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
@@ -171,7 +171,7 @@ function buildCsp(nonce: string): string {
171
171
  "style-src 'self' 'unsafe-inline' https://cdn.meshulam.co.il",
172
172
  "img-src 'self' data: blob: https:",
173
173
  "font-src 'self' data:",
174
- "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://*.brainerce.com",
174
+ "frame-src 'self' https://meshulam.co.il https://*.meshulam.co.il https://grow.link https://*.grow.link https://grow.security https://*.grow.security https://creditguard.co.il https://*.creditguard.co.il https://js.stripe.com https://hooks.stripe.com https://pay.google.com https://secure.cardcom.solutions https://checkout.stripe.com https://www.paypal.com https://www.sandbox.paypal.com https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://*.brainerce.com",
175
175
  `connect-src 'self' ${brainerceApiOrigin()} https://api.brainerce.com https://*.meshulam.co.il https://grow.link https://*.grow.link https://*.grow.security https://pay.google.com https://*.stripe.com https://*.creditguard.co.il`,
176
176
  "worker-src 'self' blob:",
177
177
  // 'self' (not 'none') so iframe-based payment providers (e.g. Cardcom)
@@ -1,12 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import Image from 'next/image';
4
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
5
  import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
6
6
  import { formatPrice } from 'brainerce';
7
- import { useCurrency } from '@/lib/use-currency';
8
- import { useTranslations } from '@/lib/translations';
9
- import { cn } from '@/lib/utils';
7
+ import { useCurrency } from '@/core/lib/use-currency';
8
+ import { useTranslations } from '@/core/lib/translations';
9
+ import { cn } from '@/core/lib/utils';
10
10
 
11
11
  interface CartBundleOfferCardProps {
12
12
  offer: CartBundleOfferType;
@@ -33,7 +33,7 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
33
33
  if (adding) return;
34
34
  try {
35
35
  setAdding(true);
36
- const { getClient } = await import('@/lib/brainerce');
36
+ const { getClient } = await import('@/core/lib/brainerce');
37
37
  const client = getClient();
38
38
  // No variant selections in the default template — products with
39
39
  // variants are best handled with a dedicated picker per offered item.
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
- import Image from 'next/image';
4
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
5
  import type { CartItem as CartItemType } from 'brainerce';
6
6
  import { getCartItemImage, formatPrice } from 'brainerce';
7
- import { getClient } from '@/lib/brainerce';
8
- import { useTranslations } from '@/lib/translations';
9
- import { useCurrency } from '@/lib/use-currency';
10
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
11
- import { cn } from '@/lib/utils';
7
+ import { getClient } from '@/core/lib/brainerce';
8
+ import { useTranslations } from '@/core/lib/translations';
9
+ import { useCurrency } from '@/core/lib/use-currency';
10
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
11
+ import { cn } from '@/core/lib/utils';
12
12
 
13
13
  interface CartItemProps {
14
14
  item: CartItemType;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import type { CartNudge } from 'brainerce';
4
- import { cn } from '@/lib/utils';
4
+ import { cn } from '@/core/lib/utils';
5
5
 
6
6
  interface CartNudgesProps {
7
7
  nudges: CartNudge[];
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { formatPrice } from 'brainerce';
4
- import { useTranslations } from '@/lib/translations';
5
- import { useCart } from '@/providers/store-provider';
6
- import { useCurrency } from '@/lib/use-currency';
7
- import { cn } from '@/lib/utils';
4
+ import { useTranslations } from '@/core/lib/translations';
5
+ import { useCart } from '@/core/providers/store-provider';
6
+ import { useCurrency } from '@/core/lib/use-currency';
7
+ import { cn } from '@/core/lib/utils';
8
8
 
9
9
  interface CartSummaryProps {
10
10
  className?: string;
@@ -1,13 +1,13 @@
1
1
  'use client';
2
2
 
3
3
  import { useState, useEffect } from 'react';
4
- import Image from 'next/image';
4
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
5
  import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
6
6
  import { formatPrice } from 'brainerce';
7
- import { getClient } from '@/lib/brainerce';
8
- import { useCurrency } from '@/lib/use-currency';
9
- import { useTranslations } from '@/lib/translations';
10
- import { cn } from '@/lib/utils';
7
+ import { getClient } from '@/core/lib/brainerce';
8
+ import { useCurrency } from '@/core/lib/use-currency';
9
+ import { useTranslations } from '@/core/lib/translations';
10
+ import { cn } from '@/core/lib/utils';
11
11
 
12
12
  interface CartUpgradeBannerProps {
13
13
  suggestion: CartUpgradeSuggestion;
@@ -0,0 +1,149 @@
1
+ 'use client';
2
+
3
+ import { Link } from '@/core/lib/navigation';
4
+ import { useCartPage } from '@/core/hooks/use-cart-page';
5
+ import { CartItem } from '@/ui/cart/cart-item';
6
+ import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
7
+ import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
8
+ import { CartSummary } from '@/ui/cart/cart-summary';
9
+ import { CouponInput } from '@/ui/cart/coupon-input';
10
+ import { CartNudges } from '@/ui/cart/cart-nudges';
11
+ import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
12
+ import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
13
+ import { CartRecommendationSection } from '@/ui/product/recommendation-section';
14
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
15
+ import { useTranslations } from '@/core/lib/translations';
16
+
17
+ export function CartView() {
18
+ const t = useTranslations('cart');
19
+ const tc = useTranslations('common');
20
+ const { cart, cartLoading, refreshCart, itemCount, cartRecs, upgrades, bundles } = useCartPage();
21
+
22
+ if (cartLoading) {
23
+ return (
24
+ <div className="flex min-h-[60vh] items-center justify-center">
25
+ <LoadingSpinner size="lg" />
26
+ </div>
27
+ );
28
+ }
29
+
30
+ // Empty cart state
31
+ if (!cart || cart.items.length === 0) {
32
+ return (
33
+ <div className="mx-auto max-w-7xl px-4 py-16 text-center sm:px-6 lg:px-8">
34
+ <svg
35
+ className="text-muted-foreground mx-auto mb-4 h-16 w-16"
36
+ fill="none"
37
+ viewBox="0 0 24 24"
38
+ stroke="currentColor"
39
+ >
40
+ <path
41
+ strokeLinecap="round"
42
+ strokeLinejoin="round"
43
+ strokeWidth={1.5}
44
+ d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"
45
+ />
46
+ </svg>
47
+ <h1 className="text-foreground text-2xl font-bold">{t('emptyTitle')}</h1>
48
+ <p className="text-muted-foreground mt-2">{t('emptySubtitle')}</p>
49
+ <Link
50
+ href="/products"
51
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
52
+ >
53
+ {tc('continueShopping')}
54
+ </Link>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ return (
60
+ <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
61
+ <h1 className="text-foreground mb-6 text-2xl font-bold">
62
+ {t('title')} ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
63
+ </h1>
64
+
65
+ {/* Reservation countdown */}
66
+ {cart.reservation?.hasReservation && (
67
+ <ReservationCountdown reservation={cart.reservation} className="mb-6" />
68
+ )}
69
+
70
+ <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
71
+ {/* Cart Items */}
72
+ <div className="lg:col-span-2">
73
+ {/* Nudges */}
74
+ {cart.nudges && cart.nudges.length > 0 && (
75
+ <CartNudges nudges={cart.nudges} className="mb-4" />
76
+ )}
77
+
78
+ {/* Cart items */}
79
+ <div>
80
+ {cart.items.map((item) => (
81
+ <div key={item.id}>
82
+ <CartItem item={item} onUpdate={refreshCart} />
83
+ {upgrades?.upgrades?.[item.productId] && (
84
+ <CartUpgradeBanner
85
+ suggestion={upgrades.upgrades[item.productId]}
86
+ cartItem={item}
87
+ onUpgrade={refreshCart}
88
+ className="mb-2 ms-24"
89
+ />
90
+ )}
91
+ </div>
92
+ ))}
93
+ </div>
94
+
95
+ {/* Bundle offers */}
96
+ {bundles?.bundles && bundles.bundles.length > 0 && (
97
+ <div className="mt-6 space-y-3">
98
+ <h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
99
+ {bundles.bundles.map((offer) => (
100
+ <CartBundleOfferCard
101
+ key={offer.id}
102
+ offer={offer}
103
+ cartId={cart.id}
104
+ onAdd={refreshCart}
105
+ />
106
+ ))}
107
+ </div>
108
+ )}
109
+
110
+ {/* Coupon input */}
111
+ <div className="border-border mt-6 border-t pt-4">
112
+ <CouponInput cart={cart} onUpdate={refreshCart} />
113
+ </div>
114
+ </div>
115
+
116
+ {/* Summary sidebar */}
117
+ <div className="lg:col-span-1">
118
+ <div className="bg-muted/50 border-border sticky top-24 rounded-lg border p-6">
119
+ <FreeShippingBar className="mb-4" />
120
+ <CartSummary />
121
+
122
+ <Link
123
+ href="/checkout"
124
+ className="bg-primary text-primary-foreground mt-6 inline-flex w-full items-center justify-center rounded px-6 py-3 text-sm font-medium transition-opacity hover:opacity-90"
125
+ >
126
+ {t('proceedToCheckout')}
127
+ </Link>
128
+
129
+ <Link
130
+ href="/products"
131
+ className="text-muted-foreground hover:text-foreground mt-3 inline-flex w-full items-center justify-center px-6 py-2 text-sm transition-colors"
132
+ >
133
+ {tc('continueShopping')}
134
+ </Link>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ {/* Cross-sell recommendations */}
140
+ {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
141
+ <CartRecommendationSection
142
+ title={t('youMightAlsoNeed')}
143
+ items={cartRecs.recommendations}
144
+ className="mt-10"
145
+ />
146
+ )}
147
+ </div>
148
+ );
149
+ }
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { useState } from 'react';
4
4
  import type { Cart } from 'brainerce';
5
- import { getClient } from '@/lib/brainerce';
6
- import { useTranslations } from '@/lib/translations';
7
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
8
- import { cn } from '@/lib/utils';
5
+ import { getClient } from '@/core/lib/brainerce';
6
+ import { useTranslations } from '@/core/lib/translations';
7
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
8
+ import { cn } from '@/core/lib/utils';
9
9
 
10
10
  interface CouponInputProps {
11
11
  cart: Cart;
@@ -1,10 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import { formatPrice } from 'brainerce';
4
- import { useStoreInfo, useCart } from '@/providers/store-provider';
5
- import { useCurrency } from '@/lib/use-currency';
6
- import { useTranslations } from '@/lib/translations';
7
- import { cn } from '@/lib/utils';
4
+ import { useStoreInfo, useCart } from '@/core/providers/store-provider';
5
+ import { useCurrency } from '@/core/lib/use-currency';
6
+ import { useTranslations } from '@/core/lib/translations';
7
+ import { cn } from '@/core/lib/utils';
8
8
 
9
9
  interface FreeShippingBarProps {
10
10
  className?: string;
@@ -2,8 +2,8 @@
2
2
 
3
3
  import { useState, useEffect, useCallback } from 'react';
4
4
  import type { ReservationInfo } from 'brainerce';
5
- import { useTranslations } from '@/lib/translations';
6
- import { cn } from '@/lib/utils';
5
+ import { useTranslations } from '@/core/lib/translations';
6
+ import { cn } from '@/core/lib/utils';
7
7
 
8
8
  interface ReservationCountdownProps {
9
9
  reservation: ReservationInfo;
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ import type { DiscountBanner } from 'brainerce';
4
+
5
+ /** Store-wide discount banner strip shown at the very top of the homepage. */
6
+ export function DiscountBannerStrip({ banners }: { banners: DiscountBanner[] }) {
7
+ if (banners.length === 0) return null;
8
+
9
+ return (
10
+ <div className="bg-primary text-primary-foreground">
11
+ <div className="mx-auto max-w-7xl px-4 py-2 sm:px-6 lg:px-8">
12
+ <div className="flex items-center justify-center gap-4 overflow-x-auto text-sm font-medium">
13
+ {banners.map((banner) => (
14
+ <span key={banner.ruleId}>{banner.text}</span>
15
+ ))}
16
+ </div>
17
+ </div>
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import type { Product } from 'brainerce';
4
+ import { Link } from '@/core/lib/navigation';
5
+ import { useTranslations } from '@/core/lib/translations';
6
+ import { ProductGrid } from '@/ui/product/product-grid';
7
+
8
+ /** Homepage featured-products grid with a "view all" link. */
9
+ export function FeaturedProductsSection({ products }: { products: Product[] }) {
10
+ const t = useTranslations('home');
11
+ const tc = useTranslations('common');
12
+
13
+ return (
14
+ <section className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
15
+ <div className="mb-8 flex items-center justify-between">
16
+ <h2 className="text-foreground text-2xl font-bold">{t('featuredProducts')}</h2>
17
+ <Link href="/products" className="text-primary text-sm font-medium hover:underline">
18
+ {tc('viewAll')}
19
+ </Link>
20
+ </div>
21
+ <ProductGrid products={products} />
22
+ </section>
23
+ );
24
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { Link } from '@/core/lib/navigation';
4
+ import { useTranslations } from '@/core/lib/translations';
5
+
6
+ /** Homepage hero: welcome headline, subtitle, and shop-now CTA. */
7
+ export function HeroSection({ storeName }: { storeName?: string | null }) {
8
+ const t = useTranslations('home');
9
+ const tc = useTranslations('common');
10
+
11
+ return (
12
+ <section className="bg-muted">
13
+ <div className="mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 lg:px-8">
14
+ <h1 className="text-foreground text-4xl font-bold tracking-tight sm:text-5xl">
15
+ {t('welcomeTo')} {storeName || tc('store')}
16
+ </h1>
17
+ <p className="text-muted-foreground mx-auto mt-4 max-w-2xl text-lg">
18
+ {t('heroSubtitle')}
19
+ </p>
20
+ <Link
21
+ href="/products"
22
+ className="bg-primary text-primary-foreground mt-8 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
23
+ >
24
+ {tc('shopNow')}
25
+ </Link>
26
+ </div>
27
+ </section>
28
+ );
29
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { useStoreInfo } from '@/core/providers/store-provider';
4
+ import { useHomeData } from '@/core/hooks/use-home-data';
5
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
6
+ import { DiscountBannerStrip } from './discount-banner-strip';
7
+ import { HeroSection } from './hero-section';
8
+ import { FeaturedProductsSection } from './featured-products-section';
9
+
10
+ export function HomeClient() {
11
+ const { storeInfo, loading: storeLoading } = useStoreInfo();
12
+ const { products, banners, loading } = useHomeData();
13
+
14
+ if (storeLoading || loading) {
15
+ return (
16
+ <div className="flex min-h-[60vh] items-center justify-center">
17
+ <LoadingSpinner size="lg" />
18
+ </div>
19
+ );
20
+ }
21
+
22
+ return (
23
+ <div>
24
+ <DiscountBannerStrip banners={banners} />
25
+ <HeroSection storeName={storeInfo?.name} />
26
+ <FeaturedProductsSection products={products} />
27
+ </div>
28
+ );
29
+ }
@@ -20,8 +20,8 @@
20
20
 
21
21
  import * as React from 'react';
22
22
  import type { Content } from 'brainerce';
23
- import { useTranslations } from '@/lib/translations';
24
- import { sanitizeHtml } from '@/lib/sanitize';
23
+ import { useTranslations } from '@/core/lib/translations';
24
+ import { sanitizeHtml } from '@/core/lib/sanitize';
25
25
 
26
26
  interface FaqSectionProps {
27
27
  /** Pre-fetched FAQ row. `null` renders the empty-state fallback. */
@@ -13,9 +13,9 @@
13
13
  * the cart icon's icon-only treatment; the icon + aria-label keep it accessible.
14
14
  */
15
15
  import * as React from 'react';
16
- import { Link } from '@/lib/navigation';
17
- import { useAuth } from '@/providers/store-provider';
18
- import { useTranslations } from '@/lib/translations';
16
+ import { Link } from '@/core/lib/navigation';
17
+ import { useAuth } from '@/core/providers/store-provider';
18
+ import { useTranslations } from '@/core/lib/translations';
19
19
 
20
20
  const UserIcon = (props: React.SVGProps<SVGSVGElement>) => (
21
21
  <svg