create-brainerce-store 1.84.0 → 1.85.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 (42) hide show
  1. package/README.md +130 -92
  2. package/dist/index.js +39 -17
  3. package/messages/en.json +3 -0
  4. package/messages/he.json +3 -0
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.env.local.ejs +10 -1
  7. package/templates/nextjs/base/AGENTS.md.ejs +229 -207
  8. package/templates/nextjs/base/AI-GUIDE.md +4 -0
  9. package/templates/nextjs/base/CLAUDE.md.ejs +240 -218
  10. package/templates/nextjs/base/scripts/connect.mjs +115 -18
  11. package/templates/nextjs/base/scripts/env-file.mjs +93 -0
  12. package/templates/nextjs/base/scripts/fetch-store-info.mjs +109 -83
  13. package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +2 -1
  14. package/templates/nextjs/base/src/app/forgot-password/page.tsx +3 -2
  15. package/templates/nextjs/base/src/app/layout.tsx.ejs +50 -21
  16. package/templates/nextjs/base/src/app/opengraph-image.tsx +5 -1
  17. package/templates/nextjs/base/src/app/reset-password/page.tsx +25 -138
  18. package/templates/nextjs/base/src/app/reset-password/reset-password-form.tsx +138 -0
  19. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +174 -137
  20. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +82 -11
  21. package/templates/nextjs/base/src/core/lib/store-name.ts.ejs +56 -0
  22. package/templates/nextjs/base/src/core/lib/use-store-name.ts +18 -0
  23. package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
  24. package/templates/nextjs/base/src/ui/layout/header-search.tsx +2 -2
  25. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +160 -160
  26. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +2 -2
  27. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +374 -345
  28. package/templates/nextjs/designs/atelier/globals.css +408 -402
  29. package/templates/nextjs/designs/atelier/messages-patch/en.json +91 -96
  30. package/templates/nextjs/designs/atelier/messages-patch/he.json +91 -96
  31. package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +131 -128
  32. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +2 -1
  33. package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +222 -219
  34. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +178 -151
  35. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +61 -31
  36. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +2 -2
  37. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +161 -155
  38. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +232 -227
  39. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +271 -268
  40. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +558 -553
  41. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +113 -110
  42. package/templates/nextjs/designs/atelier/ui/shared/no-photo-tile.tsx +73 -0
