create-brainerce-store 1.78.0 → 1.80.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 +27 -2
- package/messages/en.json +12 -3
- package/messages/he.json +12 -3
- package/package.json +10 -1
- package/templates/nextjs/base/.eslintrc.json +2 -0
- package/templates/nextjs/base/AGENTS.md.ejs +23 -5
- package/templates/nextjs/base/CLAUDE.md.ejs +23 -5
- package/templates/nextjs/base/src/app/checkout/page.tsx +19 -2
- package/templates/nextjs/base/src/app/order-status/page.tsx +18 -3
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +220 -214
- package/templates/nextjs/base/src/components/account/order-history.tsx +11 -4
- package/templates/nextjs/base/src/components/account/profile-section.tsx +7 -1
- package/templates/nextjs/base/src/components/auth/register-form.tsx +18 -1
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -2
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +1 -1
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +343 -328
- package/templates/nextjs/base/src/core/lib/kit.ts +88 -0
- package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +87 -12
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +59 -12
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +205 -197
- package/templates/nextjs/base/src/ui/product/product-card.tsx +230 -221
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +524 -493
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +117 -108
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +23 -3
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +210 -202
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +251 -242
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +540 -509
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +110 -101
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +22 -2
- package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +41 -11
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +50 -8
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +182 -174
- package/templates/nextjs/ui-canvas/product/product-card.tsx +174 -165
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +31 -0
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +114 -105
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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({
|
|
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
|
-
|
|
29
|
-
const
|
|
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;
|