create-brainerce-store 1.72.0 → 1.74.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 (88) hide show
  1. package/dist/index.js +37 -4
  2. package/messages/en.json +30 -1
  3. package/messages/he.json +30 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/AGENTS.md.ejs +63 -16
  6. package/templates/nextjs/base/AI-GUIDE.md +17 -4
  7. package/templates/nextjs/base/CLAUDE.md.ejs +67 -20
  8. package/templates/nextjs/base/TRANSLATIONS.md +207 -207
  9. package/templates/nextjs/base/src/app/account/page.tsx +168 -122
  10. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
  11. package/templates/nextjs/base/src/app/checkout/page.tsx +1179 -1074
  12. package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
  13. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
  14. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  15. package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +422 -371
  17. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
  18. package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
  19. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
  20. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
  21. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
  22. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +218 -127
  23. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
  24. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
  25. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
  26. package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
  27. package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
  28. package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
  29. package/templates/nextjs/base/src/core/lib/region.ts +112 -0
  30. package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
  31. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
  32. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +174 -164
  33. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
  34. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +219 -176
  35. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
  37. package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
  38. package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
  39. package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
  40. package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
  41. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
  42. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
  43. package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
  44. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
  45. package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
  46. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
  47. package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
  48. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
  49. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
  50. package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
  51. package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
  52. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
  53. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +168 -158
  54. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
  55. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +229 -184
  56. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
  57. package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
  58. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
  59. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
  60. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
  61. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
  62. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
  63. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
  64. package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
  65. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
  66. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
  67. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
  68. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
  69. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
  70. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
  71. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
  72. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
  73. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
  74. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
  75. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
  76. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
  77. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
  78. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
  79. package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
  80. package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
  81. package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
  82. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
  83. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
  84. package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
  85. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
  86. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
  87. package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
  88. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
