create-brainerce-store 1.62.0 → 1.64.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 (203) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +71 -71
  3. package/dist/index.js +26 -3
  4. package/messages/en.json +502 -502
  5. package/messages/he.json +502 -502
  6. package/package.json +1 -1
  7. package/templates/nextjs/base/.claude/commands/design.md +60 -60
  8. package/templates/nextjs/base/.env.local.ejs +24 -24
  9. package/templates/nextjs/base/.eslintrc.json +57 -57
  10. package/templates/nextjs/base/.mcp.json +8 -8
  11. package/templates/nextjs/base/AI-GUIDE.md +174 -174
  12. package/templates/nextjs/base/TRANSLATIONS.md +200 -200
  13. package/templates/nextjs/base/package.json.ejs +52 -52
  14. package/templates/nextjs/base/src/app/agents.md/route.ts +87 -0
  15. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +308 -262
  16. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +276 -276
  17. package/templates/nextjs/base/src/app/blog/rss.xml/route.ts +70 -0
  18. package/templates/nextjs/base/src/app/cart/page.tsx +7 -7
  19. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +29 -3
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -0
  21. package/templates/nextjs/base/src/app/contact/layout.tsx.ejs +14 -0
  22. package/templates/nextjs/base/src/app/globals.css +36 -36
  23. package/templates/nextjs/base/src/app/indexnow-key.txt/route.ts +26 -26
  24. package/templates/nextjs/base/src/app/layout.tsx.ejs +30 -0
  25. package/templates/nextjs/base/src/app/llms.txt/route.ts +28 -5
  26. package/templates/nextjs/base/src/app/not-found.tsx.ejs +35 -0
  27. package/templates/nextjs/base/src/app/opengraph-image.tsx +109 -0
  28. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +295 -278
  29. package/templates/nextjs/base/src/app/order-status/page.tsx +121 -121
  30. package/templates/nextjs/base/src/app/page.tsx +64 -64
  31. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  32. package/templates/nextjs/base/src/app/products/page.tsx +19 -19
  33. package/templates/nextjs/base/src/app/robots.ts +73 -14
  34. package/templates/nextjs/base/src/app/sitemap.ts +89 -66
  35. package/templates/nextjs/base/src/components/account/order-history.tsx +385 -385
  36. package/templates/nextjs/base/src/components/brainerce-bot.tsx +70 -70
  37. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +612 -612
  38. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +292 -292
  39. package/templates/nextjs/base/src/components/checkout/date-picker.tsx +309 -309
  40. package/templates/nextjs/base/src/components/checkout/datetime-picker.tsx +144 -144
  41. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +242 -242
  42. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +198 -198
  43. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +125 -125
  44. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +74 -74
  45. package/templates/nextjs/base/src/components/seo/article-json-ld.tsx +59 -59
  46. package/templates/nextjs/base/src/components/seo/breadcrumbs.tsx +37 -37
  47. package/templates/nextjs/base/src/components/seo/category-json-ld.tsx +61 -61
  48. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +97 -97
  49. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +23 -131
  50. package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +62 -0
  51. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -53
  52. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -32
  53. package/templates/nextjs/base/src/components/ui/button.tsx +49 -49
  54. package/templates/nextjs/base/src/components/ui/card.tsx +55 -55
  55. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -28
  56. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -107
  57. package/templates/nextjs/base/src/components/ui/input.tsx +22 -22
  58. package/templates/nextjs/base/src/components/ui/label.tsx +21 -21
  59. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -38
  60. package/templates/nextjs/base/src/components/ui/select.tsx +153 -153
  61. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -26
  62. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -125
  63. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -7
  64. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -55
  65. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -21
  66. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -32
  67. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -58
  68. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -51
  69. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -295
  70. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +20 -2
  71. package/templates/nextjs/base/src/core/lib/image-hosts.ts +7 -7
  72. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -121
  73. package/templates/nextjs/base/src/core/lib/resolve-currency.ts +20 -20
  74. package/templates/nextjs/base/src/core/lib/safe-redirect.ts +38 -38
  75. package/templates/nextjs/base/src/core/lib/seo.ts +46 -46
  76. package/templates/nextjs/base/src/core/lib/store-info.ts +17 -0
  77. package/templates/nextjs/base/src/core/lib/tracking.ts +171 -0
  78. package/templates/nextjs/base/src/core/lib/use-currency.ts +19 -19
  79. package/templates/nextjs/base/src/core/lib/utils.ts +21 -21
  80. package/templates/nextjs/base/src/middleware.ts.ejs +54 -2
  81. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +117 -117
  82. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +146 -146
  83. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -28
  84. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +110 -110
  85. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +126 -126
  86. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -140
  87. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +134 -134
  88. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +61 -61
  89. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -95
  90. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +22 -20
  91. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -24
  92. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  93. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -29
  94. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -116
  95. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -90
  96. package/templates/nextjs/base/src/ui/layout/header-account.tsx +40 -40
  97. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -80
  98. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -144
  99. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -152
  100. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +457 -457
  101. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -18
  102. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +191 -191
  103. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -206
  104. package/templates/nextjs/base/src/ui/product/price-display.tsx +62 -62
  105. package/templates/nextjs/base/src/ui/product/product-card.tsx +7 -2
  106. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -455
  107. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -446
  108. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -102
  109. package/templates/nextjs/base/src/ui/product/review-form.tsx +294 -294
  110. package/templates/nextjs/base/src/ui/product/reviews-section.tsx.ejs +108 -108
  111. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -61
  112. package/templates/nextjs/base/src/ui/product/variant-selector.tsx +291 -291
  113. package/templates/nextjs/base/src/ui/shared/cdn-image.tsx +26 -26
  114. package/templates/nextjs/base/tailwind.config.ts +69 -69
  115. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +30 -0
  116. package/templates/nextjs/designs/atelier/globals.css +402 -402
  117. package/templates/nextjs/designs/atelier/messages-patch/en.json +100 -100
  118. package/templates/nextjs/designs/atelier/messages-patch/he.json +100 -100
  119. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -110
  120. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -163
  121. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -140
  122. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -34
  123. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -107
  124. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -123
  125. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -147
  126. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -142
  127. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -77
  128. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -89
  129. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -34
  130. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +151 -151
  131. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -27
  132. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +75 -75
  133. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -36
  134. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +174 -174
  135. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -54
  136. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -78
  137. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -46
  138. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -118
  139. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -96
  140. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -36
  141. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -36
  142. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -137
  143. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -14
  144. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -34
  145. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -141
  146. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -138
  147. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -466
  148. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -17
  149. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -196
  150. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -193
  151. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -69
  152. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +13 -11
  153. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +477 -477
  154. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +42 -42
  155. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -260
  156. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -95
  157. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -267
  158. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -148
  159. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -66
  160. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -206
  161. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -29
  162. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -86
  163. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +276 -276
  164. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +63 -63
  165. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -71
  166. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -125
  167. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -89
  168. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -123
  169. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +29 -27
  170. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -99
  171. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -105
  172. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -106
  173. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -119
  174. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -54
  175. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -81
  176. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +21 -19
  177. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -23
  178. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -21
  179. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -26
  180. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -101
  181. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -71
  182. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -31
  183. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +76 -76
  184. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -36
  185. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -82
  186. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -119
  187. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -445
  188. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -19
  189. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -168
  190. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -165
  191. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -52
  192. package/templates/nextjs/ui-canvas/product/product-card.tsx +7 -2
  193. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -352
  194. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -27
  195. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -224
  196. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -99
  197. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -266
  198. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -96
  199. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -45
  200. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -212
  201. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -31
  202. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -27
  203. package/templates/nextjs/base/tsconfig.tsbuildinfo +0 -1
