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,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 {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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}`;
|