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.
Files changed (88) hide show
  1. package/dist/index.js +37 -4
  2. package/messages/en.json +30 -1
  3. package/messages/he.json +30 -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/TRANSLATIONS.md +207 -207
  9. package/templates/nextjs/base/src/app/account/page.tsx +168 -122
  10. package/templates/nextjs/base/src/app/category/[slug]/page.tsx +10 -1
  11. package/templates/nextjs/base/src/app/checkout/page.tsx +1179 -1074
  12. package/templates/nextjs/base/src/app/layout.tsx.ejs +48 -4
  13. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +98 -3
  14. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +14 -2
  15. package/templates/nextjs/base/src/components/account/loyalty-panel.tsx +226 -0
  16. package/templates/nextjs/base/src/components/account/order-history.tsx +422 -371
  17. package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +85 -85
  18. package/templates/nextjs/base/src/components/account/saved-payment-methods.tsx +104 -0
  19. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +618 -612
  20. package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +354 -306
  21. package/templates/nextjs/base/src/components/checkout/tax-display.tsx +141 -74
  22. package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +218 -127
  23. package/templates/nextjs/base/src/core/hooks/use-home-data.ts +63 -51
  24. package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +379 -295
  25. package/templates/nextjs/base/src/core/hooks/use-product-page.ts +33 -1
  26. package/templates/nextjs/base/src/core/lib/capabilities.ts +65 -0
  27. package/templates/nextjs/base/src/core/lib/display-price.ts +139 -0
  28. package/templates/nextjs/base/src/core/lib/region.server.ts +112 -0
  29. package/templates/nextjs/base/src/core/lib/region.ts +112 -0
  30. package/templates/nextjs/base/src/core/lib/store-info.ts +19 -0
  31. package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +423 -243
  32. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +174 -164
  33. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +109 -110
  34. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +219 -176
  35. package/templates/nextjs/base/src/ui/cart/gift-card-input.tsx +191 -0
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +137 -137
  37. package/templates/nextjs/base/src/ui/cart/tax-estimate-line.tsx +108 -0
  38. package/templates/nextjs/base/src/ui/home/home-client.tsx +27 -29
  39. package/templates/nextjs/base/src/ui/layout/header-search.tsx +249 -0
  40. package/templates/nextjs/base/src/ui/layout/region-switcher.tsx +59 -0
  41. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +171 -152
  42. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +197 -191
  43. package/templates/nextjs/base/src/ui/product/product-card.tsx +25 -52
  44. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +12 -3
  45. package/templates/nextjs/base/src/ui/product/product-listing.tsx +460 -446
  46. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +108 -102
  47. package/templates/nextjs/base/src/ui/product/review-form.tsx +33 -11
  48. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +15 -3
  49. package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +48 -4
  50. package/templates/nextjs/designs/atelier/messages-patch/en.json +21 -25
  51. package/templates/nextjs/designs/atelier/messages-patch/he.json +21 -25
  52. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +177 -181
  53. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +168 -158
  54. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +106 -107
  55. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +229 -184
  56. package/templates/nextjs/designs/atelier/ui/cart/reservation-countdown.tsx +131 -131
  57. package/templates/nextjs/designs/atelier/ui/cart/tax-estimate-line.tsx +108 -0
  58. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +10 -1
  59. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +178 -174
  60. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +4 -6
  61. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +150 -78
  62. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +79 -46
  63. package/templates/nextjs/designs/atelier/ui/layout/header-search.tsx +264 -0
  64. package/templates/nextjs/designs/atelier/ui/layout/region-switcher.tsx +44 -0
  65. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +5 -0
  66. package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +202 -196
  67. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +27 -52
  68. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +12 -3
  69. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +332 -260
  70. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +101 -95
  71. package/templates/nextjs/designs/atelier/ui/product/review-form.tsx +30 -10
  72. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +17 -2
  73. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +9 -0
  74. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +137 -137
  75. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +98 -99
  76. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +140 -140
  77. package/templates/nextjs/ui-canvas/cart/gift-card-input.tsx +142 -0
  78. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +124 -124
  79. package/templates/nextjs/ui-canvas/cart/tax-estimate-line.tsx +104 -0
  80. package/templates/nextjs/ui-canvas/home/home-client.tsx +24 -26
  81. package/templates/nextjs/ui-canvas/layout/region-switcher.tsx +51 -0
  82. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +3 -0
  83. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +174 -168
  84. package/templates/nextjs/ui-canvas/product/product-card.tsx +9 -51
  85. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +5 -3
  86. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +105 -99
  87. package/templates/nextjs/ui-canvas/product/review-form.tsx +9 -1
  88. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +17 -2
