create-brainerce-store 1.50.5 → 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 (230) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +146 -100
  3. package/messages/en.json +36 -1
  4. package/messages/he.json +36 -1
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.claude/commands/design.md +60 -0
  7. package/templates/nextjs/base/.env.local.ejs +5 -0
  8. package/templates/nextjs/base/.eslintrc.json +57 -3
  9. package/templates/nextjs/base/AGENTS.md +41 -0
  10. package/templates/nextjs/base/AI-GUIDE.md +143 -0
  11. package/templates/nextjs/base/CLAUDE.md +52 -0
  12. package/templates/nextjs/base/TRANSLATIONS.md +1 -1
  13. package/templates/nextjs/base/next.config.ts +2 -2
  14. package/templates/nextjs/base/package.json.ejs +52 -39
  15. package/templates/nextjs/base/src/app/account/page.tsx +5 -5
  16. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +1 -1
  17. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +2 -2
  18. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +1 -1
  19. package/templates/nextjs/base/src/app/auth/callback/page.tsx +4 -4
  20. package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +4 -4
  21. package/templates/nextjs/base/src/app/blog/page.tsx.ejs +2 -2
  22. package/templates/nextjs/base/src/app/cart/page.tsx +2 -173
  23. package/templates/nextjs/base/src/app/checkout/page.tsx +12 -11
  24. package/templates/nextjs/base/src/app/contact/page.tsx +3 -3
  25. package/templates/nextjs/base/src/app/faq/page.tsx.ejs +2 -2
  26. package/templates/nextjs/base/src/app/forgot-password/page.tsx +4 -4
  27. package/templates/nextjs/base/src/app/globals.css +36 -30
  28. package/templates/nextjs/base/src/app/layout.tsx.ejs +12 -12
  29. package/templates/nextjs/base/src/app/login/page.tsx +4 -4
  30. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +19 -12
  31. package/templates/nextjs/base/src/app/page.tsx +4 -4
  32. package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +3 -3
  33. package/templates/nextjs/base/src/app/payment-complete/page.tsx +1 -1
  34. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +6 -6
  35. package/templates/nextjs/base/src/app/products/page.tsx +4 -623
  36. package/templates/nextjs/base/src/app/register/page.tsx +4 -4
  37. package/templates/nextjs/base/src/app/reset-password/page.tsx +5 -5
  38. package/templates/nextjs/base/src/app/sitemap.ts +1 -1
  39. package/templates/nextjs/base/src/app/verify-email/page.tsx +5 -5
  40. package/templates/nextjs/base/src/components/account/address-book.tsx +3 -3
  41. package/templates/nextjs/base/src/components/account/order-customizations.tsx +3 -3
  42. package/templates/nextjs/base/src/components/account/order-history.tsx +13 -5
  43. package/templates/nextjs/base/src/components/account/order-payment-block.tsx +2 -2
  44. package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +2 -2
  45. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +2 -2
  46. package/templates/nextjs/base/src/components/account/profile-section.tsx +3 -3
  47. package/templates/nextjs/base/src/components/auth/login-form.tsx +4 -4
  48. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +4 -4
  49. package/templates/nextjs/base/src/components/auth/register-form.tsx +4 -4
  50. package/templates/nextjs/base/src/components/brainerce-bot.tsx +2 -2
  51. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +24 -3
  52. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +2 -2
  53. package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +2 -2
  54. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +4 -4
  55. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +20 -16
  56. package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +3 -3
  57. package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +3 -3
  58. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +3 -3
  59. package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +2 -2
  60. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +3 -3
  61. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
  62. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
  63. package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
  64. package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
  65. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
  66. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
  67. package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
  68. package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
  69. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
  70. package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
  71. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
  72. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
  73. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
  74. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
  75. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
  76. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
  77. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -0
  78. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -0
  79. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -0
  80. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +278 -0
  81. package/templates/nextjs/base/src/{hooks → core/hooks}/use-search.ts +1 -1
  82. package/templates/nextjs/base/src/{lib → core/lib}/brainerce.ts.ejs +1 -1
  83. package/templates/nextjs/base/src/core/lib/product-options.ts +121 -0
  84. package/templates/nextjs/base/src/{lib → core/lib}/resolve-currency.ts +1 -1
  85. package/templates/nextjs/base/src/{lib → core/lib}/safe-redirect.ts +1 -1
  86. package/templates/nextjs/base/src/{lib → core/lib}/sanitize.ts.ejs +1 -1
  87. package/templates/nextjs/base/src/{lib → core/lib}/use-currency.ts +1 -1
  88. package/templates/nextjs/base/src/{providers → core/providers}/store-provider.tsx.ejs +4 -4
  89. package/templates/nextjs/base/src/{components → ui}/cart/cart-bundle-offer.tsx +22 -16
  90. package/templates/nextjs/base/src/{components → ui}/cart/cart-item.tsx +8 -14
  91. package/templates/nextjs/base/src/{components → ui}/cart/cart-nudges.tsx +28 -39
  92. package/templates/nextjs/base/src/{components → ui}/cart/cart-summary.tsx +7 -5
  93. package/templates/nextjs/base/src/{components → ui}/cart/cart-upgrade-banner.tsx +13 -28
  94. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +140 -0
  95. package/templates/nextjs/base/src/{components → ui}/cart/coupon-input.tsx +14 -25
  96. package/templates/nextjs/base/src/{components → ui}/cart/free-shipping-bar.tsx +6 -7
  97. package/templates/nextjs/base/src/{components → ui}/cart/reservation-countdown.tsx +95 -103
  98. package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -0
  99. package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -0
  100. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -0
  101. package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -0
  102. package/templates/nextjs/base/src/{components/content → ui/layout}/announcement-bar.tsx.ejs +116 -125
  103. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -0
  104. package/templates/nextjs/base/src/{components/content → ui/layout}/header-account.tsx +5 -20
  105. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -0
  106. package/templates/nextjs/base/src/{components/content → ui/layout}/rich-text-block.tsx.ejs +1 -1
  107. package/templates/nextjs/base/src/{components/content → ui/layout}/site-footer.tsx.ejs +144 -164
  108. package/templates/nextjs/base/src/{components/content → ui/layout}/site-header.tsx.ejs +152 -182
  109. package/templates/nextjs/base/src/{components/products → ui/product}/customization-fields.tsx +457 -478
  110. package/templates/nextjs/base/src/{components/products → ui/product}/discount-badge.tsx +18 -22
  111. package/templates/nextjs/base/src/{components/products → ui/product}/frequently-bought-together.tsx +21 -33
  112. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -0
  113. package/templates/nextjs/base/src/{components/shared → ui/product}/price-display.tsx +5 -4
  114. package/templates/nextjs/base/src/{components/products → ui/product}/product-card.tsx +238 -271
  115. package/templates/nextjs/base/src/{app/products/[slug] → ui/product}/product-client-section.tsx +455 -631
  116. package/templates/nextjs/base/src/{components/products → ui/product}/product-grid.tsx +3 -3
  117. package/templates/nextjs/base/src/ui/product/product-listing.tsx +446 -0
  118. package/templates/nextjs/base/src/{components/products → ui/product}/recommendation-section.tsx +102 -107
  119. package/templates/nextjs/base/src/{components/reviews → ui/product}/review-form.tsx +46 -45
  120. package/templates/nextjs/base/src/{components/reviews/reviews-section.tsx → ui/product/reviews-section.tsx.ejs} +38 -15
  121. package/templates/nextjs/base/src/{components/products → ui/product}/stock-badge.tsx +61 -63
  122. package/templates/nextjs/base/src/{components/products → ui/product}/variant-selector.tsx +3 -3
  123. package/templates/nextjs/base/src/{components → ui}/shared/cdn-image.tsx +1 -1
  124. package/templates/nextjs/base/src/{components → ui}/shared/loading-spinner.tsx +2 -2
  125. package/templates/nextjs/base/tailwind.config.ts +69 -30
  126. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +251 -0
  127. package/templates/nextjs/designs/atelier/globals.css +396 -0
  128. package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
  129. package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
  130. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +110 -0
  131. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
  132. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
  133. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
  134. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
  135. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +123 -0
  136. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
  137. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
  138. package/templates/nextjs/designs/atelier/ui/cart/free-shipping-bar.tsx +77 -0
  139. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +89 -0
  140. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
  141. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
  142. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
  143. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
  144. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
  145. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
  146. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
  147. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
  148. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
  149. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
  150. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
  151. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
  152. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
  153. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
  154. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
  155. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
  156. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
  157. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
  158. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
  159. package/templates/nextjs/designs/atelier/ui/product/discount-badge.tsx +17 -0
  160. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +196 -0
  161. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
  162. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
  163. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
  164. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
  165. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
  166. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
  167. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
  168. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +267 -0
  169. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
  170. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
  171. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
  172. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
  173. package/templates/nextjs/designs/atelier/ui/shared/fly-to-cart.ts +86 -0
  174. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
  175. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
  176. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
  177. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
  178. package/templates/nextjs/themes/luxury/globals.css +405 -399
  179. package/templates/nextjs/themes/minimal/globals.css +36 -30
  180. package/templates/nextjs/themes/playful/globals.css +406 -400
  181. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +89 -0
  182. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +123 -0
  183. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +27 -0
  184. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +99 -0
  185. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +105 -0
  186. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -0
  187. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +119 -0
  188. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +54 -0
  189. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +81 -0
  190. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -0
  191. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -0
  192. package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -0
  193. package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -0
  194. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -0
  195. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -0
  196. package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -0
  197. package/templates/nextjs/{base/src/components → ui-canvas}/layout/language-switcher.tsx.ejs +76 -70
  198. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +36 -0
  199. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -0
  200. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -0
  201. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +445 -0
  202. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +19 -0
  203. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +168 -0
  204. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +165 -0
  205. package/templates/nextjs/ui-canvas/product/price-display.tsx +52 -0
  206. package/templates/nextjs/ui-canvas/product/product-card.tsx +197 -0
  207. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -0
  208. package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -0
  209. package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -0
  210. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +99 -0
  211. package/templates/nextjs/ui-canvas/product/review-form.tsx +266 -0
  212. package/templates/nextjs/ui-canvas/product/reviews-section.tsx.ejs +96 -0
  213. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +45 -0
  214. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +212 -0
  215. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +31 -0
  216. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +27 -0
  217. package/templates/nextjs/base/src/app/home-client.tsx +0 -98
  218. package/templates/nextjs/base/src/components/content/faq-section.tsx.ejs +0 -103
  219. package/templates/nextjs/base/src/components/products/modifier-group-selector.tsx +0 -238
  220. /package/templates/nextjs/base/src/{lib → core/lib}/auth.ts +0 -0
  221. /package/templates/nextjs/base/src/{lib → core/lib}/csrf.ts +0 -0
  222. /package/templates/nextjs/base/src/{lib → core/lib}/image-hosts.ts +0 -0
  223. /package/templates/nextjs/base/src/{lib → core/lib}/navigation.tsx.ejs +0 -0
  224. /package/templates/nextjs/base/src/{lib → core/lib}/nonce.ts +0 -0
  225. /package/templates/nextjs/base/src/{lib → core/lib}/sanitize-html.ts +0 -0
  226. /package/templates/nextjs/base/src/{lib → core/lib}/seo.ts +0 -0
  227. /package/templates/nextjs/base/src/{lib → core/lib}/store-info.ts +0 -0
  228. /package/templates/nextjs/base/src/{lib → core/lib}/translations.ts.ejs +0 -0
  229. /package/templates/nextjs/base/src/{lib → core/lib}/utils.ts +0 -0
  230. /package/templates/nextjs/base/src/{lib → core/lib}/validation.ts +0 -0