@@ -1,36 +1,36 @@
1
- 'use client';
2
-
3
- /**
4
- * Cart link for the site header — client island so it can read the live
5
- * item count from useCart(). Icon + count bubble; count hides at zero.
6
- */
7
- import * as React from 'react';
8
- import { Link } from '@/core/lib/navigation';
9
- import { useCart } from '@/core/providers/store-provider';
10
- import { useTranslations } from '@/core/lib/translations';
11
- import { IconBag } from '@/ui/shared/icons';
12
-
13
- export function HeaderCart() {
14
- const { itemCount } = useCart();
15
- const t = useTranslations('nav');
16
-
17
- return (
18
- <Link
19
- id="cart-anchor"
20
- href="/cart"
21
- aria-label={t('cart')}
22
- className="relative inline-flex h-10 w-10 items-center justify-center rounded-full text-foreground transition-colors hover:bg-secondary"
23
- >
24
- <IconBag size={20} />
25
- {itemCount > 0 && (
26
- <span
27
- aria-hidden="true"
28
- className="absolute -top-0.5 -end-0.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[11px] font-bold leading-none text-primary-foreground"
29
- >
30
- {itemCount > 99 ? '99+' : itemCount}
31
- </span>
32
- )}
33
- <span className="sr-only">{t('cart')}</span>
34
- </Link>
35
- );
36
- }
1
+ 'use client';
2
+
3
+ /**
4
+ * Cart link for the site header — client island so it can read the live
5
+ * item count from useCart(). Icon + count bubble; count hides at zero.
6
+ */
7
+ import * as React from 'react';
8
+ import { Link } from '@/core/lib/navigation';
9
+ import { useCart } from '@/core/providers/store-provider';
10
+ import { useTranslations } from '@/core/lib/translations';
11
+ import { IconBag } from '@/ui/shared/icons';
12
+
13
+ export function HeaderCart() {
14
+ const { itemCount } = useCart();
15
+ const t = useTranslations('nav');
16
+
17
+ return (
18
+ <Link
19
+ id="cart-anchor"
20
+ href="/cart"
21
+ aria-label={t('cart')}
22
+ className="relative inline-flex h-10 w-10 items-center justify-center rounded-full text-foreground transition-colors hover:bg-secondary"
23
+ >
24
+ <IconBag size={20} />
25
+ {itemCount > 0 && (
26
+ <span
27
+ aria-hidden="true"
28
+ className="absolute -top-0.5 -end-0.5 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-[11px] font-bold leading-none text-primary-foreground"
29
+ >
30
+ {itemCount > 99 ? '99+' : itemCount}
31
+ </span>
32
+ )}
33
+ <span className="sr-only">{t('cart')}</span>
34
+ </Link>
35
+ );
36
+ }
@@ -1,137 +1,137 @@
1
- <% if (i18nEnabled) { %>
2
- 'use client';
3
-
4
- import { useEffect, useRef, useState } from 'react';
5
- import { usePathname } from 'next/navigation';
6
- import { supportedLocales } from '@/i18n';
7
-
8
- const LOCALE_NAMES: Record<string, string> = {
9
- en: 'English',
10
- he: 'עברית',
11
- ar: 'العربية',
12
- es: 'Español',
13
- fr: 'Français',
14
- de: 'Deutsch',
15
- it: 'Italiano',
16
- pt: 'Português',
17
- nl: 'Nederlands',
18
- ja: '日本語',
19
- ko: '한국어',
20
- zh: '中文',
21
- ru: 'Русский',
22
- tr: 'Türkçe',
23
- pl: 'Polski',
24
- sv: 'Svenska',
25
- da: 'Dansk',
26
- no: 'Norsk',
27
- fi: 'Suomi',
28
- cs: 'Čeština',
29
- th: 'ไทย',
30
- vi: 'Tiếng Việt',
31
- };
32
-
33
- const defaultLocale = '<%= defaultLocale %>';
34
-
35
- /**
36
- * DESIGN ME — locale switcher in the header; reads supported locales from
37
- * `@/i18n` and swaps the URL prefix. Keep the aria-label and the redirect
38
- * logic exactly as-is.
39
- */
40
- export function LanguageSwitcher() {
41
- const pathname = usePathname();
42
- const firstSeg = pathname.split('/')[1];
43
- const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
44
- ? firstSeg
45
- : defaultLocale;
46
-
47
- function switchLocale(newLocale: string) {
48
- // Strip any existing locale segment, then re-add only if the new one
49
- // isn't the default — matches the as-needed prefix scheme in middleware.
50
- const segments = pathname.split('/');
51
- if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
52
- segments.splice(1, 1);
53
- }
54
- const bare = segments.join('/') || '/';
55
- window.location.href =
56
- newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
57
- }
58
-
59
- const [open, setOpen] = useState(false);
60
- const rootRef = useRef<HTMLDivElement>(null);
61
-
62
- // Close on outside click / Escape — a native <select> looks cheap next to
63
- // the rest of the header, so this is a small styled popover instead.
64
- useEffect(() => {
65
- if (!open) return;
66
- function onDown(e: MouseEvent) {
67
- if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
68
- }
69
- function onKey(e: KeyboardEvent) {
70
- if (e.key === 'Escape') setOpen(false);
71
- }
72
- document.addEventListener('mousedown', onDown);
73
- document.addEventListener('keydown', onKey);
74
- return () => {
75
- document.removeEventListener('mousedown', onDown);
76
- document.removeEventListener('keydown', onKey);
77
- };
78
- }, [open]);
79
-
80
- if (supportedLocales.length <= 1) return null;
81
-
82
- return (
83
- <div ref={rootRef} className="relative">
84
- <button
85
- type="button"
86
- aria-label="Select language"
87
- aria-expanded={open}
88
- aria-haspopup="listbox"
89
- onClick={() => setOpen((v) => !v)}
90
- className="text-foreground/70 hover:text-foreground hover:bg-secondary/60 inline-flex h-9 items-center gap-1.5 rounded-full px-3 text-sm font-medium transition-colors"
91
- >
92
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} className="h-4 w-4" aria-hidden="true">
93
- <circle cx="12" cy="12" r="9" />
94
- <path d="M3 12h18M12 3c2.5 2.6 3.8 5.7 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.7-3.8-9S9.5 5.6 12 3z" />
95
- </svg>
96
- {LOCALE_NAMES[currentLocale] || currentLocale}
97
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`} aria-hidden="true">
98
- <path d="M6 9l6 6 6-6" />
99
- </svg>
100
- </button>
101
-
102
- {open && (
103
- <ul
104
- role="listbox"
105
- aria-label="Select language"
106
- className="border-border/60 bg-background absolute end-0 top-full z-50 mt-2 min-w-36 overflow-hidden rounded-xl border py-1 shadow-lg"
107
- >
108
- {supportedLocales.map((locale) => (
109
- <li key={locale}>
110
- <button
111
- type="button"
112
- role="option"
113
- aria-selected={locale === currentLocale}
114
- onClick={() => {
115
- setOpen(false);
116
- if (locale !== currentLocale) switchLocale(locale);
117
- }}
118
- className={`hover:bg-secondary/70 flex w-full items-center justify-between px-4 py-2 text-sm transition-colors ${
119
- locale === currentLocale ? 'text-primary font-semibold' : 'text-foreground/80'
120
- }`}
121
- >
122
- {LOCALE_NAMES[locale] || locale}
123
- {locale === currentLocale && (
124
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} className="h-4 w-4" aria-hidden="true">
125
- <path d="M5 13l4 4L19 7" />
126
- </svg>
127
- )}
128
- </button>
129
- </li>
130
- ))}
131
- </ul>
132
- )}
133
- </div>
134
- );
135
- }
136
-
137
- <% } %>
1
+ <% if (i18nEnabled) { %>
2
+ 'use client';
3
+
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import { usePathname } from 'next/navigation';
6
+ import { supportedLocales } from '@/i18n';
7
+
8
+ const LOCALE_NAMES: Record<string, string> = {
9
+ en: 'English',
10
+ he: 'עברית',
11
+ ar: 'العربية',
12
+ es: 'Español',
13
+ fr: 'Français',
14
+ de: 'Deutsch',
15
+ it: 'Italiano',
16
+ pt: 'Português',
17
+ nl: 'Nederlands',
18
+ ja: '日本語',
19
+ ko: '한국어',
20
+ zh: '中文',
21
+ ru: 'Русский',
22
+ tr: 'Türkçe',
23
+ pl: 'Polski',
24
+ sv: 'Svenska',
25
+ da: 'Dansk',
26
+ no: 'Norsk',
27
+ fi: 'Suomi',
28
+ cs: 'Čeština',
29
+ th: 'ไทย',
30
+ vi: 'Tiếng Việt',
31
+ };
32
+
33
+ const defaultLocale = '<%= defaultLocale %>';
34
+
35
+ /**
36
+ * DESIGN ME — locale switcher in the header; reads supported locales from
37
+ * `@/i18n` and swaps the URL prefix. Keep the aria-label and the redirect
38
+ * logic exactly as-is.
39
+ */
40
+ export function LanguageSwitcher() {
41
+ const pathname = usePathname();
42
+ const firstSeg = pathname.split('/')[1];
43
+ const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
44
+ ? firstSeg
45
+ : defaultLocale;
46
+
47
+ function switchLocale(newLocale: string) {
48
+ // Strip any existing locale segment, then re-add only if the new one
49
+ // isn't the default — matches the as-needed prefix scheme in middleware.
50
+ const segments = pathname.split('/');
51
+ if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
52
+ segments.splice(1, 1);
53
+ }
54
+ const bare = segments.join('/') || '/';
55
+ window.location.href =
56
+ newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
57
+ }
58
+
59
+ const [open, setOpen] = useState(false);
60
+ const rootRef = useRef<HTMLDivElement>(null);
61
+
62
+ // Close on outside click / Escape — a native <select> looks cheap next to
63
+ // the rest of the header, so this is a small styled popover instead.
64
+ useEffect(() => {
65
+ if (!open) return;
66
+ function onDown(e: MouseEvent) {
67
+ if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
68
+ }
69
+ function onKey(e: KeyboardEvent) {
70
+ if (e.key === 'Escape') setOpen(false);
71
+ }
72
+ document.addEventListener('mousedown', onDown);
73
+ document.addEventListener('keydown', onKey);
74
+ return () => {
75
+ document.removeEventListener('mousedown', onDown);
76
+ document.removeEventListener('keydown', onKey);
77
+ };
78
+ }, [open]);
79
+
80
+ if (supportedLocales.length <= 1) return null;
81
+
82
+ return (
83
+ <div ref={rootRef} className="relative">
84
+ <button
85
+ type="button"
86
+ aria-label="Select language"
87
+ aria-expanded={open}
88
+ aria-haspopup="listbox"
89
+ onClick={() => setOpen((v) => !v)}
90
+ className="text-foreground/70 hover:text-foreground hover:bg-secondary/60 inline-flex h-9 items-center gap-1.5 rounded-full px-3 text-sm font-medium transition-colors"
91
+ >
92
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.6} className="h-4 w-4" aria-hidden="true">
93
+ <circle cx="12" cy="12" r="9" />
94
+ <path d="M3 12h18M12 3c2.5 2.6 3.8 5.7 3.8 9S14.5 18.4 12 21c-2.5-2.6-3.8-5.7-3.8-9S9.5 5.6 12 3z" />
95
+ </svg>
96
+ {LOCALE_NAMES[currentLocale] || currentLocale}
97
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={1.8} className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`} aria-hidden="true">
98
+ <path d="M6 9l6 6 6-6" />
99
+ </svg>
100
+ </button>
101
+
102
+ {open && (
103
+ <ul
104
+ role="listbox"
105
+ aria-label="Select language"
106
+ className="border-border/60 bg-background absolute end-0 top-full z-50 mt-2 min-w-36 overflow-hidden rounded-xl border py-1 shadow-lg"
107
+ >
108
+ {supportedLocales.map((locale) => (
109
+ <li key={locale}>
110
+ <button
111
+ type="button"
112
+ role="option"
113
+ aria-selected={locale === currentLocale}
114
+ onClick={() => {
115
+ setOpen(false);
116
+ if (locale !== currentLocale) switchLocale(locale);
117
+ }}
118
+ className={`hover:bg-secondary/70 flex w-full items-center justify-between px-4 py-2 text-sm transition-colors ${
119
+ locale === currentLocale ? 'text-primary font-semibold' : 'text-foreground/80'
120
+ }`}
121
+ >
122
+ {LOCALE_NAMES[locale] || locale}
123
+ {locale === currentLocale && (
124
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} className="h-4 w-4" aria-hidden="true">
125
+ <path d="M5 13l4 4L19 7" />
126
+ </svg>
127
+ )}
128
+ </button>
129
+ </li>
130
+ ))}
131
+ </ul>
132
+ )}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ <% } %>
@@ -1,14 +1,14 @@
1
- /**
2
- * Dashboard nav items default to CMS-page URLs (`/pages/<slug>`), but this
3
- * storefront ships dedicated routes for some of those destinations. Remap the
4
- * known slugs so merchant nav always lands on the richer page instead of a
5
- * thin (or missing) CMS page. Unknown URLs pass through untouched.
6
- */
7
- const ROUTE_ALIASES: Record<string, string> = {
8
- '/pages/contact': '/contact',
9
- '/pages/faq': '/faq',
10
- };
11
-
12
- export function resolveNavUrl(url: string): string {
13
- return ROUTE_ALIASES[url] ?? url;
14
- }
1
+ /**
2
+ * Dashboard nav items default to CMS-page URLs (`/pages/<slug>`), but this
3
+ * storefront ships dedicated routes for some of those destinations. Remap the
4
+ * known slugs so merchant nav always lands on the richer page instead of a
5
+ * thin (or missing) CMS page. Unknown URLs pass through untouched.
6
+ */
7
+ const ROUTE_ALIASES: Record<string, string> = {
8
+ '/pages/contact': '/contact',
9
+ '/pages/faq': '/faq',
10
+ };
11
+
12
+ export function resolveNavUrl(url: string): string {
13
+ return ROUTE_ALIASES[url] ?? url;
14
+ }
@@ -1,34 +1,34 @@
1
- /**
2
- * Inline rich-text block (bare skeleton) — fully driven by the merchant's
3
- * dashboard.
4
- *
5
- * The merchant configures rich-text blocks in the Brainerce dashboard
6
- * under Sell → Content → Rich Text. Use this component anywhere on a page
7
- * to drop a merchant-editable HTML block (intro copy, banner descriptions,
8
- * inline announcements that aren't tied to the top-of-page Announcement
9
- * bar, etc.).
10
- *
11
- * Security: HTML is sanitized via `sanitizeHtml` (isomorphic-dompurify)
12
- * before injecting via dangerouslySetInnerHTML.
13
- *
14
- * DESIGN ME — merchant-editable HTML block; fed by
15
- * `client.content.richText.get(key, locale)` pre-fetched in the route.
16
- * Style the injected HTML via the wrapper `className`.
17
- */
18
- import * as React from 'react';
19
- import type { Content } from 'brainerce';
20
- import { sanitizeHtml } from '@/core/lib/sanitize';
21
-
22
- interface RichTextBlockProps {
23
- /** Pre-fetched RICH_TEXT row, e.g. via `client.content.richText.get(key, locale)`. */
24
- block: Content<'RICH_TEXT'> | null;
25
- /** Optional wrapper className for layout positioning. */
26
- className?: string;
27
- }
28
-
29
- export function RichTextBlock({ block, className }: RichTextBlockProps) {
30
- if (!block?.data?.html) return null;
31
- return (
32
- <div className={className} dangerouslySetInnerHTML={{ __html: sanitizeHtml(block.data.html) }} />
33
- );
34
- }
1
+ /**
2
+ * Inline rich-text block (bare skeleton) — fully driven by the merchant's
3
+ * dashboard.
4
+ *
5
+ * The merchant configures rich-text blocks in the Brainerce dashboard
6
+ * under Sell → Content → Rich Text. Use this component anywhere on a page
7
+ * to drop a merchant-editable HTML block (intro copy, banner descriptions,
8
+ * inline announcements that aren't tied to the top-of-page Announcement
9
+ * bar, etc.).
10
+ *
11
+ * Security: HTML is sanitized via `sanitizeHtml` (isomorphic-dompurify)
12
+ * before injecting via dangerouslySetInnerHTML.
13
+ *
14
+ * DESIGN ME — merchant-editable HTML block; fed by
15
+ * `client.content.richText.get(key, locale)` pre-fetched in the route.
16
+ * Style the injected HTML via the wrapper `className`.
17
+ */
18
+ import * as React from 'react';
19
+ import type { Content } from 'brainerce';
20
+ import { sanitizeHtml } from '@/core/lib/sanitize';
21
+
22
+ interface RichTextBlockProps {
23
+ /** Pre-fetched RICH_TEXT row, e.g. via `client.content.richText.get(key, locale)`. */
24
+ block: Content<'RICH_TEXT'> | null;
25
+ /** Optional wrapper className for layout positioning. */
26
+ className?: string;
27
+ }
28
+
29
+ export function RichTextBlock({ block, className }: RichTextBlockProps) {
30
+ if (!block?.data?.html) return null;
31
+ return (
32
+ <div className={className} dangerouslySetInnerHTML={{ __html: sanitizeHtml(block.data.html) }} />
33
+ );
34
+ }