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,142 +1,146 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import type { Cart } from 'brainerce';
|
|
5
|
-
import { getClient } from '@/core/lib/brainerce';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const [
|
|
30
|
-
const [
|
|
31
|
-
const [
|
|
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
|
-
{removing
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { Cart } from 'brainerce';
|
|
5
|
+
import { getClient } from '@/core/lib/brainerce';
|
|
6
|
+
import { couponErrorKey } from '@/core/lib/coupon-error';
|
|
7
|
+
import { useTranslations } from '@/core/lib/translations';
|
|
8
|
+
import { LoadingSpinner } from '@/ui/shared/loading-spinner';
|
|
9
|
+
import { cn } from '@/core/lib/utils';
|
|
10
|
+
import { IconTag, IconX } from '@/ui/shared/icons';
|
|
11
|
+
|
|
12
|
+
interface CouponInputProps {
|
|
13
|
+
cart: Cart;
|
|
14
|
+
/** When provided, uses applyCheckoutCoupon/removeCheckoutCoupon instead of
|
|
15
|
+
* applyCoupon/removeCoupon. Always pass this when a checkout session exists. */
|
|
16
|
+
checkoutId?: string;
|
|
17
|
+
onUpdate: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Coupon apply/remove control used on the cart page AND the checkout page
|
|
23
|
+
* (via `checkoutId`); the `cart` prop comes from useCartPage(). Keeps the
|
|
24
|
+
* applied-coupon state and the error message.
|
|
25
|
+
*/
|
|
26
|
+
export function CouponInput({ cart, checkoutId, onUpdate, className }: CouponInputProps) {
|
|
27
|
+
const t = useTranslations('coupon');
|
|
28
|
+
const tc = useTranslations('common');
|
|
29
|
+
const [code, setCode] = useState('');
|
|
30
|
+
const [applying, setApplying] = useState(false);
|
|
31
|
+
const [removing, setRemoving] = useState(false);
|
|
32
|
+
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
|
|
34
|
+
const appliedCoupon = cart.couponCode || null;
|
|
35
|
+
|
|
36
|
+
async function handleApply() {
|
|
37
|
+
const trimmed = code.trim();
|
|
38
|
+
if (!trimmed || applying) return;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
setApplying(true);
|
|
42
|
+
setError(null);
|
|
43
|
+
const client = getClient();
|
|
44
|
+
if (checkoutId) {
|
|
45
|
+
await client.applyCheckoutCoupon(checkoutId, trimmed);
|
|
46
|
+
} else {
|
|
47
|
+
await client.applyCoupon(cart.id, trimmed);
|
|
48
|
+
}
|
|
49
|
+
setCode('');
|
|
50
|
+
onUpdate();
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// Known codes are shown from our own (translated) messages; the API's
|
|
53
|
+
// `message` is English. An uncoded refusal falls back to that message.
|
|
54
|
+
const key = couponErrorKey(err);
|
|
55
|
+
const message = key ? t(key) : err instanceof Error ? err.message : t('invalidCode');
|
|
56
|
+
setError(message);
|
|
57
|
+
} finally {
|
|
58
|
+
setApplying(false);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function handleRemove() {
|
|
63
|
+
if (removing) return;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
setRemoving(true);
|
|
67
|
+
setError(null);
|
|
68
|
+
const client = getClient();
|
|
69
|
+
if (checkoutId) {
|
|
70
|
+
await client.removeCheckoutCoupon(checkoutId);
|
|
71
|
+
} else {
|
|
72
|
+
await client.removeCoupon(cart.id);
|
|
73
|
+
}
|
|
74
|
+
onUpdate();
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error('Failed to remove coupon:', err);
|
|
77
|
+
} finally {
|
|
78
|
+
setRemoving(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Show applied coupon
|
|
83
|
+
if (appliedCoupon) {
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className={cn(
|
|
87
|
+
'bg-secondary inline-flex items-center gap-2 rounded-full py-1.5 pe-2 ps-4',
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
<span aria-hidden="true" className="text-primary">
|
|
92
|
+
<IconTag size={16} />
|
|
93
|
+
</span>
|
|
94
|
+
<span className="text-foreground text-sm font-semibold">{appliedCoupon}</span>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={handleRemove}
|
|
98
|
+
disabled={removing}
|
|
99
|
+
aria-label={tc('remove')}
|
|
100
|
+
className="text-muted-foreground hover:bg-accent hover:text-foreground flex h-7 w-7 items-center justify-center rounded-full transition-colors disabled:opacity-50"
|
|
101
|
+
>
|
|
102
|
+
{removing ? <LoadingSpinner size="sm" /> : <IconX size={16} />}
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className={cn(className)}>
|
|
110
|
+
<div className="flex max-w-sm gap-2">
|
|
111
|
+
<input
|
|
112
|
+
type="text"
|
|
113
|
+
value={code}
|
|
114
|
+
onChange={(e) => {
|
|
115
|
+
setCode(e.target.value);
|
|
116
|
+
if (error) setError(null);
|
|
117
|
+
}}
|
|
118
|
+
onKeyDown={(e) => {
|
|
119
|
+
if (e.key === 'Enter') {
|
|
120
|
+
e.preventDefault();
|
|
121
|
+
handleApply();
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
124
|
+
placeholder={t('placeholder')}
|
|
125
|
+
aria-label={t('placeholder')}
|
|
126
|
+
aria-invalid={error ? true : undefined}
|
|
127
|
+
className="input"
|
|
128
|
+
/>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
onClick={handleApply}
|
|
132
|
+
disabled={applying || !code.trim()}
|
|
133
|
+
className="btn-soft btn-md shrink-0"
|
|
134
|
+
>
|
|
135
|
+
{applying ? <LoadingSpinner size="sm" /> : tc('apply')}
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{error && (
|
|
140
|
+
<p role="alert" className="text-destructive mt-2 text-sm font-medium">
|
|
141
|
+
{error}
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -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 gap-4">
|
|
96
|
-
<dt className="text-muted-foreground">
|
|
96
|
+
<dt className="text-muted-foreground">
|
|
97
|
+
{estimate.pricesIncludeTax ? `${label} ${tc('inclSuffix')}` : label}
|
|
98
|
+
</dt>
|
|
97
99
|
<dd className="text-foreground font-medium">
|
|
98
100
|
{formatPrice(estimate.estimatedTax, { currency: estimate.currency }) as string}
|
|
99
101
|
</dd>
|
|
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}
|
|
@@ -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 { cn } from '@/core/lib/utils';
|
|
13
17
|
import { IconPlus, IconBag } from '@/ui/shared/icons';
|
|
14
18
|
|
|
@@ -224,7 +228,7 @@ export function FrequentlyBoughtTogether({
|
|
|
224
228
|
*/}
|
|
225
229
|
{addError && (
|
|
226
230
|
<p role="alert" className="text-destructive mt-3 text-sm font-medium">
|
|
227
|
-
{
|
|
231
|
+
{t(addToCartErrorMessageKey(addError))}
|
|
228
232
|
</p>
|
|
229
233
|
)}
|
|
230
234
|
</section>
|
|
@@ -16,7 +16,11 @@ import { flyToCart } from '@/ui/shared/fly-to-cart';
|
|
|
16
16
|
import { openCartDrawer } from '@/ui/cart/cart-drawer';
|
|
17
17
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
18
18
|
import { trackAddToCart } from '@/core/lib/tracking';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
addToCartErrorMessageKey,
|
|
21
|
+
toAddToCartError,
|
|
22
|
+
type AddToCartError,
|
|
23
|
+
} from '@/core/lib/add-to-cart-error';
|
|
20
24
|
import { cn } from '@/core/lib/utils';
|
|
21
25
|
import { NoPhotoTile } from '@/ui/shared/no-photo-tile';
|
|
22
26
|
import { IconBag, IconCheck, IconDownload, IconStar } from '@/ui/shared/icons';
|
|
@@ -262,7 +266,7 @@ export function ProductCard({ product, className }: ProductCardProps) {
|
|
|
262
266
|
*/}
|
|
263
267
|
{addError && (
|
|
264
268
|
<p role="alert" className="text-destructive mt-2 text-xs">
|
|
265
|
-
{
|
|
269
|
+
{tp(addToCartErrorMessageKey(addError))}
|
|
266
270
|
</p>
|
|
267
271
|
)}
|
|
268
272
|
</div>
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
5
|
import { NoPhotoTile } from '@/ui/shared/no-photo-tile';
|
|
6
|
-
import type { Product, ProductMetafield
|
|
6
|
+
import type { Product, ProductMetafield } from 'brainerce';
|
|
7
7
|
import { useProductPage } from '@/core/hooks/use-product-page';
|
|
8
|
+
import { addToCartErrorMessageKey } from '@/core/lib/add-to-cart-error';
|
|
8
9
|
import { PriceDisplay } from '@/ui/product/price-display';
|
|
9
10
|
import { VariantSelector } from '@/ui/product/variant-selector';
|
|
10
11
|
import { StockBadge } from '@/ui/product/stock-badge';
|
|
@@ -289,32 +290,6 @@ export function ProductClientSection({
|
|
|
289
290
|
)}
|
|
290
291
|
</div>
|
|
291
292
|
|
|
292
|
-
{/* Downloadable files info */}
|
|
293
|
-
{product.isDownloadable && product.downloads && product.downloads.length > 0 && (
|
|
294
|
-
<section className="bg-secondary/60 mt-6 rounded-xl border p-4">
|
|
295
|
-
<h2 className="text-sm font-semibold">
|
|
296
|
-
{t('filesIncluded').replace('{count}', String(product.downloads.length))}
|
|
297
|
-
</h2>
|
|
298
|
-
<ul className="text-muted-foreground mt-2 space-y-1.5 text-sm">
|
|
299
|
-
{product.downloads.map((file: DownloadFile) => (
|
|
300
|
-
<li key={file.id} className="flex items-center gap-2">
|
|
301
|
-
<IconDownload size={16} />
|
|
302
|
-
<span>{file.name}</span>
|
|
303
|
-
{file.size && (
|
|
304
|
-
<span className="text-xs">
|
|
305
|
-
(
|
|
306
|
-
{file.size < 1024 * 1024
|
|
307
|
-
? `${(file.size / 1024).toFixed(0)} KB`
|
|
308
|
-
: `${(file.size / (1024 * 1024)).toFixed(1)} MB`}
|
|
309
|
-
)
|
|
310
|
-
</span>
|
|
311
|
-
)}
|
|
312
|
-
</li>
|
|
313
|
-
))}
|
|
314
|
-
</ul>
|
|
315
|
-
</section>
|
|
316
|
-
)}
|
|
317
|
-
|
|
318
293
|
{/* Variant Selector */}
|
|
319
294
|
{/* A KIT is bought as ONE line, but the shopper needs to see what is
|
|
320
295
|
in the box before deciding. These rows are display only — never add
|
|
@@ -447,7 +422,7 @@ export function ProductClientSection({
|
|
|
447
422
|
*/}
|
|
448
423
|
{addToCartError && (
|
|
449
424
|
<p role="alert" className="text-destructive mt-3 text-sm font-medium">
|
|
450
|
-
{
|
|
425
|
+
{t(addToCartErrorMessageKey(addToCartError))}
|
|
451
426
|
</p>
|
|
452
427
|
)}
|
|
453
428
|
|
|
@@ -6,7 +6,11 @@ import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
7
|
import { useCurrency } from '@/core/lib/use-currency';
|
|
8
8
|
import { useTranslations } from '@/core/lib/translations';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
addToCartErrorMessageKey,
|
|
11
|
+
toAddToCartError,
|
|
12
|
+
type AddToCartError,
|
|
13
|
+
} from '@/core/lib/add-to-cart-error';
|
|
10
14
|
import { cn } from '@/core/lib/utils';
|
|
11
15
|
|
|
12
16
|
interface CartBundleOfferCardProps {
|
|
@@ -98,7 +102,7 @@ export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBun
|
|
|
98
102
|
nothing is added, which reads as a broken button.
|
|
99
103
|
*/}
|
|
100
104
|
{addError && (
|
|
101
|
-
<p role="alert">{
|
|
105
|
+
<p role="alert">{tp(addToCartErrorMessageKey(addError))}</p>
|
|
102
106
|
)}
|
|
103
107
|
</article>
|
|
104
108
|
);
|
|
@@ -7,7 +7,11 @@ import { formatPrice } from 'brainerce';
|
|
|
7
7
|
import { getClient } from '@/core/lib/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
|
|
|
13
17
|
interface CartUpgradeBannerProps {
|
|
@@ -188,9 +192,7 @@ export function CartUpgradeBanner({
|
|
|
188
192
|
<p role="alert">
|
|
189
193
|
{upgradeError === 'ORIGINAL_NOT_REMOVED'
|
|
190
194
|
? t('upgradeOriginalNotRemoved')
|
|
191
|
-
: upgradeError
|
|
192
|
-
? tp('cartFull')
|
|
193
|
-
: tp('addToCartFailed')}
|
|
195
|
+
: tp(addToCartErrorMessageKey(upgradeError))}
|
|
194
196
|
</p>
|
|
195
197
|
)}
|
|
196
198
|
</div>
|