create-brainerce-store 1.87.0 → 1.88.1
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 +9 -2
- package/messages/en.json +10 -1
- package/messages/he.json +10 -1
- package/package.json +1 -1
- package/templates/nextjs/base/.eslintrc.json +1 -0
- package/templates/nextjs/base/AI-GUIDE.md +224 -221
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +296 -296
- package/templates/nextjs/base/src/app/checkout/page.tsx +89 -6
- package/templates/nextjs/base/src/app/products/layout.tsx.ejs +18 -0
- package/templates/nextjs/base/src/components/auth/register-form.tsx +381 -375
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +14 -8
- package/templates/nextjs/base/src/core/hooks/use-page-exists.ts +37 -0
- package/templates/nextjs/base/src/core/lib/add-to-cart-error.ts +27 -2
- package/templates/nextjs/base/src/core/lib/brainerce.server.ts +121 -121
- package/templates/nextjs/base/src/core/lib/coupon-error.ts +29 -0
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +6 -2
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +6 -4
- package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +138 -134
- package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +10 -3
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +5 -1
- package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +2 -2
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +6 -2
- package/templates/nextjs/base/src/ui/product/product-card.tsx +6 -2
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +4 -36
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +6 -2
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +6 -4
- package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +146 -142
- package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +10 -3
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +6 -2
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +6 -2
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +3 -28
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +6 -2
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +6 -4
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +123 -119
- package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +4 -3
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +5 -1
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +6 -2
- package/templates/nextjs/ui-canvas/product/product-card.tsx +6 -2
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +3 -28
- package/templates/nextjs/base/src/app/products/layout.tsx +0 -18
|
@@ -15,7 +15,13 @@ import type {
|
|
|
15
15
|
} from 'brainerce';
|
|
16
16
|
import { formatPrice } from 'brainerce';
|
|
17
17
|
import { getClient } from '@/core/lib/brainerce';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
useStoreInfo,
|
|
20
|
+
useCart,
|
|
21
|
+
useAuth,
|
|
22
|
+
useRegion,
|
|
23
|
+
useStoreCapabilities,
|
|
24
|
+
} from '@/core/providers/store-provider';
|
|
19
25
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
20
26
|
import { CheckoutForm } from '@/components/checkout/checkout-form';
|
|
21
27
|
import { ShippingStep } from '@/components/checkout/shipping-step';
|
|
@@ -31,16 +37,68 @@ import { GiftCardInput } from '@/ui/cart/gift-card-input';
|
|
|
31
37
|
import { ReservationCountdown } from '@/ui/cart/reservation-countdown';
|
|
32
38
|
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
33
39
|
import { useTranslations } from '@/core/lib/translations';
|
|
34
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
addToCartErrorMessageKey,
|
|
42
|
+
toAddToCartError,
|
|
43
|
+
type AddToCartError,
|
|
44
|
+
} from '@/core/lib/add-to-cart-error';
|
|
35
45
|
import { cn } from '@/core/lib/utils';
|
|
36
46
|
import { isValidCheckoutId } from '@/core/lib/safe-redirect';
|
|
37
47
|
import { trackBeginCheckout } from '@/core/lib/tracking';
|
|
38
48
|
|
|
49
|
+
// Remembers which checkout belongs to which cart for this browser tab, so a
|
|
50
|
+
// shopper who leaves /checkout and comes back (the bare link has no
|
|
51
|
+
// `?checkout_id=`) resumes the same checkout instead of re-entering everything.
|
|
52
|
+
const CHECKOUT_RESUME_PREFIX = 'brainerce:checkout:';
|
|
53
|
+
|
|
54
|
+
function readStoredCheckoutId(cartId: string): string | null {
|
|
55
|
+
try {
|
|
56
|
+
const raw = window.sessionStorage.getItem(CHECKOUT_RESUME_PREFIX + cartId);
|
|
57
|
+
return isValidCheckoutId(raw) ? raw : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function storeCheckoutId(cartId: string, checkoutId: string): void {
|
|
64
|
+
try {
|
|
65
|
+
window.sessionStorage.setItem(CHECKOUT_RESUME_PREFIX + cartId, checkoutId);
|
|
66
|
+
} catch {
|
|
67
|
+
// Private mode / blocked storage: resuming is a convenience, never required.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function clearStoredCheckoutId(cartId: string): void {
|
|
72
|
+
try {
|
|
73
|
+
window.sessionStorage.removeItem(CHECKOUT_RESUME_PREFIX + cartId);
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A remembered checkout is only reused while it is still open AND still
|
|
81
|
+
* describes the cart: same lines, same quantities. A cart edited since would
|
|
82
|
+
* otherwise resume a checkout that charges for something else.
|
|
83
|
+
*/
|
|
84
|
+
function matchesCart(
|
|
85
|
+
prior: Checkout,
|
|
86
|
+
cart: { items: Array<{ productId: string; variantId?: string | null; quantity: number }> }
|
|
87
|
+
): boolean {
|
|
88
|
+
if (prior.status !== 'PENDING' && prior.status !== 'SHIPPING_SET') return false;
|
|
89
|
+
const key = (i: { productId: string; variantId?: string | null; quantity: number }) =>
|
|
90
|
+
`${i.productId}:${i.variantId ?? ''}:${i.quantity}`;
|
|
91
|
+
const a = prior.lineItems.map(key).sort();
|
|
92
|
+
const b = cart.items.map(key).sort();
|
|
93
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
94
|
+
}
|
|
95
|
+
|
|
39
96
|
type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
|
|
40
97
|
|
|
41
98
|
function CheckoutContent() {
|
|
42
99
|
const searchParams = useSearchParams();
|
|
43
100
|
const { storeInfo } = useStoreInfo();
|
|
101
|
+
const { capabilities } = useStoreCapabilities();
|
|
44
102
|
const { cart, refreshCart } = useCart();
|
|
45
103
|
const { isLoggedIn } = useAuth();
|
|
46
104
|
// ⛔ THE call the region feature exists for. Product reads alone and the
|
|
@@ -211,9 +269,27 @@ function CheckoutContent() {
|
|
|
211
269
|
const locations = await client.getPickupLocations().catch(() => [] as PickupLocation[]);
|
|
212
270
|
setPickupLocations(locations);
|
|
213
271
|
|
|
272
|
+
// The bare /checkout link carries no id. Look for the one this tab already
|
|
273
|
+
// opened for this very cart, and use it only while it still matches the
|
|
274
|
+
// cart. A shopper choosing a subset of lines always starts a fresh one.
|
|
275
|
+
let resumed: Checkout | null = null;
|
|
276
|
+
if (!existingCheckoutId && !selectedItemsParam) {
|
|
277
|
+
const storedId = readStoredCheckoutId(cart.id);
|
|
278
|
+
if (storedId) {
|
|
279
|
+
try {
|
|
280
|
+
const prior = await client.getCheckout(storedId);
|
|
281
|
+
if (matchesCart(prior, cart)) resumed = prior;
|
|
282
|
+
else clearStoredCheckoutId(cart.id);
|
|
283
|
+
} catch {
|
|
284
|
+
clearStoredCheckoutId(cart.id);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
214
289
|
// If returning with existing checkout ID, resume it
|
|
215
|
-
if (existingCheckoutId) {
|
|
216
|
-
const existing = await client.getCheckout(existingCheckoutId);
|
|
290
|
+
if (existingCheckoutId || resumed) {
|
|
291
|
+
const existing = resumed ?? (await client.getCheckout(existingCheckoutId as string));
|
|
292
|
+
storeCheckoutId(cart.id, existing.id);
|
|
217
293
|
setCheckout(existing);
|
|
218
294
|
|
|
219
295
|
// Preload custom field definitions and any existing values so the
|
|
@@ -279,6 +355,8 @@ function CheckoutContent() {
|
|
|
279
355
|
...(regionId ? { regionId } : {}),
|
|
280
356
|
});
|
|
281
357
|
setCheckout(newCheckout);
|
|
358
|
+
// A subset checkout belongs to those lines only, never to the whole cart.
|
|
359
|
+
if (!isStrictSubset) storeCheckoutId(cart.id, newCheckout.id);
|
|
282
360
|
|
|
283
361
|
// Put the id in the URL so a RELOAD resumes instead of starting over.
|
|
284
362
|
//
|
|
@@ -1057,7 +1135,7 @@ function CheckoutContent() {
|
|
|
1057
1135
|
*/}
|
|
1058
1136
|
{bumpError && (
|
|
1059
1137
|
<p className="text-destructive text-xs" role="alert">
|
|
1060
|
-
{
|
|
1138
|
+
{tp(addToCartErrorMessageKey(bumpError))}
|
|
1061
1139
|
</p>
|
|
1062
1140
|
)}
|
|
1063
1141
|
</div>
|
|
@@ -1077,7 +1155,12 @@ function CheckoutContent() {
|
|
|
1077
1155
|
for an order still worth the same. They look adjacent
|
|
1078
1156
|
because a shopper reaches for them at the same moment, and
|
|
1079
1157
|
they behave differently because they are different things. */}
|
|
1080
|
-
{
|
|
1158
|
+
{/* Hidden while the store has gift cards off (`hasGiftCards`), so the
|
|
1159
|
+
shopper is not offered a box that can never accept a code. A card
|
|
1160
|
+
already applied keeps the field so it can still be removed. */}
|
|
1161
|
+
{checkout?.id &&
|
|
1162
|
+
(capabilities?.features.hasGiftCards === true ||
|
|
1163
|
+
(checkout.tenders?.length ?? 0) > 0) && (
|
|
1081
1164
|
<GiftCardInput
|
|
1082
1165
|
className="mt-3"
|
|
1083
1166
|
checkoutId={checkout.id}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: <%- language === 'he' ? "'מוצרים'" : "'Products'" %>,
|
|
5
|
+
description: <%- language === 'he' ? "'כל קטלוג המוצרים שלנו.'" : "'Browse our full collection of products.'" %>,
|
|
6
|
+
alternates: {
|
|
7
|
+
canonical: '/products',
|
|
8
|
+
},
|
|
9
|
+
openGraph: {
|
|
10
|
+
title: <%- language === 'he' ? "'מוצרים'" : "'Products'" %>,
|
|
11
|
+
description: <%- language === 'he' ? "'כל קטלוג המוצרים שלנו.'" : "'Browse our full collection of products.'" %>,
|
|
12
|
+
type: 'website',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
|
|
17
|
+
return <>{children}</>;
|
|
18
|
+
}
|