create-brainerce-store 1.16.0 → 1.17.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 +1 -1
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/cart/page.tsx +11 -3
- package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +16 -10
- package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +24 -5
- package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +1 -6
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +5 -1
- package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +15 -3
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.16.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
36
|
bin: {
|
|
37
37
|
"create-brainerce-store": "dist/index.js"
|
package/package.json
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
CartRecommendationsResponse,
|
|
7
|
+
CartUpgradesResponse,
|
|
8
|
+
CartBundlesResponse,
|
|
9
|
+
} from 'brainerce';
|
|
6
10
|
import { getClient } from '@/lib/brainerce';
|
|
7
11
|
import { useCart } from '@/providers/store-provider';
|
|
8
12
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
@@ -42,7 +46,11 @@ export default function CartPage() {
|
|
|
42
46
|
|
|
43
47
|
// Load upgrade suggestions when cart changes
|
|
44
48
|
useEffect(() => {
|
|
45
|
-
if (
|
|
49
|
+
if (
|
|
50
|
+
!cart?.id ||
|
|
51
|
+
cart.items.length === 0 ||
|
|
52
|
+
storeInfo?.upsell?.cartUpgradeBannerEnabled === false
|
|
53
|
+
) {
|
|
46
54
|
setUpgrades(null);
|
|
47
55
|
return;
|
|
48
56
|
}
|
|
@@ -144,7 +152,7 @@ export default function CartPage() {
|
|
|
144
152
|
<div className="mt-6 space-y-3">
|
|
145
153
|
<h3 className="text-foreground text-sm font-semibold">{t('bundleOffers')}</h3>
|
|
146
154
|
{bundles.bundles.map((offer) => (
|
|
147
|
-
<CartBundleOfferCard key={offer.id} offer={offer} onAdd={refreshCart} />
|
|
155
|
+
<CartBundleOfferCard key={offer.id} offer={offer} cartId={cart.id} onAdd={refreshCart} />
|
|
148
156
|
))}
|
|
149
157
|
</div>
|
|
150
158
|
)}
|
|
@@ -4,18 +4,18 @@ import { useState } from 'react';
|
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
import type { CartBundleOffer as CartBundleOfferType } from 'brainerce';
|
|
6
6
|
import { formatPrice } from 'brainerce';
|
|
7
|
-
import { getClient } from '@/lib/brainerce';
|
|
8
7
|
import { useStoreInfo } from '@/providers/store-provider';
|
|
9
8
|
import { useTranslations } from '@/lib/translations';
|
|
10
9
|
import { cn } from '@/lib/utils';
|
|
11
10
|
|
|
12
11
|
interface CartBundleOfferCardProps {
|
|
13
12
|
offer: CartBundleOfferType;
|
|
13
|
+
cartId: string;
|
|
14
14
|
onAdd: () => void;
|
|
15
15
|
className?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOfferCardProps) {
|
|
18
|
+
export function CartBundleOfferCard({ offer, cartId, onAdd, className }: CartBundleOfferCardProps) {
|
|
19
19
|
const { storeInfo } = useStoreInfo();
|
|
20
20
|
const t = useTranslations('cart');
|
|
21
21
|
const currency = storeInfo?.currency || 'USD';
|
|
@@ -23,24 +23,25 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
23
23
|
|
|
24
24
|
const product = offer.bundleProduct;
|
|
25
25
|
const firstImage = product.images?.[0];
|
|
26
|
-
const imageUrl = firstImage
|
|
26
|
+
const imageUrl = firstImage
|
|
27
|
+
? typeof firstImage === 'string'
|
|
28
|
+
? firstImage
|
|
29
|
+
: firstImage.url
|
|
30
|
+
: null;
|
|
27
31
|
const originalPrice = parseFloat(offer.originalPrice);
|
|
28
32
|
const discountedPrice = parseFloat(offer.discountedPrice);
|
|
29
33
|
const discountLabel =
|
|
30
34
|
offer.discountType === 'PERCENTAGE'
|
|
31
35
|
? `${offer.discountValue}%`
|
|
32
|
-
: formatPrice(parseFloat(offer.discountValue), { currency }) as string;
|
|
36
|
+
: (formatPrice(parseFloat(offer.discountValue), { currency }) as string);
|
|
33
37
|
|
|
34
38
|
async function handleAdd() {
|
|
35
39
|
if (adding) return;
|
|
36
40
|
try {
|
|
37
41
|
setAdding(true);
|
|
42
|
+
const { getClient } = await import('@/lib/brainerce');
|
|
38
43
|
const client = getClient();
|
|
39
|
-
await client.
|
|
40
|
-
productId: product.id,
|
|
41
|
-
variantId: offer.bundleVariantId || undefined,
|
|
42
|
-
quantity: 1,
|
|
43
|
-
});
|
|
44
|
+
await client.addBundleToCart(cartId, offer.id);
|
|
44
45
|
onAdd();
|
|
45
46
|
} catch (err) {
|
|
46
47
|
console.error('Failed to add bundle item:', err);
|
|
@@ -63,7 +64,12 @@ export function CartBundleOfferCard({ offer, onAdd, className }: CartBundleOffer
|
|
|
63
64
|
) : (
|
|
64
65
|
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
65
66
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
66
|
-
<path
|
|
67
|
+
<path
|
|
68
|
+
strokeLinecap="round"
|
|
69
|
+
strokeLinejoin="round"
|
|
70
|
+
strokeWidth={1.5}
|
|
71
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
72
|
+
/>
|
|
67
73
|
</svg>
|
|
68
74
|
</div>
|
|
69
75
|
)}
|
|
@@ -35,20 +35,28 @@ export function CartUpgradeBanner({
|
|
|
35
35
|
if (sessionStorage.getItem(storageKey)) {
|
|
36
36
|
setDismissed(true);
|
|
37
37
|
}
|
|
38
|
-
} catch {
|
|
38
|
+
} catch {
|
|
39
|
+
/* ignore */
|
|
40
|
+
}
|
|
39
41
|
}, [storageKey]);
|
|
40
42
|
|
|
41
43
|
if (dismissed) return null;
|
|
42
44
|
|
|
43
45
|
const target = suggestion.targetProduct;
|
|
44
46
|
const firstImage = target.images?.[0];
|
|
45
|
-
const imageUrl = firstImage
|
|
47
|
+
const imageUrl = firstImage
|
|
48
|
+
? typeof firstImage === 'string'
|
|
49
|
+
? firstImage
|
|
50
|
+
: firstImage.url
|
|
51
|
+
: null;
|
|
46
52
|
const formattedDelta = formatPrice(parseFloat(suggestion.priceDelta), { currency }) as string;
|
|
47
53
|
|
|
48
54
|
function handleDismiss() {
|
|
49
55
|
try {
|
|
50
56
|
sessionStorage.setItem(storageKey, '1');
|
|
51
|
-
} catch {
|
|
57
|
+
} catch {
|
|
58
|
+
/* ignore */
|
|
59
|
+
}
|
|
52
60
|
setDismissed(true);
|
|
53
61
|
}
|
|
54
62
|
|
|
@@ -81,7 +89,13 @@ export function CartUpgradeBanner({
|
|
|
81
89
|
className="text-muted-foreground hover:text-foreground absolute end-2 top-2 text-xs"
|
|
82
90
|
aria-label={t('dismissUpgrade')}
|
|
83
91
|
>
|
|
84
|
-
<svg
|
|
92
|
+
<svg
|
|
93
|
+
className="h-4 w-4"
|
|
94
|
+
fill="none"
|
|
95
|
+
viewBox="0 0 24 24"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
strokeWidth={2}
|
|
98
|
+
>
|
|
85
99
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
86
100
|
</svg>
|
|
87
101
|
</button>
|
|
@@ -93,7 +107,12 @@ export function CartUpgradeBanner({
|
|
|
93
107
|
) : (
|
|
94
108
|
<div className="text-muted-foreground flex h-full w-full items-center justify-center">
|
|
95
109
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
96
|
-
<path
|
|
110
|
+
<path
|
|
111
|
+
strokeLinecap="round"
|
|
112
|
+
strokeLinejoin="round"
|
|
113
|
+
strokeWidth={1.5}
|
|
114
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
115
|
+
/>
|
|
97
116
|
</svg>
|
|
98
117
|
</div>
|
|
99
118
|
)}
|
|
@@ -33,12 +33,7 @@ export function FreeShippingBar({ className }: FreeShippingBarProps) {
|
|
|
33
33
|
<div className={cn('rounded-lg border border-green-200 bg-green-50 p-3', className)}>
|
|
34
34
|
<div className="flex items-center gap-2 text-sm font-medium text-green-700">
|
|
35
35
|
<svg className="h-4 w-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
36
|
-
<path
|
|
37
|
-
strokeLinecap="round"
|
|
38
|
-
strokeLinejoin="round"
|
|
39
|
-
strokeWidth={2}
|
|
40
|
-
d="M5 13l4 4L19 7"
|
|
41
|
-
/>
|
|
36
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
42
37
|
</svg>
|
|
43
38
|
{t('freeShippingQualified')}
|
|
44
39
|
</div>
|
|
@@ -22,7 +22,11 @@ export function OrderBumpCard({ bump, isAdded, onToggle, loading, className }: O
|
|
|
22
22
|
|
|
23
23
|
const product = bump.bumpProduct;
|
|
24
24
|
const firstImage = product.images?.[0];
|
|
25
|
-
const imageUrl = firstImage
|
|
25
|
+
const imageUrl = firstImage
|
|
26
|
+
? typeof firstImage === 'string'
|
|
27
|
+
? firstImage
|
|
28
|
+
: firstImage.url
|
|
29
|
+
: null;
|
|
26
30
|
const originalPrice = parseFloat(bump.originalPrice);
|
|
27
31
|
const hasDiscount = bump.discountedPrice != null;
|
|
28
32
|
const discountedPrice = hasDiscount ? parseFloat(bump.discountedPrice!) : null;
|
|
@@ -59,8 +59,18 @@ function ProductThumb({
|
|
|
59
59
|
<Image src={imageUrl} alt={name} fill sizes="80px" className="object-cover" />
|
|
60
60
|
) : (
|
|
61
61
|
<div className="flex h-full w-full items-center justify-center">
|
|
62
|
-
<svg
|
|
63
|
-
|
|
62
|
+
<svg
|
|
63
|
+
className="text-muted-foreground h-8 w-8"
|
|
64
|
+
fill="none"
|
|
65
|
+
viewBox="0 0 24 24"
|
|
66
|
+
stroke="currentColor"
|
|
67
|
+
>
|
|
68
|
+
<path
|
|
69
|
+
strokeLinecap="round"
|
|
70
|
+
strokeLinejoin="round"
|
|
71
|
+
strokeWidth={1.5}
|
|
72
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
73
|
+
/>
|
|
64
74
|
</svg>
|
|
65
75
|
</div>
|
|
66
76
|
)}
|
|
@@ -137,7 +147,9 @@ export function FrequentlyBoughtTogether({
|
|
|
137
147
|
|
|
138
148
|
return (
|
|
139
149
|
<div className={cn('border-border rounded-lg border p-6', className)}>
|
|
140
|
-
<h2 className="text-foreground mb-4 text-xl font-semibold">
|
|
150
|
+
<h2 className="text-foreground mb-4 text-xl font-semibold">
|
|
151
|
+
{t('frequentlyBoughtTogether')}
|
|
152
|
+
</h2>
|
|
141
153
|
|
|
142
154
|
<div className="flex flex-wrap items-center gap-3">
|
|
143
155
|
{/* Current product (always included, no checkbox) */}
|