create-brainerce-store 1.78.0 → 1.79.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.
Files changed (31) hide show
  1. package/dist/index.js +12 -2
  2. package/messages/en.json +8 -1
  3. package/messages/he.json +8 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/.eslintrc.json +2 -0
  6. package/templates/nextjs/base/AGENTS.md.ejs +23 -5
  7. package/templates/nextjs/base/CLAUDE.md.ejs +23 -5
  8. package/templates/nextjs/base/src/app/checkout/page.tsx +19 -2
  9. package/templates/nextjs/base/src/app/order-status/page.tsx +18 -3
  10. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +220 -214
  11. package/templates/nextjs/base/src/components/account/order-history.tsx +11 -4
  12. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -2
  13. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +343 -328
  14. package/templates/nextjs/base/src/core/lib/kit.ts +88 -0
  15. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +87 -12
  16. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +205 -197
  17. package/templates/nextjs/base/src/ui/product/product-card.tsx +230 -221
  18. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +524 -493
  19. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +117 -108
  20. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +23 -3
  21. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +210 -202
  22. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +251 -242
  23. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +540 -509
  24. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +110 -101
  25. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +22 -2
  26. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +41 -11
  27. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +182 -174
  28. package/templates/nextjs/ui-canvas/product/product-card.tsx +174 -165
  29. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +31 -0
  30. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +114 -105
  31. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +22 -2
