create-brainerce-store 1.72.0 → 1.74.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 +37 -4
- package/messages/en.json +30 -1
- package/messages/he.json +30 -1
- package/package.json +1 -1
- package/templates/nextjs/base/AGENTS.md.ejs +63 -16
- package/templates/nextjs/base/AI-GUIDE.md +17 -4
- package/templates/nextjs/base/CLAUDE.md.ejs +67 -20
- package/templates/nextjs/base/TRANSLATIONS.md +207 -207
- package/templates/nextjs/base/src/app/account/page.tsx +168 -122
- package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
- package/templates/nextjs/base/src/app/checkout/page.tsx +1179 -1074
- package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
- package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
- package/templates/nextjs/base/src/components/account/order-history.tsx +422 -371
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
- package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +218 -127
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
- package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
- package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
- package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
- package/templates/nextjs/base/src/core/lib/region.ts +112 -0
- package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +174 -164
- package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +219 -176
- package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
- package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
- package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
- package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
- package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
- package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
- package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
- package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
- package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
- package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
- package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
- package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
- package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
- package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +168 -158
- package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
- package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +229 -184
- package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
- package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
- package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
- package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
- package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
- package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
- package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
- package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
- package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
- package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
- package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
- package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
- package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
- package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
- package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
- package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
- package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
- package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
- package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
- package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
|
@@ -1,99 +1,105 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Link } from '@/core/lib/navigation';
|
|
4
|
-
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
-
import type { ProductRecommendation } from 'brainerce';
|
|
6
|
-
import { PriceDisplay } from '@/ui/product/price-display';
|
|
7
|
-
import { cn } from '@/core/lib/utils';
|
|
8
|
-
|
|
9
|
-
interface RecommendationCardProps {
|
|
10
|
-
item: ProductRecommendation;
|
|
11
|
-
className?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function RecommendationCard({ item, className }: RecommendationCardProps) {
|
|
15
|
-
const firstImage = item.images?.[0];
|
|
16
|
-
const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
|
|
17
|
-
const slug = item.slug || item.id;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Link } from '@/core/lib/navigation';
|
|
4
|
+
import { CdnImage as Image } from '@/ui/shared/cdn-image';
|
|
5
|
+
import type { ProductRecommendation } from 'brainerce';
|
|
6
|
+
import { PriceDisplay } from '@/ui/product/price-display';
|
|
7
|
+
import { cn } from '@/core/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface RecommendationCardProps {
|
|
10
|
+
item: ProductRecommendation;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function RecommendationCard({ item, className }: RecommendationCardProps) {
|
|
15
|
+
const firstImage = item.images?.[0];
|
|
16
|
+
const imageUrl = typeof firstImage === 'string' ? firstImage : firstImage?.url || null;
|
|
17
|
+
const slug = item.slug || item.id;
|
|
18
|
+
// ⛔ NOT region-converted, and it cannot be. `ProductRecommendation` is a
|
|
19
|
+
// trimmed shape that carries `basePrice` / `salePrice` only: the backend
|
|
20
|
+
// attaches no displayPrice/displayCurrency to it, so there is no converted
|
|
21
|
+
// amount to render and converting here would mean inventing an FX rate.
|
|
22
|
+
// These tiles stay in the store currency on a multi-region store. Do not
|
|
23
|
+
// "fix" it with a client-side conversion.
|
|
24
|
+
const basePrice = parseFloat(item.basePrice);
|
|
25
|
+
const salePrice = item.salePrice ? parseFloat(item.salePrice) : undefined;
|
|
26
|
+
const isOnSale = salePrice != null && salePrice < basePrice;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Link href={`/products/${slug}`} className={cn(className)}>
|
|
30
|
+
{/* `relative` + `aspect-square` are layout-critical for next/image fill */}
|
|
31
|
+
<span className="relative block aspect-square">
|
|
32
|
+
{imageUrl ? (
|
|
33
|
+
<Image
|
|
34
|
+
src={imageUrl}
|
|
35
|
+
alt={item.name}
|
|
36
|
+
fill
|
|
37
|
+
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
|
|
38
|
+
/>
|
|
39
|
+
) : null}
|
|
40
|
+
</span>
|
|
41
|
+
<span className="block">
|
|
42
|
+
<span className="block">{item.name}</span>
|
|
43
|
+
<PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
|
|
44
|
+
</span>
|
|
45
|
+
</Link>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface RecommendationSectionProps {
|
|
50
|
+
title: string;
|
|
51
|
+
items: ProductRecommendation[];
|
|
52
|
+
className?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* DESIGN ME — recommendation strip on the product page (upsells / related);
|
|
57
|
+
* items come from useProductPage().recommendations, title is passed in.
|
|
58
|
+
*
|
|
59
|
+
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
60
|
+
*/
|
|
61
|
+
export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
|
|
62
|
+
if (items.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<section className={cn(className)}>
|
|
66
|
+
<h2>{title}</h2>
|
|
67
|
+
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
68
|
+
{items.map((item) => (
|
|
69
|
+
<RecommendationCard key={item.id} item={item} />
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface CartRecommendationSectionProps {
|
|
77
|
+
title: string;
|
|
78
|
+
items: ProductRecommendation[];
|
|
79
|
+
className?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* DESIGN ME — cross-sell strip on the cart page ("you might also need");
|
|
84
|
+
* items come from useCartPage().cartRecs.
|
|
85
|
+
*
|
|
86
|
+
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
87
|
+
*/
|
|
88
|
+
export function CartRecommendationSection({
|
|
89
|
+
title,
|
|
90
|
+
items,
|
|
91
|
+
className,
|
|
92
|
+
}: CartRecommendationSectionProps) {
|
|
93
|
+
if (items.length === 0) return null;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<section className={cn(className)}>
|
|
97
|
+
<h2>{title}</h2>
|
|
98
|
+
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
|
99
|
+
{items.map((item) => (
|
|
100
|
+
<RecommendationCard key={item.id} item={item} />
|
|
101
|
+
))}
|
|
102
|
+
</div>
|
|
103
|
+
</section>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
@@ -323,7 +323,15 @@ function ReviewEditor({
|
|
|
323
323
|
</label>
|
|
324
324
|
|
|
325
325
|
{/* DESIGN ME — review photo picker. Renders only when the store allows
|
|
326
|
-
photos; every limit comes from `photos`, never hard-coded.
|
|
326
|
+
photos; every limit comes from `photos`, never hard-coded.
|
|
327
|
+
Two things to get right when you style it. Wrap the input in a <label>
|
|
328
|
+
and hide the input with sr-only: the browser's own "Choose Files" button
|
|
329
|
+
cannot be styled and will look nothing like the rest of your form. Then
|
|
330
|
+
paint your own disabled state, because `disabled` on a hidden input greys
|
|
331
|
+
NOTHING — at photos.maxPerReview the control would look alive and do
|
|
332
|
+
nothing. Say why it is disabled. Keep it a real input (no ref.click()) so
|
|
333
|
+
keyboard and assistive tech reach it, and add focus-within so the focus
|
|
334
|
+
ring the native control gave you for free survives. */}
|
|
327
335
|
{photos.enabled && (
|
|
328
336
|
<fieldset>
|
|
329
337
|
<legend>
|
|
@@ -3,9 +3,16 @@
|
|
|
3
3
|
import type { InventoryInfo } from 'brainerce';
|
|
4
4
|
import { cn } from '@/core/lib/utils';
|
|
5
5
|
import { useTranslations } from '@/core/lib/translations';
|
|
6
|
+
import { useStoreCapabilities } from '@/core/providers/store-provider';
|
|
7
|
+
import { resolveLowStockThreshold } from '@/core/lib/capabilities';
|
|
6
8
|
|
|
7
9
|
interface StockBadgeProps {
|
|
8
10
|
inventory: InventoryInfo | null | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Override the threshold. Leave it unset — the merchant's configured value
|
|
13
|
+
* comes from the store's capabilities, and a hardcoded number here shows the
|
|
14
|
+
* wrong badge on every store that chose a different one.
|
|
15
|
+
*/
|
|
9
16
|
lowStockThreshold?: number;
|
|
10
17
|
className?: string;
|
|
11
18
|
}
|
|
@@ -17,9 +24,17 @@ interface StockBadgeProps {
|
|
|
17
24
|
*
|
|
18
25
|
* Building blocks: shadcn/ui primitives in src/components/ui (Button, Card, Badge, Input, Select, Accordion, Dialog, Skeleton, ...) + lucide-react icons are installed and ready to compose with.
|
|
19
26
|
*/
|
|
20
|
-
export function StockBadge({ inventory, lowStockThreshold
|
|
27
|
+
export function StockBadge({ inventory, lowStockThreshold, className }: StockBadgeProps) {
|
|
21
28
|
const t = useTranslations('productDetail');
|
|
22
|
-
|
|
29
|
+
// Merchant-configured, from the channel's capabilities. Falls back to the
|
|
30
|
+
// platform default while the fetch is in flight or if it failed, and resolves
|
|
31
|
+
// to 0 (nothing is ever low) when the merchant turned low-stock warnings off.
|
|
32
|
+
const { capabilities } = useStoreCapabilities();
|
|
33
|
+
const label = getStockLabel(
|
|
34
|
+
inventory,
|
|
35
|
+
lowStockThreshold ?? resolveLowStockThreshold(capabilities),
|
|
36
|
+
t
|
|
37
|
+
);
|
|
23
38
|
|
|
24
39
|
return <span className={cn(className)}>{label}</span>;
|
|
25
40
|
}
|