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.
- package/README.md +130 -92
- package/dist/index.js +39 -17
- package/messages/en.json +3 -0
- package/messages/he.json +3 -0
- package/package.json +1 -1
- package/templates/nextjs/base/.env.local.ejs +10 -1
- package/templates/nextjs/base/AGENTS.md.ejs +229 -207
- package/templates/nextjs/base/AI-GUIDE.md +4 -0
- package/templates/nextjs/base/CLAUDE.md.ejs +240 -218
- package/templates/nextjs/base/scripts/connect.mjs +115 -18
- package/templates/nextjs/base/scripts/env-file.mjs +93 -0
- package/templates/nextjs/base/scripts/fetch-store-info.mjs +109 -83
- package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +2 -1
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +3 -2
- package/templates/nextjs/base/src/app/layout.tsx.ejs +50 -21
- package/templates/nextjs/base/src/app/opengraph-image.tsx +5 -1
- package/templates/nextjs/base/src/app/reset-password/page.tsx +25 -138
- package/templates/nextjs/base/src/app/reset-password/reset-password-form.tsx +138 -0
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +174 -137
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +82 -11
- package/templates/nextjs/base/src/core/lib/store-name.ts.ejs +56 -0
- package/templates/nextjs/base/src/core/lib/use-store-name.ts +18 -0
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +27 -27
- package/templates/nextjs/base/src/ui/layout/header-search.tsx +2 -2
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +160 -160
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +2 -2
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +374 -345
- package/templates/nextjs/designs/atelier/globals.css +408 -402
- package/templates/nextjs/designs/atelier/messages-patch/en.json +91 -96
- package/templates/nextjs/designs/atelier/messages-patch/he.json +91 -96
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +131 -128
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +2 -1
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +222 -219
- package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +178 -151
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +61 -31
- package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +2 -2
- package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +161 -155
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +232 -227
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +271 -268
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +558 -553
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +113 -110
- 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
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
const [
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// `
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `
|
|
104
|
-
* `
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
//
|
|
125
|
-
// `
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
// product
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
{upgrading
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|