create-brainerce-store 1.79.0 → 1.81.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/dist/index.js +168 -10
- package/messages/en.json +13 -4
- package/messages/he.json +13 -4
- package/package.json +10 -1
- package/templates/nextjs/base/.eslintrc.json +2 -0
- package/templates/nextjs/base/AI-GUIDE.md +32 -2
- package/templates/nextjs/base/package.json.ejs +53 -52
- package/templates/nextjs/base/scripts/connect.mjs +149 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +18 -0
- package/templates/nextjs/base/src/components/account/profile-section.tsx +7 -1
- package/templates/nextjs/base/src/components/auth/register-form.tsx +18 -1
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +1 -1
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +22 -0
- package/templates/nextjs/base/src/core/lib/add-to-cart-error.ts +49 -0
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +18 -0
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +49 -0
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +96 -2
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +59 -12
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +17 -0
- package/templates/nextjs/base/src/ui/product/product-card.tsx +17 -0
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +13 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +18 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +49 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +101 -5
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +17 -0
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +17 -0
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +13 -0
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +16 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +45 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +95 -2
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +50 -8
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +15 -0
- package/templates/nextjs/ui-canvas/product/product-card.tsx +15 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +13 -0
|
@@ -7,6 +7,7 @@ import { formatPrice } from 'brainerce';
|
|
|
7
7
|
import { useCart, useStoreInfo } from '@/core/providers/store-provider';
|
|
8
8
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
9
9
|
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
+
import { toAddToCartError, type AddToCartError } from '@/core/lib/add-to-cart-error';
|
|
10
11
|
import { cn } from '@/core/lib/utils';
|
|
11
12
|
|
|
12
13
|
interface FrequentlyBoughtTogetherProps {
|
|
@@ -90,6 +91,7 @@ export function FrequentlyBoughtTogether({
|
|
|
90
91
|
|
|
91
92
|
const [selected, setSelected] = useState<Set<string>>(() => new Set(crossSells.map((i) => i.id)));
|
|
92
93
|
const [adding, setAdding] = useState(false);
|
|
94
|
+
const [addError, setAddError] = useState<AddToCartError | null>(null);
|
|
93
95
|
|
|
94
96
|
if (!storeInfo?.upsell?.frequentlyBoughtTogetherEnabled) return null;
|
|
95
97
|
if (crossSells.length === 0) return null;
|
|
@@ -120,6 +122,7 @@ export function FrequentlyBoughtTogether({
|
|
|
120
122
|
|
|
121
123
|
async function handleAddAll() {
|
|
122
124
|
if (adding || selected.size === 0) return;
|
|
125
|
+
setAddError(null);
|
|
123
126
|
try {
|
|
124
127
|
setAdding(true);
|
|
125
128
|
const { getClient } = await import('@/core/lib/brainerce');
|
|
@@ -130,6 +133,10 @@ export function FrequentlyBoughtTogether({
|
|
|
130
133
|
}
|
|
131
134
|
await refreshCart();
|
|
132
135
|
} catch (err) {
|
|
136
|
+
// The loop is not transactional: if the cart hits its 50-line cap
|
|
137
|
+
// part-way through, the earlier items ARE in the cart and the rest are
|
|
138
|
+
// not. Saying so beats a silent stop the shopper reads as a dead button.
|
|
139
|
+
setAddError(toAddToCartError(err));
|
|
133
140
|
console.error('Failed to add items to cart:', err);
|
|
134
141
|
} finally {
|
|
135
142
|
setAdding(false);
|
|
@@ -177,6 +184,14 @@ export function FrequentlyBoughtTogether({
|
|
|
177
184
|
{adding ? t('addingAll') : t('addSelectedToCart')}
|
|
178
185
|
</button>
|
|
179
186
|
</div>
|
|
187
|
+
|
|
188
|
+
{/*
|
|
189
|
+
Why the add was refused. Some items may already be in the cart:
|
|
190
|
+
the loop above adds one at a time and stops at the first refusal.
|
|
191
|
+
*/}
|
|
192
|
+
{addError && (
|
|
193
|
+
<p role="alert">{addError === 'CART_FULL' ? t('cartFull') : t('addToCartFailed')}</p>
|
|
194
|
+
)}
|
|
180
195
|
</section>
|
|
181
196
|
);
|
|
182
197
|
}
|
|
@@ -14,6 +14,7 @@ import { DiscountBadge } from '@/ui/product/discount-badge';
|
|
|
14
14
|
import { useCart } from '@/core/providers/store-provider';
|
|
15
15
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
16
16
|
import { trackAddToCart } from '@/core/lib/tracking';
|
|
17
|
+
import { toAddToCartError, type AddToCartError } from '@/core/lib/add-to-cart-error';
|
|
17
18
|
import { cn } from '@/core/lib/utils';
|
|
18
19
|
|
|
19
20
|
interface ProductCardProps {
|
|
@@ -58,6 +59,7 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
58
59
|
|
|
59
60
|
const [adding, setAdding] = useState(false);
|
|
60
61
|
const [added, setAdded] = useState(false);
|
|
62
|
+
const [addError, setAddError] = useState<AddToCartError | null>(null);
|
|
61
63
|
|
|
62
64
|
// ⛔ A KIT carries NO `inventory` object — its stock is on `kitAvailable`
|
|
63
65
|
// (`null` = unlimited, `0` = not sellable). Reading `inventory` here left a
|
|
@@ -79,6 +81,7 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
if (adding || !canPurchase) return;
|
|
84
|
+
setAddError(null);
|
|
82
85
|
|
|
83
86
|
try {
|
|
84
87
|
setAdding(true);
|
|
@@ -94,6 +97,10 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
94
97
|
setAdded(true);
|
|
95
98
|
setTimeout(() => setAdded(false), 2000);
|
|
96
99
|
} catch (err) {
|
|
100
|
+
// Quick-add used to fail in silence: the button spun, reset, and the
|
|
101
|
+
// item never appeared. A cart at the 50-line cap is the commonest
|
|
102
|
+
// cause and the shopper can fix it, so say so on the card.
|
|
103
|
+
setAddError(toAddToCartError(err));
|
|
97
104
|
console.error('Failed to add to cart:', err);
|
|
98
105
|
} finally {
|
|
99
106
|
setAdding(false);
|
|
@@ -169,6 +176,14 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
169
176
|
{added ? tp('addedToCart') : isVariable ? tProd('selectOptions') : tp('addToCart')}
|
|
170
177
|
</button>
|
|
171
178
|
)}
|
|
179
|
+
|
|
180
|
+
{/*
|
|
181
|
+
Quick-add refusal. The card has no other error surface, and without
|
|
182
|
+
this the shopper taps, the button resets, and nothing appears.
|
|
183
|
+
*/}
|
|
184
|
+
{addError && (
|
|
185
|
+
<p role="alert">{addError === 'CART_FULL' ? tp('cartFull') : tp('addToCartFailed')}</p>
|
|
186
|
+
)}
|
|
172
187
|
</article>
|
|
173
188
|
);
|
|
174
189
|
}
|
|
@@ -107,6 +107,7 @@ export function ProductClientSection({
|
|
|
107
107
|
setQuantity,
|
|
108
108
|
addingToCart,
|
|
109
109
|
addedMessage,
|
|
110
|
+
addToCartError,
|
|
110
111
|
handleAddToCart,
|
|
111
112
|
customizationFields,
|
|
112
113
|
customizationValues,
|
|
@@ -347,6 +348,18 @@ export function ProductClientSection({
|
|
|
347
348
|
</button>
|
|
348
349
|
</div>
|
|
349
350
|
|
|
351
|
+
{/*
|
|
352
|
+
Why the add was refused. Deliberately OUTSIDE the modifier block
|
|
353
|
+
above: that block only renders when the product HAS modifier
|
|
354
|
+
groups, so a refusal shown there would be invisible on almost
|
|
355
|
+
every product and the shopper would just watch the button reset.
|
|
356
|
+
*/}
|
|
357
|
+
{addToCartError && (
|
|
358
|
+
<p role="alert">
|
|
359
|
+
{addToCartError === 'CART_FULL' ? t('cartFull') : t('addToCartFailed')}
|
|
360
|
+
</p>
|
|
361
|
+
)}
|
|
362
|
+
|
|
350
363
|
{/*
|
|
351
364
|
Sold out is not the end of the page. `canOfferStockAlert` is the whole
|
|
352
365
|
gate — it also covers the merchant's switch and backorderable items,
|