@@ -1,219 +1,222 @@
1
- 'use client';
2
-
3
- import { useState, useEffect } from 'react';
4
- import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
- import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
6
- import { formatPrice } from 'brainerce';
7
- import { getClient } from '@/core/lib/brainerce';
8
- import { useCurrency } from '@/core/lib/use-currency';
9
- import { useTranslations } from '@/core/lib/translations';
10
- import { toAddToCartError, type AddToCartError } from '@/core/lib/add-to-cart-error';
11
- import { cn } from '@/core/lib/utils';
12
- import { IconX } from '@/ui/shared/icons';
13
-
14
- interface CartUpgradeBannerProps {
15
- suggestion: CartUpgradeSuggestion;
16
- cartItem: CartItemType;
17
- onUpgrade: () => void;
18
- className?: string;
19
- }
20
-
21
- /**
22
- * Per-line upgrade suggestion under a cart item ("upgrade to X for +Y");
23
- * the suggestion comes from useCartPage().upgrades keyed by productId.
24
- * Keeps the session-scoped dismiss and the swap logic.
25
- */
26
- export function CartUpgradeBanner({
27
- suggestion,
28
- cartItem,
29
- onUpgrade,
30
- className,
31
- }: CartUpgradeBannerProps) {
32
- const t = useTranslations('cart');
33
- const tp = useTranslations('productDetail');
34
- const currency = useCurrency();
35
- const [upgrading, setUpgrading] = useState(false);
36
- const [dismissed, setDismissed] = useState(false);
37
- // True once the upgraded product is in the cart. A retry after a failed
38
- // removal must retry the REMOVAL only, or a second press buys two of them.
39
- const [upgradeAdded, setUpgradeAdded] = useState(false);
40
- // `'ORIGINAL_NOT_REMOVED'` is kept local instead of being added to the shared
41
- // `AddToCartError`: every other consumer of that type branches on
42
- // `=== 'CART_FULL'` and would silently render "we could not add this" for a
43
- // case where the add actually succeeded.
44
- const [upgradeError, setUpgradeError] = useState<AddToCartError | 'ORIGINAL_NOT_REMOVED' | null>(
45
- null
46
- );
47
-
48
- const storageKey = `dismissed_upgrade_${suggestion.sourceProductId}`;
49
-
50
- useEffect(() => {
51
- try {
52
- if (sessionStorage.getItem(storageKey)) {
53
- setDismissed(true);
54
- }
55
- } catch {
56
- /* ignore */
57
- }
58
- }, [storageKey]);
59
-
60
- if (dismissed) return null;
61
-
62
- const target = suggestion.targetProduct;
63
-
64
- // A one-click upgrade cannot choose a variation for the shopper. When the
65
- // target is VARIABLE with no pinned variant the backend says so
66
- // (`requiresVariantSelection`), and adding it without a `variantId` is
67
- // rejected outright, so offering the button here would only ever produce an
68
- // error. Say nothing instead of promising something that cannot work; the
69
- // shopper can still reach the product from the catalogue and pick a variation
70
- // there. If you want to support this properly, add a variation picker to the
71
- // banner and pass the chosen `variantId` to `smartAddToCart` below.
72
- if (target.requiresVariantSelection && !target.pinnedVariant?.id) return null;
73
- const firstImage = target.images?.[0];
74
- const imageUrl = firstImage
75
- ? typeof firstImage === 'string'
76
- ? firstImage
77
- : firstImage.url
78
- : null;
79
- const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
80
-
81
- function handleDismiss() {
82
- try {
83
- sessionStorage.setItem(storageKey, '1');
84
- } catch {
85
- /* ignore */
86
- }
87
- setDismissed(true);
88
- }
89
-
90
- /**
91
- * Swap the cart line for the upgraded product: ADD FIRST, THEN REMOVE.
92
- *
93
- * The order is load-bearing, not incidental. This used to remove and then
94
- * add, so any failure of the add (a network blip, the target selling out, or
95
- * the 50-line cart cap answering 400) left the shopper with NEITHER product
96
- * and nothing on screen: the remove had committed, the add had not, and the
97
- * whole thing went to the console. Adding first means a failed add leaves the
98
- * cart exactly as it was.
99
- *
100
- * The other candidate, remove-then-add with a compensating re-add, was
101
- * rejected because the compensation is not a restore. A cart line carries
102
- * `modifiers`, `customizations`, `notes` and `parentCartItemId`, and
103
- * `smartAddToCart` accepts none of those back in that shape (it takes
104
- * `selections`), so re-adding would silently strip the shopper's choices even
105
- * when it SUCCEEDS, and orphan a nested-combo child line. The SDK has no swap
106
- * primitive to sidestep the choice with (`smartRemoveFromCart` is only
107
- * `smartUpdateCartItem(productId, 0, variantId)`), so these two orderings are
108
- * the whole option space.
109
- *
110
- * The accepted cost: the cart is briefly one line longer, so a cart sitting
111
- * at the 50-line cap refuses a swap that is net-zero. The shopper is told the
112
- * cart is full and the cart is untouched, which is a far better outcome than
113
- * losing the line. Do NOT add a "fall back to remove-then-add when the cart is
114
- * full" path; that hands the loss window back for a rare edge.
115
- */
116
- async function handleUpgrade() {
117
- if (upgrading) return;
118
- setUpgradeError(null);
119
- try {
120
- setUpgrading(true);
121
- const client = getClient();
122
- if (!upgradeAdded) {
123
- // A VARIABLE target needs a variant. The backend already tells us which
124
- // case we are in (`pinnedVariant` when the slot pins one,
125
- // `requiresVariantSelection` when the shopper must choose), but this
126
- // banner used to send neither -- and `CartService.addItem` rejects a
127
- // VARIABLE product with no `variantId`, so an upgrade to a variable
128
- // product failed EVERY time, not occasionally. Under the old
129
- // remove-then-add ordering that silently destroyed the original line.
130
- // The unpinned case is filtered out before render, so by here either a
131
- // pin exists or the target is SIMPLE.
132
- await client.smartAddToCart({
133
- productId: target.id,
134
- quantity: cartItem.quantity,
135
- ...(target.pinnedVariant?.id ? { variantId: target.pinnedVariant.id } : {}),
136
- });
137
- setUpgradeAdded(true);
138
- }
139
- try {
140
- await client.smartRemoveFromCart(cartItem.productId, cartItem.variantId || undefined);
141
- } catch (err) {
142
- // The upgrade IS in the cart and only the old line survived, so nothing
143
- // was lost and this is not a failed upgrade. Deliberately does NOT call
144
- // onUpgrade(): the backend drops an upgrade suggestion once its target
145
- // is in the cart, so refreshing here unmounts this banner and takes the
146
- // message with it. Pressing Upgrade again retries the removal alone.
147
- setUpgradeError('ORIGINAL_NOT_REMOVED');
148
- console.error('Upgraded, but could not remove the original cart item:', err);
149
- return;
150
- }
151
- onUpgrade();
152
- } catch (err) {
153
- // The add failed, so the cart is untouched. Say so: logging alone left the
154
- // button spinning back to idle with nothing changed and no explanation.
155
- setUpgradeError(toAddToCartError(err));
156
- console.error('Failed to upgrade cart item:', err);
157
- } finally {
158
- setUpgrading(false);
159
- }
160
- }
161
-
162
- return (
163
- <div
164
- className={cn(
165
- 'flex items-center gap-3 rounded-lg border border-primary/25 bg-secondary/60 p-3',
166
- className
167
- )}
168
- >
169
- {/* Product image — `relative` + fixed box are layout-critical for next/image fill */}
170
- <span className="relative block h-12 w-12 shrink-0 overflow-hidden rounded-lg border bg-background">
171
- {imageUrl ? (
172
- <Image src={imageUrl} alt={target.name} fill sizes="48px" className="object-cover" />
173
- ) : null}
174
- </span>
175
-
176
- {/* Text */}
177
- <div className="min-w-0 flex-1">
178
- <p className="text-sm text-foreground">
179
- {t('upgradeFor', { name: target.name, amount: formattedDelta })}
180
- </p>
181
-
182
- {/*
183
- Why the swap did not complete. Without this the button resets, the
184
- cart looks unchanged, and the shopper has no idea whether anything
185
- happened.
186
- */}
187
- {upgradeError && (
188
- <p role="alert" className="text-destructive mt-1 text-xs font-medium">
189
- {upgradeError === 'ORIGINAL_NOT_REMOVED'
190
- ? t('upgradeOriginalNotRemoved')
191
- : upgradeError === 'CART_FULL'
192
- ? tp('cartFull')
193
- : tp('addToCartFailed')}
194
- </p>
195
- )}
196
- </div>
197
-
198
- {/* Upgrade button */}
199
- <button
200
- type="button"
201
- onClick={handleUpgrade}
202
- disabled={upgrading}
203
- className="btn-primary btn-sm shrink-0"
204
- >
205
- {upgrading ? t('upgrading') : t('upgrade')}
206
- </button>
207
-
208
- {/* Dismiss button */}
209
- <button
210
- type="button"
211
- onClick={handleDismiss}
212
- aria-label={t('dismissUpgrade')}
213
- className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
214
- >
215
- <IconX size={16} />
216
- </button>
217
- </div>
218
- );
219
- }
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
5
+ import { NoPhotoTile } from '@/ui/shared/no-photo-tile';
6
+ import type { CartUpgradeSuggestion, CartItem as CartItemType } from 'brainerce';
7
+ import { formatPrice } from 'brainerce';
8
+ import { getClient } from '@/core/lib/brainerce';
9
+ import { useCurrency } from '@/core/lib/use-currency';
10
+ import { useTranslations } from '@/core/lib/translations';
11
+ import { toAddToCartError, type AddToCartError } from '@/core/lib/add-to-cart-error';
12
+ import { cn } from '@/core/lib/utils';
13
+ import { IconX } from '@/ui/shared/icons';
14
+
15
+ interface CartUpgradeBannerProps {
16
+ suggestion: CartUpgradeSuggestion;
17
+ cartItem: CartItemType;
18
+ onUpgrade: () => void;
19
+ className?: string;
20
+ }
21
+
22
+ /**
23
+ * Per-line upgrade suggestion under a cart item ("upgrade to X for +Y");
24
+ * the suggestion comes from useCartPage().upgrades keyed by productId.
25
+ * Keeps the session-scoped dismiss and the swap logic.
26
+ */
27
+ export function CartUpgradeBanner({
28
+ suggestion,
29
+ cartItem,
30
+ onUpgrade,
31
+ className,
32
+ }: CartUpgradeBannerProps) {
33
+ const t = useTranslations('cart');
34
+ const tp = useTranslations('productDetail');
35
+ const currency = useCurrency();
36
+ const [upgrading, setUpgrading] = useState(false);
37
+ const [dismissed, setDismissed] = useState(false);
38
+ // True once the upgraded product is in the cart. A retry after a failed
39
+ // removal must retry the REMOVAL only, or a second press buys two of them.
40
+ const [upgradeAdded, setUpgradeAdded] = useState(false);
41
+ // `'ORIGINAL_NOT_REMOVED'` is kept local instead of being added to the shared
42
+ // `AddToCartError`: every other consumer of that type branches on
43
+ // `=== 'CART_FULL'` and would silently render "we could not add this" for a
44
+ // case where the add actually succeeded.
45
+ const [upgradeError, setUpgradeError] = useState<AddToCartError | 'ORIGINAL_NOT_REMOVED' | null>(
46
+ null
47
+ );
48
+
49
+ const storageKey = `dismissed_upgrade_${suggestion.sourceProductId}`;
50
+
51
+ useEffect(() => {
52
+ try {
53
+ if (sessionStorage.getItem(storageKey)) {
54
+ setDismissed(true);
55
+ }
56
+ } catch {
57
+ /* ignore */
58
+ }
59
+ }, [storageKey]);
60
+
61
+ if (dismissed) return null;
62
+
63
+ const target = suggestion.targetProduct;
64
+
65
+ // A one-click upgrade cannot choose a variation for the shopper. When the
66
+ // target is VARIABLE with no pinned variant the backend says so
67
+ // (`requiresVariantSelection`), and adding it without a `variantId` is
68
+ // rejected outright, so offering the button here would only ever produce an
69
+ // error. Say nothing instead of promising something that cannot work; the
70
+ // shopper can still reach the product from the catalogue and pick a variation
71
+ // there. If you want to support this properly, add a variation picker to the
72
+ // banner and pass the chosen `variantId` to `smartAddToCart` below.
73
+ if (target.requiresVariantSelection && !target.pinnedVariant?.id) return null;
74
+ const firstImage = target.images?.[0];
75
+ const imageUrl = firstImage
76
+ ? typeof firstImage === 'string'
77
+ ? firstImage
78
+ : firstImage.url
79
+ : null;
80
+ const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
81
+
82
+ function handleDismiss() {
83
+ try {
84
+ sessionStorage.setItem(storageKey, '1');
85
+ } catch {
86
+ /* ignore */
87
+ }
88
+ setDismissed(true);
89
+ }
90
+
91
+ /**
92
+ * Swap the cart line for the upgraded product: ADD FIRST, THEN REMOVE.
93
+ *
94
+ * The order is load-bearing, not incidental. This used to remove and then
95
+ * add, so any failure of the add (a network blip, the target selling out, or
96
+ * the 50-line cart cap answering 400) left the shopper with NEITHER product
97
+ * and nothing on screen: the remove had committed, the add had not, and the
98
+ * whole thing went to the console. Adding first means a failed add leaves the
99
+ * cart exactly as it was.
100
+ *
101
+ * The other candidate, remove-then-add with a compensating re-add, was
102
+ * rejected because the compensation is not a restore. A cart line carries
103
+ * `modifiers`, `customizations`, `notes` and `parentCartItemId`, and
104
+ * `smartAddToCart` accepts none of those back in that shape (it takes
105
+ * `selections`), so re-adding would silently strip the shopper's choices even
106
+ * when it SUCCEEDS, and orphan a nested-combo child line. The SDK has no swap
107
+ * primitive to sidestep the choice with (`smartRemoveFromCart` is only
108
+ * `smartUpdateCartItem(productId, 0, variantId)`), so these two orderings are
109
+ * the whole option space.
110
+ *
111
+ * The accepted cost: the cart is briefly one line longer, so a cart sitting
112
+ * at the 50-line cap refuses a swap that is net-zero. The shopper is told the
113
+ * cart is full and the cart is untouched, which is a far better outcome than
114
+ * losing the line. Do NOT add a "fall back to remove-then-add when the cart is
115
+ * full" path; that hands the loss window back for a rare edge.
116
+ */
117
+ async function handleUpgrade() {
118
+ if (upgrading) return;
119
+ setUpgradeError(null);
120
+ try {
121
+ setUpgrading(true);
122
+ const client = getClient();
123
+ if (!upgradeAdded) {
124
+ // A VARIABLE target needs a variant. The backend already tells us which
125
+ // case we are in (`pinnedVariant` when the slot pins one,
126
+ // `requiresVariantSelection` when the shopper must choose), but this
127
+ // banner used to send neither -- and `CartService.addItem` rejects a
128
+ // VARIABLE product with no `variantId`, so an upgrade to a variable
129
+ // product failed EVERY time, not occasionally. Under the old
130
+ // remove-then-add ordering that silently destroyed the original line.
131
+ // The unpinned case is filtered out before render, so by here either a
132
+ // pin exists or the target is SIMPLE.
133
+ await client.smartAddToCart({
134
+ productId: target.id,
135
+ quantity: cartItem.quantity,
136
+ ...(target.pinnedVariant?.id ? { variantId: target.pinnedVariant.id } : {}),
137
+ });
138
+ setUpgradeAdded(true);
139
+ }
140
+ try {
141
+ await client.smartRemoveFromCart(cartItem.productId, cartItem.variantId || undefined);
142
+ } catch (err) {
143
+ // The upgrade IS in the cart and only the old line survived, so nothing
144
+ // was lost and this is not a failed upgrade. Deliberately does NOT call
145
+ // onUpgrade(): the backend drops an upgrade suggestion once its target
146
+ // is in the cart, so refreshing here unmounts this banner and takes the
147
+ // message with it. Pressing Upgrade again retries the removal alone.
148
+ setUpgradeError('ORIGINAL_NOT_REMOVED');
149
+ console.error('Upgraded, but could not remove the original cart item:', err);
150
+ return;
151
+ }
152
+ onUpgrade();
153
+ } catch (err) {
154
+ // The add failed, so the cart is untouched. Say so: logging alone left the
155
+ // button spinning back to idle with nothing changed and no explanation.
156
+ setUpgradeError(toAddToCartError(err));
157
+ console.error('Failed to upgrade cart item:', err);
158
+ } finally {
159
+ setUpgrading(false);
160
+ }
161
+ }
162
+
163
+ return (
164
+ <div
165
+ className={cn(
166
+ 'flex items-center gap-3 rounded-lg border border-primary/25 bg-secondary/60 p-3',
167
+ className
168
+ )}
169
+ >
170
+ {/* Product image — `relative` + fixed box are layout-critical for next/image fill */}
171
+ <span className="relative block h-12 w-12 shrink-0 overflow-hidden rounded-lg border bg-background">
172
+ {imageUrl ? (
173
+ <Image src={imageUrl} alt={target.name} fill sizes="48px" className="object-cover" />
174
+ ) : (
175
+ <NoPhotoTile name={target.name} size="sm" />
176
+ )}
177
+ </span>
178
+
179
+ {/* Text */}
180
+ <div className="min-w-0 flex-1">
181
+ <p className="text-sm text-foreground">
182
+ {t('upgradeFor', { name: target.name, amount: formattedDelta })}
183
+ </p>
184
+
185
+ {/*
186
+ Why the swap did not complete. Without this the button resets, the
187
+ cart looks unchanged, and the shopper has no idea whether anything
188
+ happened.
189
+ */}
190
+ {upgradeError && (
191
+ <p role="alert" className="text-destructive mt-1 text-xs font-medium">
192
+ {upgradeError === 'ORIGINAL_NOT_REMOVED'
193
+ ? t('upgradeOriginalNotRemoved')
194
+ : upgradeError === 'CART_FULL'
195
+ ? tp('cartFull')
196
+ : tp('addToCartFailed')}
197
+ </p>
198
+ )}
199
+ </div>
200
+
201
+ {/* Upgrade button */}
202
+ <button
203
+ type="button"
204
+ onClick={handleUpgrade}
205
+ disabled={upgrading}
206
+ className="btn-primary btn-sm shrink-0"
207
+ >
208
+ {upgrading ? t('upgrading') : t('upgrade')}
209
+ </button>
210
+
211
+ {/* Dismiss button */}
212
+ <button
213
+ type="button"
214
+ onClick={handleDismiss}
215
+ aria-label={t('dismissUpgrade')}
216
+ className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
217
+ >
218
+ <IconX size={16} />
219
+ </button>
220
+ </div>
221
+ );
222
+ }