create-brainerce-store 1.86.1 → 1.88.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 +9 -2
- package/messages/en.json +15 -2
- package/messages/he.json +15 -2
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +296 -255
- package/templates/nextjs/base/src/app/checkout/page.tsx +89 -6
- package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +8 -0
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +6 -0
- package/templates/nextjs/base/src/app/products/layout.tsx.ejs +18 -0
- package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +37 -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/components/google-customer-reviews-optin.tsx +126 -0
- 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 -81
- package/templates/nextjs/base/src/core/lib/coupon-error.ts +29 -0
- package/templates/nextjs/base/src/middleware.ts.ejs +22 -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
|
@@ -1,134 +1,138 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { Tag } from 'lucide-react';
|
|
5
|
-
import type { Cart } from 'brainerce';
|
|
6
|
-
import { getClient } from '@/core/lib/brainerce';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const [
|
|
27
|
-
const [
|
|
28
|
-
const [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Tag } from 'lucide-react';
|
|
5
|
+
import type { Cart } from 'brainerce';
|
|
6
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
7
|
+
import { couponErrorKey } from '@/core/lib/coupon-error';
|
|
8
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
+
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
10
|
+
import { Button } from '@/components/ui/button';
|
|
11
|
+
import { Input } from '@/components/ui/input';
|
|
12
|
+
import { cn } from '@/core/lib/utils';
|
|
13
|
+
|
|
14
|
+
interface CouponInputProps {
|
|
15
|
+
cart: Cart;
|
|
16
|
+
/** When provided, uses applyCheckoutCoupon/removeCheckoutCoupon instead of
|
|
17
|
+
* applyCoupon/removeCoupon. Always pass this when a checkout session exists. */
|
|
18
|
+
checkoutId?: string;
|
|
19
|
+
onUpdate: () => void;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CouponInput({ cart, checkoutId, onUpdate, className }: CouponInputProps) {
|
|
24
|
+
const t = useTranslations('coupon');
|
|
25
|
+
const tc = useTranslations('common');
|
|
26
|
+
const [code, setCode] = useState('');
|
|
27
|
+
const [applying, setApplying] = useState(false);
|
|
28
|
+
const [removing, setRemoving] = useState(false);
|
|
29
|
+
const [error, setError] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
const appliedCoupon = cart.couponCode || null;
|
|
32
|
+
|
|
33
|
+
async function handleApply() {
|
|
34
|
+
const trimmed = code.trim();
|
|
35
|
+
if (!trimmed || applying) return;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
setApplying(true);
|
|
39
|
+
setError(null);
|
|
40
|
+
const client = getClient();
|
|
41
|
+
if (checkoutId) {
|
|
42
|
+
await client.applyCheckoutCoupon(checkoutId, trimmed);
|
|
43
|
+
} else {
|
|
44
|
+
await client.applyCoupon(cart.id, trimmed);
|
|
45
|
+
}
|
|
46
|
+
setCode('');
|
|
47
|
+
onUpdate();
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// Known codes are shown from our own (translated) messages; the API's
|
|
50
|
+
// `message` is English. An uncoded refusal falls back to that message.
|
|
51
|
+
const key = couponErrorKey(err);
|
|
52
|
+
const message = key ? t(key) : err instanceof Error ? err.message : t('invalidCode');
|
|
53
|
+
setError(message);
|
|
54
|
+
} finally {
|
|
55
|
+
setApplying(false);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleRemove() {
|
|
60
|
+
if (removing) return;
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
setRemoving(true);
|
|
64
|
+
setError(null);
|
|
65
|
+
const client = getClient();
|
|
66
|
+
if (checkoutId) {
|
|
67
|
+
await client.removeCheckoutCoupon(checkoutId);
|
|
68
|
+
} else {
|
|
69
|
+
await client.removeCoupon(cart.id);
|
|
70
|
+
}
|
|
71
|
+
onUpdate();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error('Failed to remove coupon:', err);
|
|
74
|
+
} finally {
|
|
75
|
+
setRemoving(false);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Show applied coupon
|
|
80
|
+
if (appliedCoupon) {
|
|
81
|
+
return (
|
|
82
|
+
<div className={cn('space-y-2', className)}>
|
|
83
|
+
<div className="bg-muted flex items-center justify-between rounded px-3 py-2">
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<Tag className="text-primary h-4 w-4 flex-shrink-0" aria-hidden="true" />
|
|
86
|
+
<span className="text-foreground text-sm font-medium">{appliedCoupon}</span>
|
|
87
|
+
</div>
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={handleRemove}
|
|
91
|
+
disabled={removing}
|
|
92
|
+
className="text-destructive hover:text-destructive/80 text-xs transition-colors disabled:opacity-40"
|
|
93
|
+
>
|
|
94
|
+
{removing ? tc('removing') : tc('remove')}
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className={cn('space-y-2', className)}>
|
|
103
|
+
<div className="flex gap-2">
|
|
104
|
+
<Input
|
|
105
|
+
type="text"
|
|
106
|
+
value={code}
|
|
107
|
+
onChange={(e) => {
|
|
108
|
+
setCode(e.target.value);
|
|
109
|
+
if (error) setError(null);
|
|
110
|
+
}}
|
|
111
|
+
onKeyDown={(e) => {
|
|
112
|
+
if (e.key === 'Enter') {
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
handleApply();
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
placeholder={t('placeholder')}
|
|
118
|
+
className={cn('h-9 flex-1 rounded', error && 'border-destructive')}
|
|
119
|
+
/>
|
|
120
|
+
<Button
|
|
121
|
+
type="button"
|
|
122
|
+
variant="outline"
|
|
123
|
+
onClick={handleApply}
|
|
124
|
+
disabled={applying || !code.trim()}
|
|
125
|
+
className="hover:bg-muted h-9 rounded px-4"
|
|
126
|
+
>
|
|
127
|
+
{applying ? (
|
|
128
|
+
<LoadingSpinner size="sm" className="border-muted-foreground/30 border-t-foreground" />
|
|
129
|
+
) : (
|
|
130
|
+
tc('apply')
|
|
131
|
+
)}
|
|
132
|
+
</Button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{error && <p className="text-destructive text-xs">{error}</p>}
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -93,13 +93,20 @@ export function TaxEstimateLine() {
|
|
|
93
93
|
return (
|
|
94
94
|
<div className="space-y-1">
|
|
95
95
|
<div className="flex items-center justify-between">
|
|
96
|
-
<span className="text-muted-foreground">
|
|
96
|
+
<span className="text-muted-foreground">
|
|
97
|
+
{estimate.pricesIncludeTax ? `${label} ${tc('inclSuffix')}` : label}
|
|
98
|
+
</span>
|
|
97
99
|
<span className="text-foreground font-medium">
|
|
98
100
|
{formatPrice(estimate.estimatedTax, { currency: estimate.currency }) as string}
|
|
99
101
|
</span>
|
|
100
102
|
</div>
|
|
101
|
-
{/* The caveat travels WITH the number, not in a footnote nobody reads.
|
|
102
|
-
|
|
103
|
+
{/* The caveat travels WITH the number, not in a footnote nobody reads.
|
|
104
|
+
A VAT-inclusive store has nothing left to calculate: the tax is already
|
|
105
|
+
inside the prices above, so "calculated at checkout" would promise an
|
|
106
|
+
extra charge that never comes. */}
|
|
107
|
+
{estimate.pricesIncludeTax ? null : (
|
|
108
|
+
<p className="text-muted-foreground text-xs">{t('taxAtCheckout')}</p>
|
|
109
|
+
)}
|
|
103
110
|
{/* Server-written and already localized. Never replace it with copy of
|
|
104
111
|
your own, it is what explains a partial estimate. */}
|
|
105
112
|
{estimate.note ? <p className="text-muted-foreground text-xs">{estimate.note}</p> : null}
|
|
@@ -71,7 +71,11 @@ export function NewsletterSignup() {
|
|
|
71
71
|
getClient()
|
|
72
72
|
.marketing.getBenefit(locale)
|
|
73
73
|
.then((result) => {
|
|
74
|
-
|
|
74
|
+
// Only a complete, enabled offer is ever shown. An empty or partial body
|
|
75
|
+
// (an older backend, a half-configured benefit) must read as "no offer",
|
|
76
|
+
// never as "undefined off your first order".
|
|
77
|
+
const usable = result && result.enabled === true && Number.isFinite(result.discountValue);
|
|
78
|
+
if (!cancelled) setOffer(usable ? result : null);
|
|
75
79
|
})
|
|
76
80
|
.catch(() => {
|
|
77
81
|
// Silent on purpose. A failed offer read must not stop someone
|
|
@@ -71,7 +71,7 @@ export function SiteFooter({ footer, storeName }: SiteFooterProps) {
|
|
|
71
71
|
<div className="mx-auto max-w-md text-start">
|
|
72
72
|
<NewsletterSignup />
|
|
73
73
|
</div>
|
|
74
|
-
<div>© {year} {brandLabel}. All rights reserved
|
|
74
|
+
<div>© {year} {brandLabel}. <%- language === 'he' ? 'כל הזכויות שמורות.' : 'All rights reserved.' %></div>
|
|
75
75
|
</div>
|
|
76
76
|
</footer>
|
|
77
77
|
);
|
|
@@ -152,7 +152,7 @@ export function SiteFooter({ footer, storeName }: SiteFooterProps) {
|
|
|
152
152
|
</div>
|
|
153
153
|
|
|
154
154
|
<div className="border-border text-muted-foreground mt-10 flex flex-col items-center justify-between gap-2 border-t pt-6 text-xs sm:flex-row">
|
|
155
|
-
<span>{data.copyright ?? `© ${year} ${brandLabel}. All rights reserved
|
|
155
|
+
<span>{data.copyright ?? `© ${year} ${brandLabel}. <%- language === 'he' ? 'כל הזכויות שמורות.' : 'All rights reserved.' %>`}</span>
|
|
156
156
|
</div>
|
|
157
157
|
</div>
|
|
158
158
|
</footer>
|
|
@@ -8,7 +8,11 @@ import { formatPrice } from 'brainerce';
|
|
|
8
8
|
import { useCart, useStoreInfo } from '@/core/providers/store-provider';
|
|
9
9
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
10
10
|
import { useTranslations } from '@/core/lib/translations';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
addToCartErrorMessageKey,
|
|
13
|
+
toAddToCartError,
|
|
14
|
+
type AddToCartError,
|
|
15
|
+
} from '@/core/lib/add-to-cart-error';
|
|
12
16
|
import { Button } from '@/components/ui/button';
|
|
13
17
|
import { Card } from '@/components/ui/card';
|
|
14
18
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
@@ -214,7 +218,7 @@ export function FrequentlyBoughtTogether({
|
|
|
214
218
|
*/}
|
|
215
219
|
{addError && (
|
|
216
220
|
<p role="alert" className="text-destructive mt-3 text-sm">
|
|
217
|
-
{
|
|
221
|
+
{t(addToCartErrorMessageKey(addError))}
|
|
218
222
|
</p>
|
|
219
223
|
)}
|
|
220
224
|
</Card>
|
|
@@ -18,7 +18,11 @@ import { Card } from '@/components/ui/card';
|
|
|
18
18
|
import { useCart } from '@/core/providers/store-provider';
|
|
19
19
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
20
20
|
import { trackAddToCart } from '@/core/lib/tracking';
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
addToCartErrorMessageKey,
|
|
23
|
+
toAddToCartError,
|
|
24
|
+
type AddToCartError,
|
|
25
|
+
} from '@/core/lib/add-to-cart-error';
|
|
22
26
|
import { cn } from '@/core/lib/utils';
|
|
23
27
|
|
|
24
28
|
interface ProductCardProps {
|
|
@@ -238,7 +242,7 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
238
242
|
*/}
|
|
239
243
|
{addError && (
|
|
240
244
|
<p className="text-destructive text-xs" role="alert">
|
|
241
|
-
{
|
|
245
|
+
{tp(addToCartErrorMessageKey(addError))}
|
|
242
246
|
</p>
|
|
243
247
|
)}
|
|
244
248
|
</div>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Download,
|
|
3
|
+
import { Download, Image as ImageIcon } from 'lucide-react';
|
|
4
4
|
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
-
import type { Product, ProductMetafield
|
|
5
|
+
import type { Product, ProductMetafield } from 'brainerce';
|
|
6
6
|
import { useProductPage } from '@/core/hooks/use-product-page';
|
|
7
|
+
import { addToCartErrorMessageKey } from '@/core/lib/add-to-cart-error';
|
|
7
8
|
import { PriceDisplay } from '@/ui/product/price-display';
|
|
8
9
|
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
9
10
|
import { VariantSelector } from '@/ui/product/variant-selector';
|
|
@@ -283,39 +284,6 @@ export function ProductClientSection({
|
|
|
283
284
|
<StockBadge inventory={inventory} />
|
|
284
285
|
)}
|
|
285
286
|
|
|
286
|
-
{/* Downloadable files info */}
|
|
287
|
-
{product.isDownloadable && product.downloads && product.downloads.length > 0 && (
|
|
288
|
-
<div className="bg-muted/50 rounded-lg border p-4">
|
|
289
|
-
<p className="text-foreground mb-2 text-sm font-medium">
|
|
290
|
-
{t('filesIncluded')} ({product.downloads.length})
|
|
291
|
-
</p>
|
|
292
|
-
<ul className="space-y-1.5">
|
|
293
|
-
{product.downloads.map((file: DownloadFile) => (
|
|
294
|
-
<li
|
|
295
|
-
key={file.id}
|
|
296
|
-
className="text-muted-foreground flex items-center gap-2 text-sm"
|
|
297
|
-
>
|
|
298
|
-
<FileText
|
|
299
|
-
className="h-4 w-4 flex-shrink-0"
|
|
300
|
-
strokeWidth={1.5}
|
|
301
|
-
aria-hidden="true"
|
|
302
|
-
/>
|
|
303
|
-
<span className="truncate">{file.name}</span>
|
|
304
|
-
{file.size && (
|
|
305
|
-
<span className="flex-shrink-0 text-xs">
|
|
306
|
-
(
|
|
307
|
-
{file.size < 1024 * 1024
|
|
308
|
-
? `${(file.size / 1024).toFixed(0)} KB`
|
|
309
|
-
: `${(file.size / (1024 * 1024)).toFixed(1)} MB`}
|
|
310
|
-
)
|
|
311
|
-
</span>
|
|
312
|
-
)}
|
|
313
|
-
</li>
|
|
314
|
-
))}
|
|
315
|
-
</ul>
|
|
316
|
-
</div>
|
|
317
|
-
)}
|
|
318
|
-
|
|
319
287
|
{/* Variant Selector */}
|
|
320
288
|
{/* A KIT is bought as ONE line, but the shopper needs to see what is
|
|
321
289
|
in the box before deciding. These rows are display only — never add
|
|
@@ -445,7 +413,7 @@ export function ProductClientSection({
|
|
|
445
413
|
*/}
|
|
446
414
|
{addToCartError && (
|
|
447
415
|
<p className="text-destructive text-sm" role="alert">
|
|
448
|
-
{
|
|
416
|
+
{t(addToCartErrorMessageKey(addToCartError))}
|
|
449
417
|
</p>
|
|
450
418
|
)}
|
|
451
419
|
|
|
@@ -7,7 +7,11 @@ import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
|
7
7
|
import { formatPrice } from 'brainerce';
|
|
8
8
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
9
9
|
import { useTranslations } from '@/core/lib/translations';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
addToCartErrorMessageKey,
|
|
12
|
+
toAddToCartError,
|
|
13
|
+
type AddToCartError,
|
|
14
|
+
} from '@/core/lib/add-to-cart-error';
|
|
11
15
|
import { cn } from '@/core/lib/utils';
|
|
12
16
|
import { IconBag } from '@/ui/shared/icons';
|
|
13
17
|
|
|
@@ -123,7 +127,7 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
123
127
|
*/}
|
|
124
128
|
{addError && (
|
|
125
129
|
<p role="alert" className="text-destructive mt-2 text-xs">
|
|
126
|
-
{
|
|
130
|
+
{tp(addToCartErrorMessageKey(addError))}
|
|
127
131
|
</p>
|
|
128
132
|
)}
|
|
129
133
|
</article>
|
|
@@ -8,7 +8,11 @@ import { formatPrice } from 'brainerce';
|
|
|
8
8
|
import { getClient } from '@/core/lib/brainerce';
|
|
9
9
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
10
10
|
import { useTranslations } from '@/core/lib/translations';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
addToCartErrorMessageKey,
|
|
13
|
+
toAddToCartError,
|
|
14
|
+
type AddToCartError,
|
|
15
|
+
} from '@/core/lib/add-to-cart-error';
|
|
12
16
|
import { cn } from '@/core/lib/utils';
|
|
13
17
|
import { IconX } from '@/ui/shared/icons';
|
|
14
18
|
|
|
@@ -191,9 +195,7 @@ export function CartUpgradeBanner({
|
|
|
191
195
|
<p role="alert" className="text-destructive mt-1 text-xs font-medium">
|
|
192
196
|
{upgradeError === 'ORIGINAL_NOT_REMOVED'
|
|
193
197
|
? t('upgradeOriginalNotRemoved')
|
|
194
|
-
: upgradeError
|
|
195
|
-
? tp('cartFull')
|
|
196
|
-
: tp('addToCartFailed')}
|
|
198
|
+
: tp(addToCartErrorMessageKey(upgradeError))}
|
|
197
199
|
</p>
|
|
198
200
|
)}
|
|
199
201
|
</div>
|