create-brainerce-store 1.51.0 → 1.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +9 -68
  3. package/messages/en.json +32 -0
  4. package/messages/he.json +32 -0
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.claude/commands/design.md +9 -0
  7. package/templates/nextjs/base/AI-GUIDE.md +52 -3
  8. package/templates/nextjs/base/CLAUDE.md +10 -0
  9. package/templates/nextjs/base/package.json.ejs +52 -39
  10. package/templates/nextjs/base/src/app/globals.css +36 -30
  11. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
  12. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
  13. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
  14. package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
  15. package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
  16. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
  17. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
  18. package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
  19. package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
  20. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
  21. package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
  22. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
  23. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
  24. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
  25. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
  26. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
  27. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
  28. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
  29. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
  30. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
  31. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
  32. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
  33. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
  34. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
  35. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
  37. package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
  38. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
  39. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
  40. package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
  41. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
  42. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
  43. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
  44. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
  45. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
  46. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
  47. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
  48. package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
  49. package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
  50. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
  51. package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
  52. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
  53. package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
  54. package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
  55. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
  56. package/templates/nextjs/base/tailwind.config.ts +69 -42
  57. package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
  58. package/templates/nextjs/designs/atelier/globals.css +396 -0
  59. package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
  60. package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
  61. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
  62. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
  63. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
  64. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
  65. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
  66. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
  67. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
  68. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
  69. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
  70. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
  71. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
  72. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
  73. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
  74. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
  75. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
  76. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
  77. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
  78. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
  79. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
  80. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
  81. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
  82. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
  83. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
  84. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
  85. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
  86. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
  87. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
  88. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
  89. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
  90. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
  91. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
  92. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
  93. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
  94. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
  95. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
  96. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
  97. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
  98. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
  99. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
  100. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
  101. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
  102. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
  103. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
  104. package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
  105. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
  106. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
  107. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
  108. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
  109. package/templates/nextjs/themes/luxury/globals.css +405 -399
  110. package/templates/nextjs/themes/minimal/globals.css +36 -30
  111. package/templates/nextjs/themes/playful/globals.css +406 -400
  112. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
  113. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
  114. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
  115. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
  116. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
  117. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
  118. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
  119. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
  120. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
  121. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
  122. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
  123. package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
  124. package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
  125. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
  126. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
  127. package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
  128. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
  129. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
  130. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
  131. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
  132. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
  133. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
  134. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
  135. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
  136. package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
  137. package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
  138. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
  139. package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
  140. package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
  141. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
  142. package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
  143. package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
  144. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
  145. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
  146. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
  147. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
  148. package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
  149. package/templates/nextjs/designs/passepartout/globals.css +0 -412
  150. package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
  151. package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
  152. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
  153. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
  154. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
  155. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
  156. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
  157. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
  158. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
  159. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
  160. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
  161. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
  162. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
  163. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
  164. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
  165. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
  166. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
  167. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
  168. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
  169. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
  170. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
