create-brainerce-store 1.73.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.
Files changed (77) hide show
  1. package/dist/index.js +35 -2
  2. package/messages/en.json +27 -1
  3. package/messages/he.json +27 -1
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/AGENTS.md.ejs +63 -16
  6. package/templates/nextjs/base/AI-GUIDE.md +17 -4
  7. package/templates/nextjs/base/CLAUDE.md.ejs +67 -20
  8. package/templates/nextjs/base/src/app/account/page.tsx +168 -122
  9. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
  10. package/templates/nextjs/base/src/app/checkout/page.tsx +145 -40
  11. package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
  12. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
  13. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  14. package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
  15. package/templates/nextjs/base/src/components/account/order-history.tsx +58 -7
  16. package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
  17. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
  18. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
  19. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
  20. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +97 -6
  21. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
  22. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
  23. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
  24. package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
  25. package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
  26. package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
  27. package/templates/nextjs/base/src/core/lib/region.ts +112 -0
  28. package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
  29. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
  30. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +10 -0
  31. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
  32. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +45 -2
  33. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
  34. package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
  35. package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
  36. package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
  37. package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
  38. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
  39. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
  40. package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
  41. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
  42. package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
  43. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
  44. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
  45. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
  46. package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
  47. package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
  48. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +10 -0
  49. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
  50. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +56 -11
  51. package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
  52. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
  53. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
  54. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
  55. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
  56. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
  57. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
  58. package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
  59. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
  60. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
  61. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
  62. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
  63. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
  64. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
  65. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
  66. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
  67. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
  68. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
  69. package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
  70. package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
  71. package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
  72. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
  73. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
  74. package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
  75. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
  76. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
  77. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
@@ -98,6 +98,7 @@ export function ProductClientSection({
98
98
  selectedVariant,
99
99
  setSelectedVariant,
100
100
  priceInfo,
101
+ displayPrice,
101
102
  inventory,
102
103
  canPurchase,
103
104
  description,
@@ -203,8 +204,9 @@ export function ProductClientSection({
203
204
 
204
205
  {/* Price */}
205
206
  <PriceDisplay
206
- price={priceInfo.originalPrice}
207
- salePrice={priceInfo.isOnSale ? priceInfo.price : undefined}
207
+ price={displayPrice.price}
208
+ salePrice={displayPrice.salePrice ?? undefined}
209
+ currency={displayPrice.currency}
208
210
  size="lg"
209
211
  />
210
212
 
@@ -212,7 +214,7 @@ export function ProductClientSection({
212
214
  {product.isDownloadable ? (
213
215
  <p>{t('instantDownload')}</p>
214
216
  ) : (
215
- <StockBadge inventory={inventory} lowStockThreshold={5} />
217
+ <StockBadge inventory={inventory} />
216
218
  )}
217
219
 
218
220
  {/* Downloadable files info */}
@@ -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
- 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 href={`/products/${slug}`} className={cn(className)}>
24
- {/* `relative` + `aspect-square` are layout-critical for next/image fill */}
25
- <span className="relative block aspect-square">
26
- {imageUrl ? (
27
- <Image
28
- src={imageUrl}
29
- alt={item.name}
30
- fill
31
- sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
32
- />
33
- ) : null}
34
- </span>
35
- <span className="block">
36
- <span className="block">{item.name}</span>
37
- <PriceDisplay price={basePrice} salePrice={isOnSale ? salePrice : undefined} size="sm" />
38
- </span>
39
- </Link>
40
- );
41
- }
42
-
43
- interface RecommendationSectionProps {
44
- title: string;
45
- items: ProductRecommendation[];
46
- className?: string;
47
- }
48
-
49
- /**
50
- * DESIGN ME — recommendation strip on the product page (upsells / related);
51
- * items come from useProductPage().recommendations, title is passed in.
52
- *
53
- * 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.
54
- */
55
- export function RecommendationSection({ title, items, className }: RecommendationSectionProps) {
56
- if (items.length === 0) return null;
57
-
58
- return (
59
- <section className={cn(className)}>
60
- <h2>{title}</h2>
61
- <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
62
- {items.map((item) => (
63
- <RecommendationCard key={item.id} item={item} />
64
- ))}
65
- </div>
66
- </section>
67
- );
68
- }
69
-
70
- interface CartRecommendationSectionProps {
71
- title: string;
72
- items: ProductRecommendation[];
73
- className?: string;
74
- }
75
-
76
- /**
77
- * DESIGN ME — cross-sell strip on the cart page ("you might also need");
78
- * items come from useCartPage().cartRecs.
79
- *
80
- * 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.
81
- */
82
- export function CartRecommendationSection({
83
- title,
84
- items,
85
- className,
86
- }: CartRecommendationSectionProps) {
87
- if (items.length === 0) return null;
88
-
89
- return (
90
- <section className={cn(className)}>
91
- <h2>{title}</h2>
92
- <div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
93
- {items.map((item) => (
94
- <RecommendationCard key={item.id} item={item} />
95
- ))}
96
- </div>
97
- </section>
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
+ }
@@ -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 = 5, className }: StockBadgeProps) {
27
+ export function StockBadge({ inventory, lowStockThreshold, className }: StockBadgeProps) {
21
28
  const t = useTranslations('productDetail');
22
- const label = getStockLabel(inventory, lowStockThreshold, t);
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
  }