@@ -1,108 +1,117 @@
1
- 'use client';
2
-
3
- import { Image as ImageIcon } from 'lucide-react';
4
- import { Link } from '@/core/lib/navigation';
5
- import { CdnImage as Image } from '@/ui/shared/cdn-image';
6
- import type { ProductRecommendation } from 'brainerce';
7
- import { PriceDisplay } from '@/ui/product/price-display';
8
- import { cn } from '@/core/lib/utils';
9
-
10
- interface RecommendationCardProps {
11
- item: ProductRecommendation;
12
- className?: string;
13
- }
14
-
15
- function RecommendationCard({ item, className }: RecommendationCardProps) {
16
- const firstImage = item.images?.[0];
17
- const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
18
- const slug = item.slug || item.id;
19
- // ⛔ NOT region-converted, and it cannot be. `ProductRecommendation` is a
20
- // trimmed shape that carries `basePrice` / `salePrice` only: the backend
21
- // attaches no displayPrice/displayCurrency to it, so there is no converted
22
- // amount to render and converting here would mean inventing an FX rate.
23
- // These tiles stay in the store currency on a multi-region store. Do not
24
- // "fix" it with a client-side conversion.
25
- const basePrice = parseFloat(item.basePrice);
26
- const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
27
- const isOnSale = salePrice != null && salePrice < basePrice;
28
-
29
- return (
30
- <Link
31
- href={`/products/${slug}`}
32
- className={cn(
33
- // Card-equivalent shell (bg-card + border + rounded-lg) as a link —
34
- // the whole recommendation tile is clickable.
35
- 'bg-card text-card-foreground group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
36
- className
37
- )}
38
- >
39
- <div className="bg-muted relative aspect-square overflow-hidden">
40
- {imageUrl ? (
41
- <Image
42
- src={imageUrl}
43
- alt={item.name}
44
- fill
45
- sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
46
- className="object-cover transition-transform duration-300 group-hover:scale-105"
47
- />
48
- ) : (
49
- <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
50
- <ImageIcon className="h-10 w-10" strokeWidth={1.5} aria-hidden="true" />
51
- </div>
52
- )}
53
- </div>
54
- <div className="space-y-1.5 p-3">
55
- <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
56
- {item.name}
57
- </h3>
58
- <PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
59
- </div>
60
- </Link>
61
- );
62
- }
63
-
64
- interface RecommendationSectionProps {
65
- title: string;
66
- items: ProductRecommendation[];
67
- className?: string;
68
- }
69
-
70
- export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
71
- if (items.length === 0) return null;
72
-
73
- return (
74
- <div className={cn('', className)}>
75
- <h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
76
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
77
- {items.map((item) => (
78
- <RecommendationCard key={item.id} item={item} />
79
- ))}
80
- </div>
81
- </div>
82
- );
83
- }
84
-
85
- interface CartRecommendationSectionProps {
86
- title: string;
87
- items: ProductRecommendation[];
88
- className?: string;
89
- }
90
-
91
- export function CartRecommendationSection({
92
- title,
93
- items,
94
- className,
95
- }: CartRecommendationSectionProps) {
96
- if (items.length === 0) return null;
97
-
98
- return (
99
- <div className={cn('', className)}>
100
- <h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
101
- <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
102
- {items.map((item) => (
103
- <RecommendationCard key={item.id} item={item} />
104
- ))}
105
- </div>
106
- </div>
107
- );
108
- }
1
+ 'use client';
2
+
3
+ import { Image as ImageIcon } from 'lucide-react';
4
+ import { Link } from '@/core/lib/navigation';
5
+ import { CdnImage as Image } from '@/ui/shared/cdn-image';
6
+ import type { ProductRecommendation } from 'brainerce';
7
+ import { PriceDisplay } from '@/ui/product/price-display';
8
+ import { cn } from '@/core/lib/utils';
9
+
10
+ interface RecommendationCardProps {
11
+ item: ProductRecommendation;
12
+ className?: string;
13
+ }
14
+
15
+ function RecommendationCard({ item, className }: RecommendationCardProps) {
16
+ const firstImage = item.images?.[0];
17
+ const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
18
+ const slug = item.slug || item.id;
19
+ // ⛔ NOT region-converted, and it cannot be. `ProductRecommendation` is a
20
+ // trimmed shape that carries `basePrice` / `salePrice` only: the backend
21
+ // attaches no displayPrice/displayCurrency to it, so there is no converted
22
+ // amount to render and converting here would mean inventing an FX rate.
23
+ // These tiles stay in the store currency on a multi-region store. Do not
24
+ // "fix" it with a client-side conversion.
25
+ const basePrice = parseFloat(item.basePrice);
26
+ const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
27
+ const isOnSale = salePrice != null && salePrice < basePrice;
28
+ // ⛔ No price on a KIT tile. The recommendations endpoint does not resolve
29
+ // kits, so `item.basePrice` here is the kit's stored PLACEHOLDER — right only
30
+ // for FIXED pricing and wrong for every SUM / SUM_MINUS_PERCENT kit. Showing
31
+ // it would quote a price the product page then contradicts. The tile still
32
+ // links through, and the product page reads the resolved price from the API.
33
+ // Do NOT recompute a kit price on the client.
34
+ const priceIsUnresolved = item.type === 'KIT';
35
+
36
+ return (
37
+ <Link
38
+ href={`/products/${slug}`}
39
+ className={cn(
40
+ // Card-equivalent shell (bg-card + border + rounded-lg) as a link —
41
+ // the whole recommendation tile is clickable.
42
+ 'bg-card text-card-foreground group block overflow-hidden rounded-lg border transition-shadow hover:shadow-md',
43
+ className
44
+ )}
45
+ >
46
+ <div className="bg-muted relative aspect-square overflow-hidden">
47
+ {imageUrl ? (
48
+ <Image
49
+ src={imageUrl}
50
+ alt={item.name}
51
+ fill
52
+ sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
53
+ className="object-cover transition-transform duration-300 group-hover:scale-105"
54
+ />
55
+ ) : (
56
+ <div className="text-muted-foreground absolute inset-0 flex items-center justify-center">
57
+ <ImageIcon className="h-10 w-10" strokeWidth={1.5} aria-hidden="true" />
58
+ </div>
59
+ )}
60
+ </div>
61
+ <div className="space-y-1.5 p-3">
62
+ <h3 className="text-foreground group-hover:text-primary line-clamp-2 text-sm font-medium transition-colors">
63
+ {item.name}
64
+ </h3>
65
+ {priceIsUnresolved ? null : (
66
+ <PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
67
+ )}
68
+ </div>
69
+ </Link>
70
+ );
71
+ }
72
+
73
+ interface RecommendationSectionProps {
74
+ title: string;
75
+ items: ProductRecommendation[];
76
+ className?: string;
77
+ }
78
+
79
+ export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
80
+ if (items.length === 0) return null;
81
+
82
+ return (
83
+ <div className={cn('', className)}>
84
+ <h2 className="text-foreground mb-4 text-xl font-semibold">{title}</h2>
85
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
86
+ {items.map((item) => (
87
+ <RecommendationCard key={item.id} item={item} />
88
+ ))}
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ interface CartRecommendationSectionProps {
95
+ title: string;
96
+ items: ProductRecommendation[];
97
+ className?: string;
98
+ }
99
+
100
+ export function CartRecommendationSection({
101
+ title,
102
+ items,
103
+ className,
104
+ }: CartRecommendationSectionProps) {
105
+ if (items.length === 0) return null;
106
+
107
+ return (
108
+ <div className={cn('', className)}>
109
+ <h2 className="text-foreground mb-4 text-lg font-semibold">{title}</h2>
110
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
111
+ {items.map((item) => (
112
+ <RecommendationCard key={item.id} item={item} />
113
+ ))}
114
+ </div>
115
+ </div>
116
+ );
117
+ }
@@ -6,9 +6,20 @@ import { cn } from '@/core/lib/utils';
6
6
  import { useTranslations } from '@/core/lib/translations';
7
7
  import { useStoreCapabilities } from '@/core/providers/store-provider';
8
8
  import { resolveLowStockThreshold } from '@/core/lib/capabilities';
9
+ import { kitInventory } from '@/core/lib/kit';
9
10
 
10
11
  interface StockBadgeProps {
11
12
  inventory: InventoryInfo | null | undefined;
13
+ /**
14
+ * KIT stock — pass `product.kitAvailable` whenever the product may be a kit.
15
+ *
16
+ * ⛔ A KIT has NO `inventory` block, so passing `inventory` alone sent every
17
+ * kit down the `!inventory` branch below and printed a red "Out of stock" on
18
+ * every card, category page, search result and product page — while the buy
19
+ * button beside it stayed enabled. `undefined` here means "not a kit";
20
+ * `null` means "unlimited". They are not interchangeable.
21
+ */
22
+ kitAvailable?: number | null;
12
23
  /**
13
24
  * Override the threshold. Leave it unset — the merchant's configured value
14
25
  * comes from the store's capabilities, and a hardcoded number here shows the
@@ -18,15 +29,22 @@ interface StockBadgeProps {
18
29
  className?: string;
19
30
  }
20
31
 
21
- export function StockBadge({ inventory, lowStockThreshold, className }: StockBadgeProps) {
32
+ export function StockBadge({
33
+ inventory,
34
+ kitAvailable,
35
+ lowStockThreshold,
36
+ className,
37
+ }: StockBadgeProps) {
22
38
  const t = useTranslations('productDetail');
23
39
  // Merchant-configured, from the channel's capabilities. Falls back to the
24
40
  // platform default while the fetch is in flight or if it failed, and resolves
25
41
  // to 0 (nothing is ever low) when the merchant turned low-stock warnings off.
26
42
  const { capabilities } = useStoreCapabilities();
27
43
  const threshold = lowStockThreshold ?? resolveLowStockThreshold(capabilities);
28
- const label = getStockLabel(inventory, threshold, t);
29
- const color = getStockColor(inventory, threshold);
44
+ // A kit reports from `kitAvailable`; everything else from its inventory row.
45
+ const stock = kitAvailable !== undefined ? kitInventory(kitAvailable) : inventory;
46
+ const label = getStockLabel(stock, threshold, t);
47
+ const color = getStockColor(stock, threshold);
30
48
 
31
49
  return (
32
50
  <Badge
@@ -43,6 +61,8 @@ function getStockLabel(
43
61
  lowStockThreshold: number,
44
62
  t: (key: string) => string
45
63
  ): string {
64
+ // No stock signal at all. A KIT reaches here only when the caller forgot to
65
+ // pass `kitAvailable` — see the prop comment above.
46
66
  if (!inventory) return t('outOfStock');
47
67
 
48
68
  const { trackingMode, inStock, available } = inventory;