create-brainerce-store 1.8.1 → 1.9.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/messages/en.json +6 -2
- package/messages/he.json +6 -2
- package/package.json +1 -1
- package/templates/nextjs/base/src/app/cart/page.tsx +24 -0
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +32 -1
- package/templates/nextjs/base/src/components/products/recommendation-section.tsx +110 -0
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.9.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/messages/en.json
CHANGED
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
"addedToCart": "Added to Cart!",
|
|
75
75
|
"outOfStock": "Out of Stock",
|
|
76
76
|
"decreaseQuantity": "Decrease quantity",
|
|
77
|
-
"increaseQuantity": "Increase quantity"
|
|
77
|
+
"increaseQuantity": "Increase quantity",
|
|
78
|
+
"youMayAlsoLike": "You May Also Like",
|
|
79
|
+
"similarProducts": "Similar Products",
|
|
80
|
+
"upgradeYourChoice": "Upgrade Your Choice"
|
|
78
81
|
},
|
|
79
82
|
"cart": {
|
|
80
83
|
"pageTitle": "Shopping Cart",
|
|
@@ -83,7 +86,8 @@
|
|
|
83
86
|
"emptySubtitle": "Looks like you haven't added anything to your cart yet.",
|
|
84
87
|
"proceedToCheckout": "Proceed to Checkout",
|
|
85
88
|
"orderSummary": "Order Summary",
|
|
86
|
-
"taxAtCheckout": "Calculated at checkout"
|
|
89
|
+
"taxAtCheckout": "Calculated at checkout",
|
|
90
|
+
"youMightAlsoNeed": "You Might Also Need"
|
|
87
91
|
},
|
|
88
92
|
"checkout": {
|
|
89
93
|
"pageTitle": "Checkout",
|
package/messages/he.json
CHANGED
|
@@ -74,7 +74,10 @@
|
|
|
74
74
|
"addedToCart": "נוסף לעגלה!",
|
|
75
75
|
"outOfStock": "אזל מהמלאי",
|
|
76
76
|
"decreaseQuantity": "הפחת כמות",
|
|
77
|
-
"increaseQuantity": "הגדל כמות"
|
|
77
|
+
"increaseQuantity": "הגדל כמות",
|
|
78
|
+
"youMayAlsoLike": "אולי גם תאהבו",
|
|
79
|
+
"similarProducts": "מוצרים דומים",
|
|
80
|
+
"upgradeYourChoice": "שדרגו את הבחירה"
|
|
78
81
|
},
|
|
79
82
|
"cart": {
|
|
80
83
|
"pageTitle": "עגלת קניות",
|
|
@@ -83,7 +86,8 @@
|
|
|
83
86
|
"emptySubtitle": "נראה שעדיין לא הוספת מוצרים לעגלה.",
|
|
84
87
|
"proceedToCheckout": "המשך לתשלום",
|
|
85
88
|
"orderSummary": "סיכום הזמנה",
|
|
86
|
-
"taxAtCheckout": "יחושב בקופה"
|
|
89
|
+
"taxAtCheckout": "יחושב בקופה",
|
|
90
|
+
"youMightAlsoNeed": "אולי גם תצטרכו"
|
|
87
91
|
},
|
|
88
92
|
"checkout": {
|
|
89
93
|
"pageTitle": "תשלום",
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
3
4
|
import Link from 'next/link';
|
|
5
|
+
import type { CartRecommendationsResponse } from 'brainerce';
|
|
6
|
+
import { getClient } from '@/lib/brainerce';
|
|
4
7
|
import { useCart } from '@/providers/store-provider';
|
|
5
8
|
import { CartItem } from '@/components/cart/cart-item';
|
|
6
9
|
import { CartSummary } from '@/components/cart/cart-summary';
|
|
7
10
|
import { CouponInput } from '@/components/cart/coupon-input';
|
|
8
11
|
import { CartNudges } from '@/components/cart/cart-nudges';
|
|
9
12
|
import { ReservationCountdown } from '@/components/cart/reservation-countdown';
|
|
13
|
+
import { CartRecommendationSection } from '@/components/products/recommendation-section';
|
|
10
14
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
11
15
|
import { useTranslations } from '@/lib/translations';
|
|
12
16
|
|
|
@@ -14,6 +18,17 @@ export default function CartPage() {
|
|
|
14
18
|
const { cart, cartLoading, refreshCart, itemCount } = useCart();
|
|
15
19
|
const t = useTranslations('cart');
|
|
16
20
|
const tc = useTranslations('common');
|
|
21
|
+
const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
|
|
22
|
+
|
|
23
|
+
// Load cross-sell recommendations when cart changes
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!cart?.id || cart.items.length === 0) {
|
|
26
|
+
setCartRecs(null);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const client = getClient();
|
|
30
|
+
client.getCartRecommendations(cart.id, 4).then(setCartRecs).catch(() => {});
|
|
31
|
+
}, [cart?.id, cart?.items.length]);
|
|
17
32
|
|
|
18
33
|
if (cartLoading) {
|
|
19
34
|
return (
|
|
@@ -105,6 +120,15 @@ export default function CartPage() {
|
|
|
105
120
|
</div>
|
|
106
121
|
</div>
|
|
107
122
|
</div>
|
|
123
|
+
|
|
124
|
+
{/* Cross-sell recommendations */}
|
|
125
|
+
{cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
|
|
126
|
+
<CartRecommendationSection
|
|
127
|
+
title={t('youMightAlsoNeed')}
|
|
128
|
+
items={cartRecs.recommendations}
|
|
129
|
+
className="mt-10"
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
108
132
|
</div>
|
|
109
133
|
);
|
|
110
134
|
}
|
|
@@ -4,7 +4,13 @@ import { useEffect, useState, useMemo } from 'react';
|
|
|
4
4
|
import { useParams } from 'next/navigation';
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import Link from 'next/link';
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
Product,
|
|
9
|
+
ProductVariant,
|
|
10
|
+
ProductImage,
|
|
11
|
+
ProductMetafield,
|
|
12
|
+
ProductRecommendationsResponse,
|
|
13
|
+
} from 'brainerce';
|
|
8
14
|
import { getProductPriceInfo, getDescriptionContent } from 'brainerce';
|
|
9
15
|
import { getClient } from '@/lib/brainerce';
|
|
10
16
|
import { useCart } from '@/providers/store-provider';
|
|
@@ -12,6 +18,7 @@ import { PriceDisplay } from '@/components/shared/price-display';
|
|
|
12
18
|
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
13
19
|
import { VariantSelector } from '@/components/products/variant-selector';
|
|
14
20
|
import { StockBadge } from '@/components/products/stock-badge';
|
|
21
|
+
import { RecommendationSection } from '@/components/products/recommendation-section';
|
|
15
22
|
import { useTranslations } from '@/lib/translations';
|
|
16
23
|
import { cn } from '@/lib/utils';
|
|
17
24
|
|
|
@@ -113,6 +120,9 @@ export default function ProductDetailPage() {
|
|
|
113
120
|
const [quantity, setQuantity] = useState(1);
|
|
114
121
|
const [addingToCart, setAddingToCart] = useState(false);
|
|
115
122
|
const [addedMessage, setAddedMessage] = useState(false);
|
|
123
|
+
const [recommendations, setRecommendations] = useState<ProductRecommendationsResponse | null>(
|
|
124
|
+
null
|
|
125
|
+
);
|
|
116
126
|
|
|
117
127
|
// Load product
|
|
118
128
|
useEffect(() => {
|
|
@@ -128,6 +138,9 @@ export default function ProductDetailPage() {
|
|
|
128
138
|
if (p.variants && p.variants.length > 0) {
|
|
129
139
|
setSelectedVariant(p.variants[0]);
|
|
130
140
|
}
|
|
141
|
+
|
|
142
|
+
// Load recommendations in background
|
|
143
|
+
client.getProductRecommendations(p.id).then(setRecommendations).catch(() => {});
|
|
131
144
|
} catch {
|
|
132
145
|
setError(t('notFound'));
|
|
133
146
|
} finally {
|
|
@@ -430,6 +443,24 @@ export default function ProductDetailPage() {
|
|
|
430
443
|
)}
|
|
431
444
|
</div>
|
|
432
445
|
</div>
|
|
446
|
+
|
|
447
|
+
{/* Upsells — premium alternatives (product page) */}
|
|
448
|
+
{recommendations?.upsells && recommendations.upsells.length > 0 && (
|
|
449
|
+
<RecommendationSection
|
|
450
|
+
title={t('upgradeYourChoice')}
|
|
451
|
+
items={recommendations.upsells}
|
|
452
|
+
className="mt-12"
|
|
453
|
+
/>
|
|
454
|
+
)}
|
|
455
|
+
|
|
456
|
+
{/* Related products — similar items (bottom of product page) */}
|
|
457
|
+
{recommendations?.related && recommendations.related.length > 0 && (
|
|
458
|
+
<RecommendationSection
|
|
459
|
+
title={t('similarProducts')}
|
|
460
|
+
items={recommendations.related}
|
|
461
|
+
className="mt-12"
|
|
462
|
+
/>
|
|
463
|
+
)}
|
|
433
464
|
</div>
|
|
434
465
|
);
|
|
435
466
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import Image from 'next/image';
|
|
6
|
+
import type { ProductRecommendation } from 'brainerce';
|
|
7
|
+
import { PriceDisplay } from '@/components/shared/price-display';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface RecommendationCardProps {
|
|
11
|
+
item: ProductRecommendation;
|
|
12
|
+
className?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function RecommendationCard({ item, className }: RecommendationCardProps) {
|
|
16
|
+
const imageUrl = item.images?.[0]?.url || null;
|
|
17
|
+
const slug = item.slug || item.id;
|
|
18
|
+
const basePrice = parseFloat(item.basePrice);
|
|
19
|
+
const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
|
|
20
|
+
const isOnSale = salePrice != null && salePrice < basePrice;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Link
|
|
24
|
+
href={`/products/${slug}`}
|
|
25
|
+
className={cn(
|
|
26
|
+
'border-border bg-background group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
<div className="bg-muted relative aspect-square overflow-hidden">
|
|
31
|
+
{imageUrl ? (
|
|
32
|
+
<Image
|
|
33
|
+
src={imageUrl}
|
|
34
|
+
alt={item.name}
|
|
35
|
+
fill
|
|
36
|
+
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
|
|
37
|
+
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
|
38
|
+
/>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
|
|
41
|
+
<svg className="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
42
|
+
<path
|
|
43
|
+
strokeLinecap="round"
|
|
44
|
+
strokeLinejoin="round"
|
|
45
|
+
strokeWidth={1.5}
|
|
46
|
+
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"
|
|
47
|
+
/>
|
|
48
|
+
</svg>
|
|
49
|
+
</div>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
<div className="space-y-1.5 p-3">
|
|
53
|
+
<h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
|
|
54
|
+
{item.name}
|
|
55
|
+
</h3>
|
|
56
|
+
<PriceDisplay
|
|
57
|
+
price={basePrice}
|
|
58
|
+
salePrice={isOnSale ? salePrice : undefined}
|
|
59
|
+
size="sm"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
</Link>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface RecommendationSectionProps {
|
|
67
|
+
title: string;
|
|
68
|
+
items: ProductRecommendation[];
|
|
69
|
+
className?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
|
|
73
|
+
if (items.length === 0) return null;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={cn('', className)}>
|
|
77
|
+
<h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
|
|
78
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
|
79
|
+
{items.map((item) => (
|
|
80
|
+
<RecommendationCard key={item.id} item={item} />
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CartRecommendationSectionProps {
|
|
88
|
+
title: string;
|
|
89
|
+
items: ProductRecommendation[];
|
|
90
|
+
className?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function CartRecommendationSection({
|
|
94
|
+
title,
|
|
95
|
+
items,
|
|
96
|
+
className,
|
|
97
|
+
}: CartRecommendationSectionProps) {
|
|
98
|
+
if (items.length === 0) return null;
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className={cn('', className)}>
|
|
102
|
+
<h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
|
|
103
|
+
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
|
104
|
+
{items.map((item) => (
|
|
105
|
+
<RecommendationCard key={item.id} item={item} />
|
|
106
|
+
))}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|