create-brainerce-store 1.8.0 → 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 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.8.0",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-brainerce-store",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
5
5
  "bin": {
6
6
  "create-brainerce-store": "dist/index.js"
@@ -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 { Product, ProductVariant, ProductImage, ProductMetafield } from 'brainerce';
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
+ }