@@ -1,103 +1,90 @@
1
- /**
2
- * FAQ accordion — fully driven by the merchant's dashboard configuration.
3
- *
4
- * The merchant configures FAQs in the Brainerce dashboard under
5
- * Sell → Content → FAQ
6
- *
7
- * Each FAQ row has a `key` (e.g. 'main', 'shipping', 'returns'). Pass the
8
- * pre-fetched payload as `faq`; this component knows nothing about which
9
- * key it came from.
10
- *
11
- * Two render branches:
12
- * 1. `faq` has items → accordion of question/answer pairs
13
- * 2. `faq` is null OR has no items → friendly empty state with a contact
14
- * CTA, so the /faq page never looks broken before the merchant seeds.
15
- *
16
- * Security: FAQ answers may contain merchant-authored HTML — we sanitize
17
- * via `sanitizeHtml` before injecting via dangerouslySetInnerHTML.
18
- */
19
- 'use client';
20
-
21
- import * as React from 'react';
22
- import type { Content } from 'brainerce';
23
- import { useTranslations } from '@/core/lib/translations';
24
- import { sanitizeHtml } from '@/core/lib/sanitize';
25
-
26
- interface FaqSectionProps {
27
- /** Pre-fetched FAQ row. `null` renders the empty-state fallback. */
28
- faq: Content<'FAQ'> | null;
29
- }
30
-
31
- export function FaqSection({ faq }: FaqSectionProps) {
32
- const t = useTranslations('content');
33
- const [openIdx, setOpenIdx] = React.useState<number | null>(0);
34
-
35
- const items = faq?.data?.items ?? [];
36
- const heading = faq?.name || t('faqTitle');
37
-
38
- // Empty state — runs when the merchant hasn't created a FAQ row yet,
39
- // or the row exists but contains no items. Keep the page non-empty and
40
- // give the visitor a path to contact instead of a blank screen.
41
- if (!faq || items.length === 0) {
42
- return (
43
- <section className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
44
- <h1 className="text-foreground mb-3 text-2xl font-semibold sm:text-3xl">{heading}</h1>
45
- <p className="text-muted-foreground mb-6 text-sm sm:text-base">
46
- <strong className="text-foreground">{t('faqEmptyTitle')}</strong>
47
- <br />
48
- {t('faqEmptyBody')}
49
- </p>
50
- <a
51
- href="/contact"
52
- className="bg-primary text-primary-foreground inline-flex items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors hover:opacity-90"
53
- >
54
- {t('faqContactCta')}
55
- </a>
56
- </section>
57
- );
58
- }
59
-
60
- return (
61
- <section className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
62
- <h1 className="text-foreground mb-6 text-2xl font-semibold sm:text-3xl">{heading}</h1>
63
- <ul className="border-border divide-border divide-y rounded-lg border">
64
- {items.map((item, idx) => {
65
- const open = openIdx === idx;
66
- return (
67
- <li key={idx}>
68
- <button
69
- type="button"
70
- onClick={() => setOpenIdx(open ? null : idx)}
71
- aria-expanded={open}
72
- className="hover:bg-muted/40 flex w-full items-center justify-between gap-4 p-4 text-start transition-colors"
73
- >
74
- <span className="text-foreground text-sm font-medium sm:text-base">
75
- {item.question}
76
- </span>
77
- <svg
78
- xmlns="http://www.w3.org/2000/svg"
79
- viewBox="0 0 24 24"
80
- fill="none"
81
- stroke="currentColor"
82
- strokeWidth="2"
83
- className={`text-muted-foreground h-4 w-4 transition-transform ${
84
- open ? 'rotate-180' : ''
85
- }`}
86
- aria-hidden="true"
87
- >
88
- <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
89
- </svg>
90
- </button>
91
- {open ? (
92
- <div
93
- className="text-muted-foreground prose prose-sm dark:prose-invert max-w-none px-4 pb-4 text-sm leading-relaxed"
94
- dangerouslySetInnerHTML={{ __html: sanitizeHtml(item.answer) }}
95
- />
96
- ) : null}
97
- </li>
98
- );
99
- })}
100
- </ul>
101
- </section>
102
- );
103
- }
1
+ /**
2
+ * FAQ accordion — fully driven by the merchant's dashboard configuration.
3
+ *
4
+ * The merchant configures FAQs in the Brainerce dashboard under
5
+ * Sell → Content → FAQ
6
+ *
7
+ * Each FAQ row has a `key` (e.g. 'main', 'shipping', 'returns'). Pass the
8
+ * pre-fetched payload as `faq`; this component knows nothing about which
9
+ * key it came from.
10
+ *
11
+ * Two render branches:
12
+ * 1. `faq` has items → shadcn Accordion of question/answer pairs
13
+ * 2. `faq` is null OR has no items → friendly empty state with a contact
14
+ * CTA, so the /faq page never looks broken before the merchant seeds.
15
+ *
16
+ * Security: FAQ answers may contain merchant-authored HTML — we sanitize
17
+ * via `sanitizeHtml` before injecting via dangerouslySetInnerHTML.
18
+ */
19
+ 'use client';
20
+
21
+ import * as React from 'react';
22
+ import type { Content } from 'brainerce';
23
+ import { useTranslations } from '@/core/lib/translations';
24
+ import { sanitizeHtml } from '@/core/lib/sanitize';
25
+ import { Link } from '@/core/lib/navigation';
26
+ import {
27
+ Accordion,
28
+ AccordionContent,
29
+ AccordionItem,
30
+ AccordionTrigger,
31
+ } from '@/components/ui/accordion';
32
+ import { Button } from '@/components/ui/button';
33
+
34
+ interface FaqSectionProps {
35
+ /** Pre-fetched FAQ row. `null` renders the empty-state fallback. */
36
+ faq: Content<'FAQ'> | null;
37
+ }
38
+
39
+ export function FaqSection({ faq }: FaqSectionProps) {
40
+ const t = useTranslations('content');
41
+
42
+ const items = faq?.data?.items ?? [];
43
+ const heading = faq?.name || t('faqTitle');
44
+
45
+ // Empty state runs when the merchant hasn't created a FAQ row yet,
46
+ // or the row exists but contains no items. Keep the page non-empty and
47
+ // give the visitor a path to contact instead of a blank screen.
48
+ if (!faq || items.length === 0) {
49
+ return (
50
+ <section className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
51
+ <h1 className="text-foreground mb-3 text-2xl font-semibold sm:text-3xl">{heading}</h1>
52
+ <p className="text-muted-foreground mb-6 text-sm sm:text-base">
53
+ <strong className="text-foreground">{t('faqEmptyTitle')}</strong>
54
+ <br />
55
+ {t('faqEmptyBody')}
56
+ </p>
57
+ <Button asChild>
58
+ <Link href="/contact">{t('faqContactCta')}</Link>
59
+ </Button>
60
+ </section>
61
+ );
62
+ }
63
+
64
+ return (
65
+ <section className="mx-auto max-w-3xl px-4 py-10 sm:px-6 lg:px-8">
66
+ <h1 className="text-foreground mb-6 text-2xl font-semibold sm:text-3xl">{heading}</h1>
67
+ <Accordion
68
+ type="single"
69
+ collapsible
70
+ defaultValue="faq-0"
71
+ className="border-border rounded-lg border"
72
+ >
73
+ {items.map((item, idx) => (
74
+ <AccordionItem
75
+ key={idx}
76
+ value={`faq-${idx}`}
77
+ className="border-border last:border-b-0"
78
+ >
79
+ <AccordionTrigger className="hover:bg-muted/40 p-4 text-sm hover:no-underline sm:text-base">
80
+ {item.question}
81
+ </AccordionTrigger>
82
+ <AccordionContent className="text-muted-foreground prose prose-sm dark:prose-invert max-w-none px-4 pb-4 text-sm leading-relaxed">
83
+ <div dangerouslySetInnerHTML={{ __html: sanitizeHtml(item.answer) }} />
84
+ </AccordionContent>
85
+ </AccordionItem>
86
+ ))}
87
+ </Accordion>
88
+ </section>
89
+ );
90
+ }
@@ -13,26 +13,11 @@
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 { User } from 'lucide-react';
16
17
  import { Link } from '@/core/lib/navigation';
