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,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { Check } from 'lucide-react';
3
4
  import { formatPrice } from 'brainerce';
4
5
  import { useStoreInfo, useCart } from '@/core/providers/store-provider';
5
6
  import { useCurrency } from '@/core/lib/use-currency';
@@ -35,9 +36,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
35
36
  return (
36
37
  <div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
37
38
  <div className="flex items-center gap-2 text-sm font-medium text-green-700">
38
- <svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
39
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
40
- </svg>
39
+ <Check className="h-4 w-4 shrink-0" aria-hidden="true" />
41
40
  {t('freeShippingQualified')}
42
41
  </div>
43
42
  </div>
@@ -1,103 +1,95 @@
1
- 'use client';
2
-
3
- import { useState, useEffect, useCallback } from 'react';
4
- import type { ReservationInfo } from 'brainerce';
5
- import { useTranslations } from '@/core/lib/translations';
6
- import { cn } from '@/core/lib/utils';
7
-
8
- interface ReservationCountdownProps {
9
- reservation: ReservationInfo;
10
- className?: string;
11
- }
12
-
13
- export function ReservationCountdown({ reservation, className }: ReservationCountdownProps) {
14
- const t = useTranslations('reservation');
15
- const [remainingSeconds, setRemainingSeconds] = useState<number>(0);
16
-
17
- const calculateRemaining = useCallback(() => {
18
- if (!reservation.expiresAt) return 0;
19
- const expiresAtMs = new Date(reservation.expiresAt).getTime();
20
- const nowMs = Date.now();
21
- return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
22
- }, [reservation.expiresAt]);
23
-
24
- useEffect(() => {
25
- setRemainingSeconds(calculateRemaining());
26
-
27
- const interval = setInterval(() => {
28
- const remaining = calculateRemaining();
29
- setRemainingSeconds(remaining);
30
-
31
- if (remaining <= 0) {
32
- clearInterval(interval);
33
- }
34
- }, 1000);
35
-
36
- return () => clearInterval(interval);
37
- }, [calculateRemaining]);
38
-
39
- if (!reservation.hasReservation) return null;
40
-
41
- const minutes = Math.floor(remainingSeconds / 60);
42
- const seconds = remainingSeconds % 60;
43
- const isExpired = remainingSeconds <= 0;
44
- const isUrgent = remainingSeconds > 0 && remainingSeconds < 120;
45
-
46
- const displayMessage = reservation.countdownMessage
47
- ? reservation.countdownMessage.replace(
48
- '{time}',
49
- `${minutes}:${seconds.toString().padStart(2, '0')}`
50
- )
51
- : null;
52
-
53
- return (
54
- <div
55
- className={cn(
56
- 'flex items-center gap-3 rounded-lg px-4 py-3 text-sm',
57
- isExpired
58
- ? 'bg-destructive/10 border-destructive/20 text-destructive border'
59
- : isUrgent
60
- ? 'border border-orange-200 bg-orange-50 text-orange-800 dark:border-orange-800 dark:bg-orange-950/30 dark:text-orange-300'
61
- : 'bg-primary/5 border-primary/20 text-foreground border',
62
- className
63
- )}
64
- >
65
- <svg
66
- className={cn(
67
- 'h-5 w-5 flex-shrink-0',
68
- isExpired
69
- ? 'text-destructive'
70
- : isUrgent
71
- ? 'text-orange-600 dark:text-orange-400'
72
- : 'text-primary'
73
- )}
74
- fill="none"
75
- viewBox="0 0 24 24"
76
- stroke="currentColor"
77
- >
78
- <path
79
- strokeLinecap="round"
80
- strokeLinejoin="round"
81
- strokeWidth={2}
82
- d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
83
- />
84
- </svg>
85
-
86
- <div className="flex-1">
87
- {isExpired ? (
88
- <p className="font-medium">{t('expired')}</p>
89
- ) : displayMessage ? (
90
- <p>{displayMessage}</p>
91
- ) : (
92
- <p>
93
- {isUrgent ? `${t('hurry')} ` : ''}
94
- {t('reservedFor')}{' '}
95
- <span className="font-semibold tabular-nums">
96
- {minutes}:{seconds.toString().padStart(2, '0')}
97
- </span>
98
- </p>
99
- )}
100
- </div>
101
- </div>
102
- );
103
- }
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useCallback } from 'react';
4
+ import { Clock } from 'lucide-react';
5
+ import type { ReservationInfo } from 'brainerce';
6
+ import { useTranslations } from '@/core/lib/translations';
7
+ import { cn } from '@/core/lib/utils';
8
+
9
+ interface ReservationCountdownProps {
10
+ reservation: ReservationInfo;
11
+ className?: string;
12
+ }
13
+
14
+ export function ReservationCountdown({ reservation, className }: ReservationCountdownProps) {
15
+ const t = useTranslations('reservation');
16
+ const [remainingSeconds, setRemainingSeconds] = useState<number>(0);
17
+
18
+ const calculateRemaining = useCallback(() => {
19
+ if (!reservation.expiresAt) return 0;
20
+ const expiresAtMs = new Date(reservation.expiresAt).getTime();
21
+ const nowMs = Date.now();
22
+ return Math.max(0, Math.floor((expiresAtMs - nowMs) / 1000));
23
+ }, [reservation.expiresAt]);
24
+
25
+ useEffect(() => {
26
+ setRemainingSeconds(calculateRemaining());
27
+
28
+ const interval = setInterval(() => {
29
+ const remaining = calculateRemaining();
30
+ setRemainingSeconds(remaining);
31
+
32
+ if (remaining <= 0) {
33
+ clearInterval(interval);
34
+ }
35
+ }, 1000);
36
+
37
+ return () => clearInterval(interval);
38
+ }, [calculateRemaining]);
39
+
40
+ if (!reservation.hasReservation) return null;
41
+
42
+ const minutes = Math.floor(remainingSeconds / 60);
43
+ const seconds = remainingSeconds % 60;
44
+ const isExpired = remainingSeconds <= 0;
45
+ const isUrgent = remainingSeconds > 0 && remainingSeconds < 120;
46
+
47
+ const displayMessage = reservation.countdownMessage
48
+ ? reservation.countdownMessage.replace(
49
+ '{time}',
50
+ `${minutes}:${seconds.toString().padStart(2, '0')}`
51
+ )
52
+ : null;
53
+
54
+ return (
55
+ <div
56
+ className={cn(
57
+ 'flex items-center gap-3 rounded-lg px-4 py-3 text-sm',
58
+ isExpired
59
+ ? 'bg-destructive/10 border-destructive/20 text-destructive border'
60
+ : isUrgent
61
+ ? 'border border-orange-200 bg-orange-50 text-orange-800 dark:border-orange-800 dark:bg-orange-950/30 dark:text-orange-300'
62
+ : 'bg-primary/5 border-primary/20 text-foreground border',
63
+ className
64
+ )}
65
+ >
66
+ <Clock
67
+ className={cn(
68
+ 'h-5 w-5 flex-shrink-0',
69
+ isExpired
70
+ ? 'text-destructive'
71
+ : isUrgent
72
+ ? 'text-orange-600 dark:text-orange-400'
73
+ : 'text-primary'
74
+ )}
75
+ aria-hidden="true"
76
+ />
77
+
78
+ <div className="flex-1">
79
+ {isExpired ? (
80
+ <p className="font-medium">{t('expired')}</p>
81
+ ) : displayMessage ? (
82
+ <p>{displayMessage}</p>
83
+ ) : (
84
+ <p>
85
+ {isUrgent ? `${t('hurry')} ` : ''}
86
+ {t('reservedFor')}{' '}
87
+ <span className="font-semibold tabular-nums">
88
+ {minutes}:{seconds.toString().padStart(2, '0')}
89
+ </span>
90
+ </p>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Link } from '@/core/lib/navigation';
4
4
  import { useTranslations } from '@/core/lib/translations';
5
+ import { Button } from '@/components/ui/button';
5
6
 
6
7
  /** Homepage hero: welcome headline, subtitle, and shop-now CTA. */
7
8
  export function HeroSection({ storeName }: { storeName?: string | null }) {
@@ -17,12 +18,9 @@ export function HeroSection({ storeName }: { storeName?: string | null }) {
17
18
  <p className="text-muted-foreground mx-auto mt-4 max-w-2xl text-lg">
18
19
  {t('heroSubtitle')}
19
20
  </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>
21
+ <Button asChild size="lg" className="mt-8 rounded px-6 text-base">
22
+ <Link href="/products">{tc('shopNow')}</Link>
23
+ </Button>
26
24
  </div>
27
25
  </section>
28
26
  );
@@ -1,125 +1,116 @@
1
- /**
2
- * Storefront announcement bar — fully driven by the merchant's dashboard.
3
- *
4
- * The merchant configures announcements in the Brainerce dashboard under
5
- * Sell → Content → Announcement
6
- *
7
- * Everything below is generic rendering logic — it adapts automatically to
8
- * any announcement shape returned by the API, so **you should not hardcode
9
- * the message or severity here**.
10
- *
11
- * API contract:
12
- * GET → client.content.announcement.list(locale) → Content<'ANNOUNCEMENT'>[]
13
- * Empty array → renders nothing (page layout stays intact).
14
- *
15
- * Time-bounded: each announcement may have `startsAt` / `endsAt` ISO
16
- * timestamps. We filter client-side so the cached server response can be
17
- * shared across visitors regardless of the current time.
18
- */
19
- 'use client';
20
-
21
- import * as React from 'react';
22
- import type { Content } from 'brainerce';
23
-
24
- interface AnnouncementBarProps {
25
- /** Pre-fetched list from the server. Pass `[]` to render nothing. */
26
- announcements: Content<'ANNOUNCEMENT'>[];
27
- }
28
-
29
- const SEVERITY_STYLES: Record<'info' | 'warning' | 'success', string> = {
30
- info: 'bg-blue-600 text-white',
31
- warning: 'bg-amber-500 text-amber-950',
32
- success: 'bg-emerald-600 text-white',
33
- };
34
-
35
- const STORAGE_KEY = 'brainerce_dismissed_announcements';
36
-
37
- export function AnnouncementBar({ announcements }: AnnouncementBarProps) {
38
- const [dismissed, setDismissed] = React.useState<Set<string>>(() => new Set());
39
-
40
- // Hydrate dismissals from localStorage on mount (avoids SSR/CSR mismatch).
41
- React.useEffect(() => {
42
- if (typeof window === 'undefined') return;
43
- try {
44
- const raw = window.localStorage.getItem(STORAGE_KEY);
45
- if (raw) setDismissed(new Set(JSON.parse(raw) as string[]));
46
- } catch {
47
- // ignore malformed storage
48
- }
49
- }, []);
50
-
51
- const dismiss = (id: string) => {
52
- setDismissed((prev) => {
53
- const next = new Set(prev);
54
- next.add(id);
55
- if (typeof window !== 'undefined') {
56
- try {
57
- window.localStorage.setItem(STORAGE_KEY, JSON.stringify([...next]));
58
- } catch {
59
- // ignore quota errors
60
- }
61
- }
62
- return next;
63
- });
64
- };
65
-
66
- const now = Date.now();
67
- const visible = announcements.filter((a) => {
68
- if (dismissed.has(a.id)) return false;
69
- const startOk = !a.data.startsAt || new Date(a.data.startsAt).getTime() <= now;
70
- const endOk = !a.data.endsAt || new Date(a.data.endsAt).getTime() >= now;
71
- return startOk && endOk;
72
- });
73
-
74
- if (visible.length === 0) return null;
75
-
76
- return (
77
- <div role="region" aria-label="Site announcements">
78
- {visible.map((announcement) => {
79
- const severity = announcement.data.severity;
80
- const tone = SEVERITY_STYLES[severity] ?? SEVERITY_STYLES.info;
81
- return (
82
- <div
83
- key={announcement.id}
84
- className={`flex items-center justify-center gap-3 px-4 py-2 text-sm ${tone}`}
85
- >
86
- <span className="flex-1 text-center">
87
- {announcement.data.message}
88
- {announcement.data.ctaHref && announcement.data.ctaLabel ? (
89
- <>
90
- {' '}
91
- <a
92
- href={announcement.data.ctaHref}
93
- className="underline underline-offset-2"
94
- >
95
- {announcement.data.ctaLabel}
96
- </a>
97
- </>
98
- ) : null}
99
- </span>
100
- {announcement.data.dismissible ? (
101
- <button
102
- type="button"
103
- onClick={() => dismiss(announcement.id)}
104
- aria-label="Dismiss announcement"
105
- className="text-current opacity-80 transition-opacity hover:opacity-100"
106
- >
107
- <svg
108
- xmlns="http://www.w3.org/2000/svg"
109
- viewBox="0 0 24 24"
110
- fill="none"
111
- stroke="currentColor"
112
- strokeWidth="2"
113
- className="h-4 w-4"
114
- aria-hidden="true"
115
- >
116
- <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
117
- </svg>
118
- </button>
119
- ) : null}
120
- </div>
121
- );
122
- })}
123
- </div>
124
- );
125
- }
1
+ /**
2
+ * Storefront announcement bar — fully driven by the merchant's dashboard.
3
+ *
4
+ * The merchant configures announcements in the Brainerce dashboard under
5
+ * Sell → Content → Announcement
6
+ *
7
+ * Everything below is generic rendering logic — it adapts automatically to
8
+ * any announcement shape returned by the API, so **you should not hardcode
9
+ * the message or severity here**.
10
+ *
11
+ * API contract:
12
+ * GET → client.content.announcement.list(locale) → Content<'ANNOUNCEMENT'>[]
13
+ * Empty array → renders nothing (page layout stays intact).
14
+ *
15
+ * Time-bounded: each announcement may have `startsAt` / `endsAt` ISO
16
+ * timestamps. We filter client-side so the cached server response can be
17
+ * shared across visitors regardless of the current time.
18
+ */
19
+ 'use client';
20
+
21
+ import * as React from 'react';
22
+ import { X } from 'lucide-react';
23
+ import type { Content } from 'brainerce';
24
+
25
+ interface AnnouncementBarProps {
26
+ /** Pre-fetched list from the server. Pass `[]` to render nothing. */
27
+ announcements: Content<'ANNOUNCEMENT'>[];
28
+ }
29
+
30
+ const SEVERITY_STYLES: Record<'info' | 'warning' | 'success', string> = {
31
+ info: 'bg-blue-600 text-white',
32
+ warning: 'bg-amber-500 text-amber-950',
33
+ success: 'bg-emerald-600 text-white',
34
+ };
35
+
36
+ const STORAGE_KEY = 'brainerce_dismissed_announcements';
37
+
38
+ export function AnnouncementBar({ announcements }: AnnouncementBarProps) {
39
+ const [dismissed, setDismissed] = React.useState<Set<string>>(() => new Set());
40
+
41
+ // Hydrate dismissals from localStorage on mount (avoids SSR/CSR mismatch).
42
+ React.useEffect(() => {
43
+ if (typeof window === 'undefined') return;
44
+ try {
45
+ const raw = window.localStorage.getItem(STORAGE_KEY);
46
+ if (raw) setDismissed(new Set(JSON.parse(raw) as string[]));
47
+ } catch {
48
+ // ignore malformed storage
49
+ }
50
+ }, []);
51
+
52
+ const dismiss = (id: string) => {
53
+ setDismissed((prev) => {
54
+ const next = new Set(prev);
55
+ next.add(id);
56
+ if (typeof window !== 'undefined') {
57
+ try {
58
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify([...next]));
59
+ } catch {
60
+ // ignore quota errors
61
+ }
62
+ }
63
+ return next;
64
+ });
65
+ };
66
+
67
+ const now = Date.now();
68
+ const visible = announcements.filter((a) => {
69
+ if (dismissed.has(a.id)) return false;
70
+ const startOk = !a.data.startsAt || new Date(a.data.startsAt).getTime() <= now;
71
+ const endOk = !a.data.endsAt || new Date(a.data.endsAt).getTime() >= now;
72
+ return startOk && endOk;
73
+ });
74
+
75
+ if (visible.length === 0) return null;
76
+
77
+ return (
78
+ <div role="region" aria-label="Site announcements">
79
+ {visible.map((announcement) => {
80
+ const severity = announcement.data.severity;
81
+ const tone = SEVERITY_STYLES[severity] ?? SEVERITY_STYLES.info;
82
+ return (
83
+ <div
84
+ key={announcement.id}
85
+ className={`flex items-center justify-center gap-3 px-4 py-2 text-sm ${tone}`}
86
+ >
87
+ <span className="flex-1 text-center">
88
+ {announcement.data.message}
89
+ {announcement.data.ctaHref && announcement.data.ctaLabel ? (
90
+ <>
91
+ {' '}
92
+ <a
93
+ href={announcement.data.ctaHref}
94
+ className="underline underline-offset-2"
95
+ >
96
+ {announcement.data.ctaLabel}
97
+ </a>
98
+ </>
99
+ ) : null}
100
+ </span>
101
+ {announcement.data.dismissible ? (
102
+ <button
103
+ type="button"
104
+ onClick={() => dismiss(announcement.id)}
105
+ aria-label="Dismiss announcement"
106
+ className="text-current opacity-80 transition-opacity hover:opacity-100"
107
+ >
108
+ <X className="h-4 w-4" aria-hidden="true" />
109
+ </button>
110
+ ) : null}
111
+ </div>
112
+ );
113
+ })}
114
+ </div>
115
+ );
116
+ }