@@ -1,11 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { useState } from 'react';
4
+ import { Tag } from 'lucide-react';
4
5
  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';
6
+ import { getClient } from '@/core/lib/brainerce';
7
+ import { useTranslations } from '@/core/lib/translations';
8
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Input } from '@/components/ui/input';
11
+ import { cn } from '@/core/lib/utils';
9
12
 
10
13
  interface CouponInputProps {
11
14
  cart: Cart;
@@ -75,19 +78,7 @@ export function CouponInput({ cart, checkoutId, onUpdate, className }: CouponInp
75
78
  <div className={cn('space-y-2', className)}>
76
79
  <div className="bg-muted flex items-center justify-between rounded px-3 py-2">
77
80
  <div className="flex items-center gap-2">
78
- <svg
79
- className="text-primary h-4 w-4 flex-shrink-0"
80
- fill="none"
81
- viewBox="0 0 24 24"
82
- stroke="currentColor"
83
- >
84
- <path
85
- strokeLinecap="round"
86
- strokeLinejoin="round"
87
- strokeWidth={2}
88
- d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
89
- />
90
- </svg>
81
+ <Tag className="text-primary h-4 w-4 flex-shrink-0" aria-hidden="true" />
91
82
  <span className="text-foreground text-sm font-medium">{appliedCoupon}</span>
92
83
  </div>
93
84
  <button
@@ -106,7 +97,7 @@ export function CouponInput({ cart, checkoutId, onUpdate, className }: CouponInp
106
97
  return (
107
98
  <div className={cn('space-y-2', className)}>
108
99
  <div className="flex gap-2">
109
- <input
100
+ <Input
110
101
  type="text"
111
102
  value={code}
112
103
  onChange={(e) => {
@@ -120,23 +111,21 @@ export function CouponInput({ cart, checkoutId, onUpdate, className }: CouponInp
120
111
  }
121
112
  }}
122
113
  placeholder={t('placeholder')}
123
- className={cn(
124
- 'bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-9 flex-1 rounded border px-3 text-sm focus:outline-none focus:ring-2',
125
- error ? 'border-destructive' : 'border-border'
126
- )}
114
+ className={cn('h-9 flex-1 rounded', error && 'border-destructive')}
127
115
  />
128
- <button
116
+ <Button
129
117
  type="button"
118
+ variant="outline"
130
119
  onClick={handleApply}
131
120
  disabled={applying || !code.trim()}
132
- className="border-border bg-background text-foreground hover:bg-muted h-9 rounded border px-4 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40"
121
+ className="h-9 rounded px-4 hover:bg-muted"
133
122
  >
134
123
  {applying ? (
135
124
  <LoadingSpinner size="sm" className="border-muted-foreground/30 border-t-foreground" />
136
125
  ) : (
137
126
  tc('apply')
138
127
  )}
139
- </button>
128
+ </Button>
140
129
  </div>
141
130
 
142
131
  {error && <p className="text-destructive text-xs">{error}</p>}
@@ -1,10 +1,11 @@
1
1
  'use client';
2
2
 
3
+ import { Check } from 'lucide-react';
3
4
  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';
5
+ import { useStoreInfo, useCart } from '@/core/providers/store-provider';
6
+ import { useCurrency } from '@/core/lib/use-currency';
7
+ import { useTranslations } from '@/core/lib/translations';
8
+ import { cn } from '@/core/lib/utils';
8
9
 
9
10
  interface FreeShippingBarProps {
10
11
  className?: string;
@@ -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 '@/lib/translations';
6
- import { cn } from '@/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
+ }
@@ -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,27 @@
1
+ 'use client';
2
+
3
+ import { Link } from '@/core/lib/navigation';
4
+ import { useTranslations } from '@/core/lib/translations';
5
+ import { Button } from '@/components/ui/button';
6
+
7
+ /** Homepage hero: welcome headline, subtitle, and shop-now CTA. */
8
+ export function HeroSection({ storeName }: { storeName?: string | null }) {
9
+ const t = useTranslations('home');
10
+ const tc = useTranslations('common');
11
+
12
+ return (
13
+ <section className="bg-muted">
14
+ <div className="mx-auto max-w-7xl px-4 py-20 text-center sm:px-6 lg:px-8">
15
+ <h1 className="text-foreground text-4xl font-bold tracking-tight sm:text-5xl">
16
+ {t('welcomeTo')} {storeName || tc('store')}
17
+ </h1>
18
+ <p className="text-muted-foreground mx-auto mt-4 max-w-2xl text-lg">
19
+ {t('heroSubtitle')}
20
+ </p>
21
+ <Button asChild size="lg" className="mt-8 rounded px-6 text-base">
22
+ <Link href="/products">{tc('shopNow')}</Link>
23
+ </Button>
24
+ </div>
25
+ </section>
26
+ );
27
+ }
@@ -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
+ }