@@ -1,85 +1,85 @@
1
- 'use client';
2
-
3
- import type { Order, OrderStatus } from 'brainerce';
4
- import { useTranslations } from '@/core/lib/translations';
5
- import { cn } from '@/core/lib/utils';
6
-
7
- /**
8
- * Message key for every order status the API can return.
9
- *
10
- * The API sends these values UPPERCASE and verbatim (`PENDING`, `ON_HOLD`,
11
- * ...). Never lowercase them before the lookup: a lowercase key silently
12
- * misses and every row falls through to the default label.
13
- */
14
- export type OrderStatusLabelKey =
15
- | 'statusDraft'
16
- | 'statusPending'
17
- | 'statusProcessing'
18
- | 'statusOnHold'
19
- | 'statusPaid'
20
- | 'statusShipped'
21
- | 'statusDelivered'
22
- | 'statusCompleted'
23
- | 'statusFulfilled'
24
- | 'statusCancelled'
25
- | 'statusRefunded'
26
- | 'statusPartiallyRefunded';
27
-
28
- export const ORDER_STATUS_LABEL_KEYS: Record<OrderStatus, OrderStatusLabelKey> = {
29
- DRAFT: 'statusDraft',
30
- PENDING: 'statusPending',
31
- PROCESSING: 'statusProcessing',
32
- ON_HOLD: 'statusOnHold',
33
- PAID: 'statusPaid',
34
- SHIPPED: 'statusShipped',
35
- DELIVERED: 'statusDelivered',
36
- COMPLETED: 'statusCompleted',
37
- FULFILLED: 'statusFulfilled',
38
- CANCELLED: 'statusCancelled',
39
- REFUNDED: 'statusRefunded',
40
- PARTIALLY_REFUNDED: 'statusPartiallyRefunded',
41
- };
42
-
43
- interface OrderStatusTimelineProps {
44
- history: Order['statusHistory'];
45
- className?: string;
46
- }
47
-
48
- export function OrderStatusTimeline({ history, className }: OrderStatusTimelineProps) {
49
- const t = useTranslations('account');
50
- if (!history || history.length === 0) return null;
51
-
52
- return (
53
- <div className={cn('border-border border-t pt-2', className)}>
54
- <p className="text-foreground mb-2 text-sm font-medium">{t('statusTimeline')}</p>
55
- <ol className="space-y-1.5">
56
- {history.map((entry, idx) => {
57
- const key = ORDER_STATUS_LABEL_KEYS[entry.status] || 'statusPending';
58
- const when = new Date(entry.at);
59
- const whenStr = isNaN(when.getTime())
60
- ? entry.at
61
- : when.toLocaleString(undefined, {
62
- year: 'numeric',
63
- month: 'short',
64
- day: 'numeric',
65
- hour: '2-digit',
66
- minute: '2-digit',
67
- });
68
- return (
69
- <li key={`${entry.status}-${idx}`} className="flex items-center gap-2 text-xs">
70
- <span
71
- className={cn(
72
- 'bg-primary inline-block h-2 w-2 flex-shrink-0 rounded-full',
73
- idx === history.length - 1 ? 'opacity-100' : 'opacity-60'
74
- )}
75
- />
76
- <span className="text-foreground font-medium">{t(key)}</span>
77
- <span className="text-muted-foreground">· {whenStr}</span>
78
- {entry.note && <span className="text-muted-foreground truncate">({entry.note})</span>}
79
- </li>
80
- );
81
- })}
82
- </ol>
83
- </div>
84
- );
85
- }
1
+ 'use client';
2
+
3
+ import type { Order, OrderStatus } from 'brainerce';
4
+ import { useTranslations } from '@/core/lib/translations';
5
+ import { cn } from '@/core/lib/utils';
6
+
7
+ /**
8
+ * Message key for every order status the API can return.
9
+ *
10
+ * The API sends these values UPPERCASE and verbatim (`PENDING`, `ON_HOLD`,
11
+ * ...). Never lowercase them before the lookup: a lowercase key silently
12
+ * misses and every row falls through to the default label.
13
+ */
14
+ export type OrderStatusLabelKey =
15
+ | 'statusDraft'
16
+ | 'statusPending'
17
+ | 'statusProcessing'
18
+ | 'statusOnHold'
19
+ | 'statusPaid'
20
+ | 'statusShipped'
21
+ | 'statusDelivered'
22
+ | 'statusCompleted'
23
+ | 'statusFulfilled'
24
+ | 'statusCancelled'
25
+ | 'statusRefunded'
26
+ | 'statusPartiallyRefunded';
27
+
28
+ export const ORDER_STATUS_LABEL_KEYS: Record<OrderStatus, OrderStatusLabelKey> = {
29
+ DRAFT: 'statusDraft',
30
+ PENDING: 'statusPending',
31
+ PROCESSING: 'statusProcessing',
32
+ ON_HOLD: 'statusOnHold',
33
+ PAID: 'statusPaid',
34
+ SHIPPED: 'statusShipped',
35
+ DELIVERED: 'statusDelivered',
36
+ COMPLETED: 'statusCompleted',
37
+ FULFILLED: 'statusFulfilled',
38
+ CANCELLED: 'statusCancelled',
39
+ REFUNDED: 'statusRefunded',
40
+ PARTIALLY_REFUNDED: 'statusPartiallyRefunded',
41
+ };
42
+
43
+ interface OrderStatusTimelineProps {
44
+ history: Order['statusHistory'];
45
+ className?: string;
46
+ }
47
+
48
+ export function OrderStatusTimeline({ history, className }: OrderStatusTimelineProps) {
49
+ const t = useTranslations('account');
50
+ if (!history || history.length === 0) return null;
51
+
52
+ return (
53
+ <div className={cn('border-border border-t pt-2', className)}>
54
+ <p className="text-foreground mb-2 text-sm font-medium">{t('statusTimeline')}</p>
55
+ <ol className="space-y-1.5">
56
+ {history.map((entry, idx) => {
57
+ const key = ORDER_STATUS_LABEL_KEYS[entry.status] || 'statusPending';
58
+ const when = new Date(entry.at);
59
+ const whenStr = isNaN(when.getTime())
60
+ ? entry.at
61
+ : when.toLocaleString(undefined, {
62
+ year: 'numeric',
63
+ month: 'short',
64
+ day: 'numeric',
65
+ hour: '2-digit',
66
+ minute: '2-digit',
67
+ });
68
+ return (
69
+ <li key={`${entry.status}-${idx}`} className="flex items-center gap-2 text-xs">
70
+ <span
71
+ className={cn(
72
+ 'bg-primary inline-block h-2 w-2 flex-shrink-0 rounded-full',
73
+ idx === history.length - 1 ? 'opacity-100' : 'opacity-60'
74
+ )}
75
+ />
76
+ <span className="text-foreground font-medium">{t(key)}</span>
77
+ <span className="text-muted-foreground">· {whenStr}</span>
78
+ {entry.note && <span className="text-muted-foreground truncate">({entry.note})</span>}
79
+ </li>
80
+ );
81
+ })}
82
+ </ol>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * The customer's vaulted payment methods.
5
+ *
6
+ * ⛔ AUTO-HIDES. `getMySavedPaymentMethods()` returns `[]` for everyone who has
7
+ * never vaulted a card, which on a new store is everyone, so the whole section
8
+ * renders null. It also renders null when the call fails or the shopper is
9
+ * logged out. There is no empty state and no error box: a shopper who has no
10
+ * saved cards does not need to be told about a feature they have not used.
11
+ *
12
+ * ⛔ THIS IS NOT ONE-CLICK CHECKOUT, and there is deliberately no "pay with
13
+ * this card" button. The ONLY storefront call that charges a vaulted card is
14
+ * `subscribeToMembership({ planId, savedPaymentTokenId })`. Nothing on the
15
+ * normal checkout accepts a saved token, so a pay-with-saved-card button would
16
+ * be a dead control. This is a read-only list.
17
+ *
18
+ * ⛔ NO CARD DATA IS HERE AND NONE IS AVAILABLE. The response carries display
19
+ * fields only: `paymentMethod`, `brand`, `last4`, `expMonth`, `expYear`,
20
+ * `isDefault`. Every one of those can be null except the first and last.
21
+ *
22
+ * Cards get here by checking out with `createPaymentIntent(checkoutId,
23
+ * { saveCard: true })`, which is honored only when the checkout has a known
24
+ * `customerId` (so never for a guest) and is silently ignored by providers
25
+ * without tokenization (Grow today). Treat vaulting as best-effort and never
26
+ * promise the shopper their card was definitely saved.
27
+ */
28
+
29
+ import { useEffect, useState } from 'react';
30
+ import type { StorefrontSavedPaymentMethod } from 'brainerce';
31
+ import { getClient } from '@/core/lib/brainerce';
32
+ import { useTranslations } from '@/core/lib/translations';
33
+ import { useAuth } from '@/core/providers/store-provider';
34
+ import { cn } from '@/core/lib/utils';
35
+
36
+ /** "12/2027" from expMonth/expYear, or null when the provider gave neither. */
37
+ function formatExpiry(month: number | null, year: number | null): string | null {
38
+ if (month == null || year == null) return null;
39
+ return `${String(month).padStart(2, '0')}/${year}`;
40
+ }
41
+
42
+ export function SavedPaymentMethods({ className }: { className?: string }) {
43
+ const t = useTranslations('account');
44
+ const { isLoggedIn } = useAuth();
45
+ const [methods, setMethods] = useState<StorefrontSavedPaymentMethod[]>([]);
46
+
47
+ useEffect(() => {
48
+ if (!isLoggedIn) {
49
+ setMethods([]);
50
+ return;
51
+ }
52
+ let cancelled = false;
53
+ getClient()
54
+ .getMySavedPaymentMethods()
55
+ .then((result) => {
56
+ if (!cancelled) setMethods(Array.isArray(result) ? result : []);
57
+ })
58
+ .catch(() => {
59
+ // Swallowed on purpose. The account page must render its profile,
60
+ // addresses and orders whether or not this optional list resolves.
61
+ if (!cancelled) setMethods([]);
62
+ });
63
+ return () => {
64
+ cancelled = true;
65
+ };
66
+ }, [isLoggedIn]);
67
+
68
+ if (methods.length === 0) return null;
69
+
70
+ return (
71
+ <div className={cn('', className)}>
72
+ <h2 className="text-foreground mb-4 text-lg font-semibold">{t('paymentMethod')}</h2>
73
+ <ul className="border-border divide-border divide-y rounded-lg border">
74
+ {methods.map((method) => {
75
+ const expiry = formatExpiry(method.expMonth, method.expYear);
76
+ return (
77
+ <li key={method.id} className="flex items-center justify-between gap-4 px-4 py-3">
78
+ <span className="text-foreground text-sm font-medium">
79
+ {/* Brand + last4 when the provider gave them; otherwise the
80
+ method itself ("paypal", "bank_account"). All store data,
81
+ so no translation key is involved. */}
82
+ {method.brand && method.last4
83
+ ? `${method.brand} •••• ${method.last4}`
84
+ : method.paymentMethod}
85
+ </span>
86
+ <span className="flex items-center gap-3">
87
+ {expiry ? (
88
+ <span className="text-muted-foreground text-sm" dir="ltr">
89
+ {expiry}
90
+ </span>
91
+ ) : null}
92
+ {method.isDefault ? (
93
+ <span className="bg-secondary text-secondary-foreground rounded px-2 py-0.5 text-xs font-medium">
94
+ {t('defaultAddress')}
95
+ </span>
96
+ ) : null}
97
+ </span>
98
+ </li>
99
+ );
100
+ })}
101
+ </ul>
102
+ </div>
103
+ );
104
+ }