@@ -1,184 +1,229 @@
1
- 'use client';
2
-
3
- import { Link } from '@/core/lib/navigation';
4
- import { useCartPage } from '@/core/hooks/use-cart-page';
5
- import { CartItem } from '@/ui/cart/cart-item';
6
- import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
7
- import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
8
- import { CartSummary } from '@/ui/cart/cart-summary';
9
- import { CouponInput } from '@/ui/cart/coupon-input';
10
- import { CartNudges } from '@/ui/cart/cart-nudges';
11
- import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
12
- import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
13
- import { CartRecommendationSection } from '@/ui/product/recommendation-section';
14
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
15
- import { useTranslations } from '@/core/lib/translations';
16
- import { IconBag, IconArrowEnd, IconShield } from '@/ui/shared/icons';
17
-
18
- export function CartView() {
19
- const t = useTranslations('cart');
20
- const tc = useTranslations('common');
21
- const tr = useTranslations('reservation');
22
- const {
23
- cart,
24
- cartLoading,
25
- refreshCart,
26
- itemCount,
27
- cartRecs,
28
- upgrades,
29
- bundles,
30
- reservationExpired,
31
- unavailableItems,
32
- canProceedToCheckout,
33
- onReservationExpired,
34
- } = useCartPage();
35
-
36
- if (cartLoading) {
37
- return <LoadingSpinner size="lg" className="min-h-[60vh]" />;
38
- }
39
-
40
- // Empty cart state — designed, never a bare sentence in blank space.
41
- if (!cart || cart.items.length === 0) {
42
- return (
43
- <section className="container-narrow flex min-h-[60vh] items-center justify-center py-16">
44
- <div className="card flex w-full max-w-md flex-col items-center gap-3 px-8 py-14 text-center">
45
- <span className="flex h-16 w-16 items-center justify-center rounded-full bg-secondary text-primary">
46
- <IconBag size={32} />
47
- </span>
48
- <h1 className="text-2xl sm:text-3xl">{t('emptyTitle')}</h1>
49
- <p className="text-muted-foreground">{t('emptySubtitle')}</p>
50
- <Link href="/products" className="btn-primary btn-md mt-3">
51
- {tc('continueShopping')}
52
- <IconArrowEnd size={18} className="rtl-flip" />
53
- </Link>
54
- </div>
55
- </section>
56
- );
57
- }
58
-
59
- return (
60
- <section className="container-narrow py-8 lg:py-12">
61
- <h1 className="text-3xl sm:text-4xl">
62
- {t('title')}{' '}
63
- <span className="font-sans text-base font-normal text-muted-foreground">
64
- ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
65
- </span>
66
- </h1>
67
-
68
- {/* Reservation countdown. onExpire refreshes the cart and closes the
69
- checkout gate below. Passing it is what makes expiry mean anything. */}
70
- {cart.reservation?.hasReservation && (
71
- <ReservationCountdown
72
- reservation={cart.reservation}
73
- onExpire={onReservationExpired}
74
- className="mt-4"
75
- />
76
- )}
77
-
78
- <div className="mt-6 grid grid-cols-1 items-start gap-8 lg:grid-cols-3 lg:gap-10">
79
- {/* Cart Items */}
80
- <div className="lg:col-span-2">
81
- {/* Nudges */}
82
- {cart.nudges && cart.nudges.length > 0 && (
83
- <CartNudges nudges={cart.nudges} className="mb-4" />
84
- )}
85
-
86
- {/* Cart items */}
87
- <ul className="card divide-y overflow-hidden">
88
- {cart.items.map((item) => (
89
- <li key={item.id} className="p-4 sm:p-5">
90
- <CartItem item={item} onUpdate={refreshCart} />
91
- {upgrades?.upgrades?.[item.productId] && (
92
- <CartUpgradeBanner
93
- suggestion={upgrades.upgrades[item.productId]}
94
- cartItem={item}
95
- onUpgrade={refreshCart}
96
- className="mt-4"
97
- />
98
- )}
99
- </li>
100
- ))}
101
- </ul>
102
-
103
- {/* Bundle offers */}
104
- {bundles?.bundles && bundles.bundles.length > 0 && (
105
- <section className="mt-6">
106
- <h3 className="mb-3 text-xl">{t('bundleOffers')}</h3>
107
- <div className="space-y-4">
108
- {bundles.bundles.map((offer) => (
109
- <CartBundleOfferCard
110
- key={offer.id}
111
- offer={offer}
112
- cartId={cart.id}
113
- onAdd={refreshCart}
114
- />
115
- ))}
116
- </div>
117
- </section>
118
- )}
119
-
120
- {/* Coupon input */}
121
- <div className="mt-6">
122
- <p className="mb-2 text-sm font-semibold text-foreground">{t('couponTitle')}</p>
123
- <CouponInput cart={cart} onUpdate={refreshCart} />
124
- </div>
125
- </div>
126
-
127
- {/* Summary sidebar */}
128
- <aside className="lg:sticky lg:top-24">
129
- <div className="card space-y-5 p-5 sm:p-6">
130
- <FreeShippingBar />
131
- <CartSummary />
132
-
133
- {/* Proceed to checkout. When the gate is closed this must be a
134
- real disabled control, never a styled-down link: an anchor
135
- ignores `disabled` and would still navigate. */}
136
- {canProceedToCheckout ? (
137
- <Link href="/checkout" className="btn-primary btn-lg w-full">
138
- {t('proceedToCheckout')}
139
- <IconArrowEnd size={20} className="rtl-flip" />
140
- </Link>
141
- ) : (
142
- <div>
143
- <button type="button" disabled className="btn-primary btn-lg w-full opacity-50">
144
- {t('proceedToCheckout')}
145
- <IconArrowEnd size={20} className="rtl-flip" />
146
- </button>
147
- <p className="mt-2 text-center text-xs text-destructive">
148
- {unavailableItems.length > 0
149
- ? t('unavailableItemsHint')
150
- : reservationExpired
151
- ? tr('expiredHint')
152
- : null}
153
- </p>
154
- </div>
155
- )}
156
-
157
- <p className="flex items-center justify-center gap-1.5 text-xs text-muted-foreground">
158
- <IconShield size={16} />
159
- {t('secureCheckoutNote')}
160
- </p>
161
- </div>
162
-
163
- <p className="mt-4 text-center">
164
- <Link
165
- href="/products"
166
- className="text-sm font-medium text-primary underline-offset-2 hover:underline"
167
- >
168
- {tc('continueShopping')}
169
- </Link>
170
- </p>
171
- </aside>
172
- </div>
173
-
174
- {/* Cross-sell recommendations */}
175
- {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
176
- <CartRecommendationSection
177
- title={t('youMightAlsoNeed')}
178
- items={cartRecs.recommendations}
179
- className="mt-12"
180
- />
181
- )}
182
- </section>
183
- );
184
- }
1
+ 'use client';
2
+
3
+ import { Link } from '@/core/lib/navigation';
4
+ import { useCartPage } from '@/core/hooks/use-cart-page';
5
+ import { CartItem } from '@/ui/cart/cart-item';
6
+ import { CartUpgradeBanner } from '@/ui/cart/cart-upgrade-banner';
7
+ import { CartBundleOfferCard } from '@/ui/cart/cart-bundle-offer';
8
+ import { CartSummary } from '@/ui/cart/cart-summary';
9
+ import { CouponInput } from '@/ui/cart/coupon-input';
10
+ import { CartNudges } from '@/ui/cart/cart-nudges';
11
+ import { FreeShippingBar } from '@/ui/cart/free-shipping-bar';
12
+ import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
13
+ import { CartRecommendationSection } from '@/ui/product/recommendation-section';
14
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
15
+ import { useTranslations } from '@/core/lib/translations';
16
+ import { IconBag, IconArrowEnd, IconShield } from '@/ui/shared/icons';
17
+
18
+ export function CartView() {
19
+ const t = useTranslations('cart');
20
+ const tc = useTranslations('common');
21
+ const tr = useTranslations('reservation');
22
+ const {
23
+ cart,
24
+ cartLoading,
25
+ refreshCart,
26
+ itemCount,
27
+ cartRecs,
28
+ upgrades,
29
+ bundles,
30
+ reservationExpired,
31
+ unavailableItems,
32
+ selectedItemIds,
33
+ selectedItems,
34
+ toggleItemSelected,
35
+ allSelected,
36
+ setAllSelected,
37
+ checkoutHref,
38
+ canProceedToCheckout,
39
+ onReservationExpired,
40
+ } = useCartPage();
41
+
42
+ if (cartLoading) {
43
+ return <LoadingSpinner size="lg" className="min-h-[60vh]" />;
44
+ }
45
+
46
+ // Empty cart state — designed, never a bare sentence in blank space.
47
+ if (!cart || cart.items.length === 0) {
48
+ return (
49
+ <section className="container-narrow flex min-h-[60vh] items-center justify-center py-16">
50
+ <div className="card flex w-full max-w-md flex-col items-center gap-3 px-8 py-14 text-center">
51
+ <span className="bg-secondary text-primary flex h-16 w-16 items-center justify-center rounded-full">
52
+ <IconBag size={32} />
53
+ </span>
54
+ <h1 className="text-2xl sm:text-3xl">{t('emptyTitle')}</h1>
55
+ <p className="text-muted-foreground">{t('emptySubtitle')}</p>
56
+ <Link href="/products" className="btn-primary btn-md mt-3">
57
+ {tc('continueShopping')}
58
+ <IconArrowEnd size={18} className="rtl-flip" />
59
+ </Link>
60
+ </div>
61
+ </section>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <section className="container-narrow py-8 lg:py-12">
67
+ <h1 className="text-3xl sm:text-4xl">
68
+ {t('title')}{' '}
69
+ <span className="text-muted-foreground font-sans text-base font-normal">
70
+ ({itemCount} {itemCount === 1 ? tc('item') : tc('items')})
71
+ </span>
72
+ </h1>
73
+
74
+ {/* Reservation countdown. onExpire refreshes the cart and closes the
75
+ checkout gate below. Passing it is what makes expiry mean anything. */}
76
+ {cart.reservation?.hasReservation && (
77
+ <ReservationCountdown
78
+ reservation={cart.reservation}
79
+ onExpire={onReservationExpired}
80
+ className="mt-4"
81
+ />
82
+ )}
83
+
84
+ <div className="mt-6 grid grid-cols-1 items-start gap-8 lg:grid-cols-3 lg:gap-10">
85
+ {/* Cart Items */}
86
+ <div className="lg:col-span-2">
87
+ {/* Nudges */}
88
+ {cart.nudges && cart.nudges.length > 0 && (
89
+ <CartNudges nudges={cart.nudges} className="mb-4" />
90
+ )}
91
+
92
+ {/* Select-all. Partial checkout: only the ticked lines are sent to
93
+ createCheckout, and whatever stays unticked is still in the cart
94
+ (and still for sale) after payment. */}
95
+ <div className="mb-3 flex items-center gap-3 px-1">
96
+ <input
97
+ id="cart-select-all"
98
+ type="checkbox"
99
+ checked={allSelected}
100
+ onChange={(event) => setAllSelected(event.target.checked)}
101
+ className="accent-primary h-4 w-4 cursor-pointer"
102
+ />
103
+ <label htmlFor="cart-select-all" className="text-foreground cursor-pointer text-sm">
104
+ {tc('all')}
105
+ </label>
106
+ </div>
107
+
108
+ {/* Cart items */}
109
+ <ul className="card divide-y overflow-hidden">
110
+ {cart.items.map((item) => (
111
+ <li key={item.id} className="flex items-start gap-3 p-4 sm:p-5">
112
+ <input
113
+ type="checkbox"
114
+ checked={selectedItemIds.includes(item.id)}
115
+ onChange={() => toggleItemSelected(item.id)}
116
+ aria-label={item.product.name}
117
+ className="accent-primary mt-8 h-4 w-4 shrink-0 cursor-pointer"
118
+ />
119
+ <div className="min-w-0 flex-1">
120
+ <CartItem item={item} onUpdate={refreshCart} />
121
+ {upgrades?.upgrades?.[item.productId] && (
122
+ <CartUpgradeBanner
123
+ suggestion={upgrades.upgrades[item.productId]}
124
+ cartItem={item}
125
+ onUpgrade={refreshCart}
126
+ className="mt-4"
127
+ />
128
+ )}
129
+ </div>
130
+ </li>
131
+ ))}
132
+ </ul>
133
+
134
+ {/* Bundle offers */}
135
+ {bundles?.bundles && bundles.bundles.length > 0 && (
136
+ <section className="mt-6">
137
+ <h3 className="mb-3 text-xl">{t('bundleOffers')}</h3>
138
+ <div className="space-y-4">
139
+ {bundles.bundles.map((offer) => (
140
+ <CartBundleOfferCard
141
+ key={offer.id}
142
+ offer={offer}
143
+ cartId={cart.id}
144
+ onAdd={refreshCart}
145
+ />
146
+ ))}
147
+ </div>
148
+ </section>
149
+ )}
150
+
151
+ {/* Coupon input */}
152
+ <div className="mt-6">
153
+ <p className="text-foreground mb-2 text-sm font-semibold">{t('couponTitle')}</p>
154
+ <CouponInput cart={cart} onUpdate={refreshCart} />
155
+ </div>
156
+ </div>
157
+
158
+ {/* Summary sidebar */}
159
+ <aside className="lg:sticky lg:top-24">
160
+ <div className="card space-y-5 p-5 sm:p-6">
161
+ <FreeShippingBar />
162
+ {/* Totals are for the WHOLE cart. A partial selection is priced
163
+ authoritatively on the checkout page, which is the only place
164
+ the server has scoped the discounts and tax to the subset. */}
165
+ <CartSummary />
166
+
167
+ {/* Shown once the shopper unticks anything. At zero selected this
168
+ is what explains the disabled button below — a count, so it
169
+ needs no copy of its own. */}
170
+ {!allSelected && (
171
+ <p className="text-muted-foreground text-center text-xs">
172
+ {selectedItems.length} {selectedItems.length === 1 ? tc('item') : tc('items')}
173
+ </p>
174
+ )}
175
+
176
+ {/* Proceed to checkout. When the gate is closed this must be a
177
+ real disabled control, never a styled-down link: an anchor
178
+ ignores `disabled` and would still navigate. */}
179
+ {canProceedToCheckout ? (
180
+ /* `checkoutHref` carries `?items=` only for a strict subset; a
181
+ full cart links to a bare /checkout exactly as before. */
182
+ <Link href={checkoutHref} className="btn-primary btn-lg w-full">
183
+ {t('proceedToCheckout')}
184
+ <IconArrowEnd size={20} className="rtl-flip" />
185
+ </Link>
186
+ ) : (
187
+ <div>
188
+ <button type="button" disabled className="btn-primary btn-lg w-full opacity-50">
189
+ {t('proceedToCheckout')}
190
+ <IconArrowEnd size={20} className="rtl-flip" />
191
+ </button>
192
+ <p className="text-destructive mt-2 text-center text-xs">
193
+ {unavailableItems.length > 0
194
+ ? t('unavailableItemsHint')
195
+ : reservationExpired
196
+ ? tr('expiredHint')
197
+ : null}
198
+ </p>
199
+ </div>
200
+ )}
201
+
202
+ <p className="text-muted-foreground flex items-center justify-center gap-1.5 text-xs">
203
+ <IconShield size={16} />
204
+ {t('secureCheckoutNote')}
205
+ </p>
206
+ </div>
207
+
208
+ <p className="mt-4 text-center">
209
+ <Link
210
+ href="/products"
211
+ className="text-primary text-sm font-medium underline-offset-2 hover:underline"
212
+ >
213
+ {tc('continueShopping')}
214
+ </Link>
215
+ </p>
216
+ </aside>
217
+ </div>
218
+
219
+ {/* Cross-sell recommendations */}
220
+ {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
221
+ <CartRecommendationSection
222
+ title={t('youMightAlsoNeed')}
223
+ items={cartRecs.recommendations}
224
+ className="mt-12"
225
+ />
226
+ )}
227
+ </section>
228
+ );
229
+ }