17
18
  import { useAuth } from '@/core/providers/store-provider';
18
19
  import { useTranslations } from '@/core/lib/translations';
19
20
 
20
- const UserIcon = (props: React.SVGProps<SVGSVGElement>) => (
21
- <svg
22
- viewBox="0 0 24 24"
23
- fill="none"
24
- stroke="currentColor"
25
- strokeWidth={2}
26
- strokeLinecap="round"
27
- strokeLinejoin="round"
28
- aria-hidden="true"
29
- {...props}
30
- >
31
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
32
- <circle cx="12" cy="7" r="4" />
33
- </svg>
34
- );
35
-
36
21
  export function HeaderAccount() {
37
22
  const { isLoggedIn } = useAuth();
38
23
  const t = useTranslations('nav');
@@ -48,7 +33,7 @@ export function HeaderAccount() {
48
33
  aria-label={label}
49
34
  className="text-foreground/80 hover:text-foreground hover:bg-muted/60 inline-flex h-9 items-center justify-center gap-1.5 rounded-md px-2 transition-colors"
50
35
  >
51
- <UserIcon className="h-5 w-5" />
36
+ <User className="h-5 w-5" aria-hidden="true" />
52
37
  <span className="hidden text-sm font-medium sm:inline">{label}</span>
53
38
  </Link>
54
39
  );
@@ -1,70 +1,80 @@
1
- <% if (i18nEnabled) { %>
2
- 'use client';
3
-
4
- import { usePathname } from 'next/navigation';
5
- import { supportedLocales } from '@/i18n';
6
-
7
- const LOCALE_NAMES: Record<string, string> = {
8
- en: 'English',
9
- he: 'עברית',
10
- ar: 'العربية',
11
- es: 'Español',
12
- fr: 'Français',
13
- de: 'Deutsch',
14
- it: 'Italiano',
15
- pt: 'Português',
16
- nl: 'Nederlands',
17
- ja: '日本語',
18
- ko: '한국어',
19
- zh: '中文',
20
- ru: 'Русский',
21
- tr: 'Türkçe',
22
- pl: 'Polski',
23
- sv: 'Svenska',
24
- da: 'Dansk',
25
- no: 'Norsk',
26
- fi: 'Suomi',
27
- cs: 'Čeština',
28
- th: 'ไทย',
29
- vi: 'Tiếng Việt',
30
- };
31
-
32
- const defaultLocale = '<%= defaultLocale %>';
33
-
34
- export function LanguageSwitcher() {
35
- const pathname = usePathname();
36
- const firstSeg = pathname.split('/')[1];
37
- const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
38
- ? firstSeg
39
- : defaultLocale;
40
-
41
- function switchLocale(newLocale: string) {
42
- // Strip any existing locale segment, then re-add only if the new one
43
- // isn't the default — matches the as-needed prefix scheme in middleware.
44
- const segments = pathname.split('/');
45
- if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
46
- segments.splice(1, 1);
47
- }
48
- const bare = segments.join('/') || '/';
49
- window.location.href =
50
- newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
51
- }
52
-
53
- if (supportedLocales.length <= 1) return null;
54
-
55
- return (
56
- <select
57
- value={currentLocale}
58
- onChange={(e) => switchLocale(e.target.value)}
59
- className="text-sm border rounded px-2 py-1 bg-transparent"
60
- aria-label="Select language"
61
- >
62
- {supportedLocales.map((locale) => (
63
- <option key={locale} value={locale}>
64
- {LOCALE_NAMES[locale] || locale}
65
- </option>
66
- ))}
67
- </select>
68
- );
69
- }
70
- <% } %>
1
+ <% if (i18nEnabled) { %>
2
+ 'use client';
3
+
4
+ import { usePathname } from 'next/navigation';
5
+ import { supportedLocales } from '@/i18n';
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from '@/components/ui/select';
13
+
14
+ const LOCALE_NAMES: Record<string, string> = {
15
+ en: 'English',
16
+ he: 'עברית',
17
+ ar: 'العربية',
18
+ es: 'Español',
19
+ fr: 'Français',
20
+ de: 'Deutsch',
21
+ it: 'Italiano',
22
+ pt: 'Português',
23
+ nl: 'Nederlands',
24
+ ja: '日本語',
25
+ ko: '한국어',
26
+ zh: '中文',
27
+ ru: 'Русский',
28
+ tr: 'Türkçe',
29
+ pl: 'Polski',
30
+ sv: 'Svenska',
31
+ da: 'Dansk',
32
+ no: 'Norsk',
33
+ fi: 'Suomi',
34
+ cs: 'Čeština',
35
+ th: 'ไทย',
36
+ vi: 'Tiếng Việt',
37
+ };
38
+
39
+ const defaultLocale = '<%= defaultLocale %>';
40
+
41
+ export function LanguageSwitcher() {
42
+ const pathname = usePathname();
43
+ const firstSeg = pathname.split('/')[1];
44
+ const currentLocale = supportedLocales.includes(firstSeg as (typeof supportedLocales)[number])
45
+ ? firstSeg
46
+ : defaultLocale;
47
+
48
+ function switchLocale(newLocale: string) {
49
+ // Strip any existing locale segment, then re-add only if the new one
50
+ // isn't the default matches the as-needed prefix scheme in middleware.
51
+ const segments = pathname.split('/');
52
+ if (supportedLocales.includes(segments[1] as (typeof supportedLocales)[number])) {
53
+ segments.splice(1, 1);
54
+ }
55
+ const bare = segments.join('/') || '/';
56
+ window.location.href =
57
+ newLocale === defaultLocale ? bare : `/${newLocale}${bare === '/' ? '' : bare}`;
58
+ }
59
+
60
+ if (supportedLocales.length <= 1) return null;
61
+
62
+ return (
63
+ <Select value={currentLocale} onValueChange={switchLocale}>
64
+ <SelectTrigger
65
+ className="h-8 w-auto gap-1 rounded border bg-transparent px-2 py-1 text-sm"
66
+ aria-label="Select language"
67
+ >
68
+ <SelectValue />
69
+ </SelectTrigger>
70
+ <SelectContent>
71
+ {supportedLocales.map((locale) => (
72
+ <SelectItem key={locale} value={locale}>
73
+ {LOCALE_NAMES[locale] || locale}
74
+ </SelectItem>
75
+ ))}
76
+ </SelectContent>
77
+ </Select>
78
+ );
79
+ }
80
+ <% } %>