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,122 +1,168 @@
1
- 'use client';
2
-
3
- import { useEffect, useState } from 'react';
4
- import { useRouter } from '@/core/lib/navigation';
5
- import type { CustomerProfile, Order } from 'brainerce';
6
- import { getClient } from '@/core/lib/brainerce';
7
- import { useAuth } from '@/core/providers/store-provider';
8
- import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
- import { ProfileSection } from '@/components/account/profile-section';
10
- import { AddressBook } from '@/components/account/address-book';
11
- import { OrderHistory } from '@/components/account/order-history';
12
- import { useTranslations } from '@/core/lib/translations';
13
-
14
- export default function AccountPage() {
15
- const router = useRouter();
16
- const { isLoggedIn, authLoading, logout } = useAuth();
17
- const t = useTranslations('account');
18
- const tc = useTranslations('common');
19
- const [profile, setProfile] = useState<CustomerProfile | null>(null);
20
- const [orders, setOrders] = useState<Order[]>([]);
21
- const [loading, setLoading] = useState(true);
22
- const [error, setError] = useState<string | null>(null);
23
-
24
- useEffect(() => {
25
- if (authLoading) return;
26
-
27
- if (!isLoggedIn) {
28
- router.push('/login');
29
- return;
30
- }
31
-
32
- async function loadAccountData() {
33
- try {
34
- const client = getClient();
35
- const [profileResult, ordersResult] = await Promise.allSettled([
36
- client.getMyProfile(),
37
- client.getMyOrders({ limit: 20 }),
38
- ]);
39
-
40
- if (profileResult.status === 'fulfilled') {
41
- setProfile(profileResult.value);
42
- } else {
43
- setError('Failed to load profile.');
44
- }
45
-
46
- if (ordersResult.status === 'fulfilled') {
47
- setOrders(ordersResult.value.data);
48
- }
49
- } catch (err) {
50
- const message = err instanceof Error ? err.message : 'Failed to load account data.';
51
- setError(message);
52
- } finally {
53
- setLoading(false);
54
- }
55
- }
56
-
57
- loadAccountData();
58
- }, [isLoggedIn, authLoading, router]);
59
-
60
- if (authLoading || !isLoggedIn) {
61
- return null;
62
- }
63
-
64
- if (loading) {
65
- return (
66
- <div className="flex min-h-[60vh] items-center justify-center">
67
- <LoadingSpinner size="lg" />
68
- </div>
69
- );
70
- }
71
-
72
- if (error && !profile) {
73
- return (
74
- <div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
75
- <h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
76
- <p className="text-muted-foreground mt-2">{error}</p>
77
- <button
78
- type="button"
79
- onClick={() => window.location.reload()}
80
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
81
- >
82
- {tc('tryAgain')}
83
- </button>
84
- </div>
85
- );
86
- }
87
-
88
- return (
89
- <div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
90
- <div className="mb-6 flex items-center justify-between">
91
- <h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
92
- <button
93
- type="button"
94
- onClick={logout}
95
- className="text-muted-foreground hover:text-foreground text-sm transition-colors"
96
- >
97
- {t('signOut')}
98
- </button>
99
- </div>
100
-
101
- {/* Profile Section */}
102
- {profile && (
103
- <ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-6" />
104
- )}
105
-
106
- {/* Address Book */}
107
- {profile && (
108
- <AddressBook
109
- addresses={profile.addresses ?? []}
110
- onUpdate={(updated) => setProfile((p) => (p ? { ...p, addresses: updated } : p))}
111
- className="mb-8"
112
- />
113
- )}
114
-
115
- {/* Order History */}
116
- <div>
117
- <h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
118
- <OrderHistory orders={orders} />
119
- </div>
120
- </div>
121
- );
122
- }
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useState } from 'react';
4
+ import { useRouter } from '@/core/lib/navigation';
5
+ import type { CustomerProfile, Order } from 'brainerce';
6
+ import { getClient } from '@/core/lib/brainerce';
7
+ import { useAuth } from '@/core/providers/store-provider';
8
+ import { LoadingSpinner } from '@/ui/shared/loading-spinner';
9
+ import { ProfileSection } from '@/components/account/profile-section';
10
+ import { AddressBook } from '@/components/account/address-book';
11
+ import { OrderHistory } from '@/components/account/order-history';
12
+ import { LoyaltyPanel } from '@/components/account/loyalty-panel';
13
+ import { SavedPaymentMethods } from '@/components/account/saved-payment-methods';
14
+ import { useTranslations } from '@/core/lib/translations';
15
+
16
+ /** Orders fetched per page. The API caps `limit` at 100. */
17
+ const ORDERS_PAGE_SIZE = 20;
18
+
19
+ export default function AccountPage() {
20
+ const router = useRouter();
21
+ const { isLoggedIn, authLoading, logout } = useAuth();
22
+ const t = useTranslations('account');
23
+ const tc = useTranslations('common');
24
+ const [profile, setProfile] = useState<CustomerProfile | null>(null);
25
+ const [orders, setOrders] = useState<Order[]>([]);
26
+ // Order history is paginated: `getMyOrders` answers the standard
27
+ // { data, meta: { page, limit, total, totalPages } } envelope. Without the
28
+ // page cursor below, order 21 is unreachable forever.
29
+ const [ordersPage, setOrdersPage] = useState(1);
30
+ const [ordersTotalPages, setOrdersTotalPages] = useState(1);
31
+ const [ordersLoadingMore, setOrdersLoadingMore] = useState(false);
32
+ const [loading, setLoading] = useState(true);
33
+ const [error, setError] = useState<string | null>(null);
34
+
35
+ /** Fetch one page of orders. `append` distinguishes load-more from a reload. */
36
+ const loadOrders = useCallback(async (page: number, append: boolean) => {
37
+ if (append) setOrdersLoadingMore(true);
38
+ try {
39
+ const result = await getClient().getMyOrders({ page, limit: ORDERS_PAGE_SIZE });
40
+ setOrders((prev) => (append ? [...prev, ...result.data] : result.data));
41
+ setOrdersPage(result.meta.page);
42
+ setOrdersTotalPages(result.meta.totalPages);
43
+ } catch {
44
+ // A failed page must not blank the pages already on screen.
45
+ } finally {
46
+ if (append) setOrdersLoadingMore(false);
47
+ }
48
+ }, []);
49
+
50
+ useEffect(() => {
51
+ if (authLoading) return;
52
+
53
+ if (!isLoggedIn) {
54
+ router.push('/login');
55
+ return;
56
+ }
57
+
58
+ async function loadAccountData() {
59
+ try {
60
+ const client = getClient();
61
+ const [profileResult, ordersResult] = await Promise.allSettled([
62
+ client.getMyProfile(),
63
+ client.getMyOrders({ page: 1, limit: ORDERS_PAGE_SIZE }),
64
+ ]);
65
+
66
+ if (profileResult.status === 'fulfilled') {
67
+ setProfile(profileResult.value);
68
+ } else {
69
+ setError('Failed to load profile.');
70
+ }
71
+
72
+ if (ordersResult.status === 'fulfilled') {
73
+ setOrders(ordersResult.value.data);
74
+ setOrdersPage(ordersResult.value.meta.page);
75
+ setOrdersTotalPages(ordersResult.value.meta.totalPages);
76
+ }
77
+ } catch (err) {
78
+ const message = err instanceof Error ? err.message : 'Failed to load account data.';
79
+ setError(message);
80
+ } finally {
81
+ setLoading(false);
82
+ }
83
+ }
84
+
85
+ loadAccountData();
86
+ }, [isLoggedIn, authLoading, router]);
87
+
88
+ if (authLoading || !isLoggedIn) {
89
+ return null;
90
+ }
91
+
92
+ if (loading) {
93
+ return (
94
+ <div className="flex min-h-[60vh] items-center justify-center">
95
+ <LoadingSpinner size="lg" />
96
+ </div>
97
+ );
98
+ }
99
+
100
+ if (error && !profile) {
101
+ return (
102
+ <div className="mx-auto max-w-3xl px-4 py-16 text-center sm:px-6 lg:px-8">
103
+ <h1 className="text-foreground text-2xl font-bold">{tc('error')}</h1>
104
+ <p className="text-muted-foreground mt-2">{error}</p>
105
+ <button
106
+ type="button"
107
+ onClick={() => window.location.reload()}
108
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
109
+ >
110
+ {tc('tryAgain')}
111
+ </button>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ return (
117
+ <div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
118
+ <div className="mb-6 flex items-center justify-between">
119
+ <h1 className="text-foreground text-2xl font-bold">{t('myAccount')}</h1>
120
+ <button
121
+ type="button"
122
+ onClick={logout}
123
+ className="text-muted-foreground hover:text-foreground text-sm transition-colors"
124
+ >
125
+ {t('signOut')}
126
+ </button>
127
+ </div>
128
+
129
+ {/* Profile Section */}
130
+ {profile && (
131
+ <ProfileSection profile={profile} onProfileUpdate={setProfile} className="mb-6" />
132
+ )}
133
+
134
+ {/* Address Book */}
135
+ {profile && (
136
+ <AddressBook
137
+ addresses={profile.addresses ?? []}
138
+ onUpdate={(updated) => setProfile((p) => (p ? { ...p, addresses: updated } : p))}
139
+ className="mb-8"
140
+ />
141
+ )}
142
+
143
+ {/* Saved cards. Renders nothing at all until the customer has vaulted
144
+ one by checking out with saveCard, which is the normal case, so it
145
+ is mounted unconditionally and hides itself. Read-only on purpose:
146
+ the only storefront call that CHARGES a vaulted card is
147
+ subscribeToMembership, so there is no "pay with this card" action to
148
+ offer here. */}
149
+ <SavedPaymentMethods className="mb-8" />
150
+
151
+ {/* Rewards. Renders nothing at all when the store has no loyalty
152
+ programme, which is the normal case until the merchant turns one on —
153
+ so it is mounted unconditionally and hides itself. */}
154
+ <LoyaltyPanel className="mb-8" />
155
+
156
+ {/* Order History */}
157
+ <div>
158
+ <h2 className="text-foreground mb-4 text-lg font-semibold">{t('orderHistory')}</h2>
159
+ <OrderHistory
160
+ orders={orders}
161
+ hasMore={ordersPage < ordersTotalPages}
162
+ loadingMore={ordersLoadingMore}
163
+ onLoadMore={() => loadOrders(ordersPage + 1, true)}
164
+ />
165
+ </div>
166
+ </div>
167
+ );
168
+ }
@@ -2,6 +2,7 @@ import type { Metadata } from 'next';
2
2
  import { notFound } from 'next/navigation';
3
3
  import { BrainerceError } from 'brainerce';
4
4
  import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce.server';
5
+ import { getRegionId } from '@/core/lib/region.server';
5
6
  import { buildMetaDescription } from '@/core/lib/seo';
6
7
  import { sanitizeHtml } from '@/core/lib/sanitize';
7
8
  import { decodeSlug } from '@/core/lib/utils';
@@ -103,7 +104,15 @@ export default async function CategoryPage({ params }: Props) {
103
104
 
104
105
  // The listing endpoint owns pagination/FX/publish gating — reuse it rather
105
106
  // than duplicating a product query here.
106
- const { data: products } = await client.getProducts({ categories: [category.id], limit: 48 });
107
+ // `regionId` on the listing read too: a category grid that prices in the
108
+ // default region while the product page prices in the buyer's is worse than
109
+ // neither doing it. Resolves to undefined on a single-region store, which
110
+ // makes this call byte-identical to what it was.
111
+ const { data: products } = await client.getProducts({
112
+ categories: [category.id],
113
+ limit: 48,
114
+ regionId: await getRegionId(),
115
+ });
107
116
 
108
117
  const baseUrl = await getCanonicalSiteUrl();
109
118
  const url = `${baseUrl}/category/${category.slug || slug}`;