create-brainerce-store 1.3.2 → 1.4.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 +62 -5
- package/messages/en.json +258 -0
- package/messages/he.json +258 -0
- package/package.json +3 -2
- package/templates/nextjs/base/src/app/account/page.tsx +108 -105
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +90 -88
- package/templates/nextjs/base/src/app/cart/page.tsx +110 -109
- package/templates/nextjs/base/src/app/checkout/page.tsx +46 -43
- package/templates/nextjs/base/src/app/layout.tsx.ejs +8 -5
- package/templates/nextjs/base/src/app/login/page.tsx +58 -56
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +18 -23
- package/templates/nextjs/base/src/app/page.tsx +98 -95
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +16 -12
- package/templates/nextjs/base/src/app/products/page.tsx +246 -243
- package/templates/nextjs/base/src/app/register/page.tsx +68 -66
- package/templates/nextjs/base/src/app/verify-email/page.tsx +293 -291
- package/templates/nextjs/base/src/components/account/order-history.tsx +198 -184
- package/templates/nextjs/base/src/components/account/profile-section.tsx +75 -73
- package/templates/nextjs/base/src/components/auth/login-form.tsx +94 -92
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -134
- package/templates/nextjs/base/src/components/auth/register-form.tsx +184 -177
- package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -150
- package/templates/nextjs/base/src/components/cart/cart-summary.tsx +70 -67
- package/templates/nextjs/base/src/components/cart/coupon-input.tsx +134 -131
- package/templates/nextjs/base/src/components/cart/reservation-countdown.tsx +103 -100
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +28 -25
- package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +6 -4
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +133 -103
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +15 -11
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +110 -111
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +7 -4
- package/templates/nextjs/base/src/components/layout/footer.tsx +38 -35
- package/templates/nextjs/base/src/components/layout/header.tsx +332 -329
- package/templates/nextjs/base/src/components/products/product-card.tsx +3 -1
- package/templates/nextjs/base/src/components/products/product-grid.tsx +35 -33
- package/templates/nextjs/base/src/components/shared/loading-spinner.tsx +32 -30
- package/templates/nextjs/base/src/i18n.ts.ejs +5 -0
- package/templates/nextjs/base/src/lib/translations.ts +11 -0
|
@@ -1,243 +1,246 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Suspense, useEffect, useState, useCallback } from 'react';
|
|
4
|
-
import { useSearchParams, useRouter } from 'next/navigation';
|
|
5
|
-
import type { Product } from 'brainerce';
|
|
6
|
-
import type { ProductQueryParams } from 'brainerce';
|
|
7
|
-
import { getClient } from '@/lib/brainerce';
|
|
8
|
-
import { ProductGrid } from '@/components/products/product-grid';
|
|
9
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
{
|
|
24
|
-
{
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const [
|
|
45
|
-
const [
|
|
46
|
-
const [
|
|
47
|
-
const [
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
const
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
{loadingMore
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Suspense, useEffect, useState, useCallback } from 'react';
|
|
4
|
+
import { useSearchParams, useRouter } from 'next/navigation';
|
|
5
|
+
import type { Product } from 'brainerce';
|
|
6
|
+
import type { ProductQueryParams } from 'brainerce';
|
|
7
|
+
import { getClient } from '@/lib/brainerce';
|
|
8
|
+
import { ProductGrid } from '@/components/products/product-grid';
|
|
9
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
10
|
+
import { useTranslations } from '@/lib/translations';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
|
|
13
|
+
const PAGE_SIZE = 20;
|
|
14
|
+
|
|
15
|
+
type SortOption = {
|
|
16
|
+
labelKey: 'sortNewest' | 'sortNameAZ' | 'sortNameZA' | 'sortPriceLow' | 'sortPriceHigh';
|
|
17
|
+
sortBy: ProductQueryParams['sortBy'];
|
|
18
|
+
sortOrder: ProductQueryParams['sortOrder'];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const sortOptions: SortOption[] = [
|
|
22
|
+
{ labelKey: 'sortNewest', sortBy: 'createdAt', sortOrder: 'desc' },
|
|
23
|
+
{ labelKey: 'sortNameAZ', sortBy: 'name', sortOrder: 'asc' },
|
|
24
|
+
{ labelKey: 'sortNameZA', sortBy: 'name', sortOrder: 'desc' },
|
|
25
|
+
{ labelKey: 'sortPriceLow', sortBy: 'price', sortOrder: 'asc' },
|
|
26
|
+
{ labelKey: 'sortPriceHigh', sortBy: 'price', sortOrder: 'desc' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
interface CategoryFilter {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ProductsContent() {
|
|
35
|
+
const searchParams = useSearchParams();
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const t = useTranslations('products');
|
|
38
|
+
const tc = useTranslations('common');
|
|
39
|
+
|
|
40
|
+
const searchQuery = searchParams.get('search') || '';
|
|
41
|
+
const categoryId = searchParams.get('category') || '';
|
|
42
|
+
const sortParam = searchParams.get('sort') || '0';
|
|
43
|
+
|
|
44
|
+
const [products, setProducts] = useState<Product[]>([]);
|
|
45
|
+
const [loading, setLoading] = useState(true);
|
|
46
|
+
const [loadingMore, setLoadingMore] = useState(false);
|
|
47
|
+
const [page, setPage] = useState(1);
|
|
48
|
+
const [totalPages, setTotalPages] = useState(1);
|
|
49
|
+
const [total, setTotal] = useState(0);
|
|
50
|
+
const [categories, setCategories] = useState<CategoryFilter[]>([]);
|
|
51
|
+
|
|
52
|
+
const sortIndex = parseInt(sortParam, 10) || 0;
|
|
53
|
+
const currentSort = sortOptions[sortIndex] || sortOptions[0];
|
|
54
|
+
|
|
55
|
+
// Load categories
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
async function loadCategories() {
|
|
58
|
+
try {
|
|
59
|
+
const client = getClient();
|
|
60
|
+
const result = await client.getCategories();
|
|
61
|
+
setCategories(result.categories.map((c) => ({ id: c.id, name: c.name })));
|
|
62
|
+
} catch {
|
|
63
|
+
// Categories endpoint may not be available in all modes
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
loadCategories();
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Load products when filters change
|
|
70
|
+
const loadProducts = useCallback(
|
|
71
|
+
async (pageNum: number, append: boolean) => {
|
|
72
|
+
try {
|
|
73
|
+
if (append) {
|
|
74
|
+
setLoadingMore(true);
|
|
75
|
+
} else {
|
|
76
|
+
setLoading(true);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const client = getClient();
|
|
80
|
+
const params: ProductQueryParams = {
|
|
81
|
+
page: pageNum,
|
|
82
|
+
limit: PAGE_SIZE,
|
|
83
|
+
sortBy: currentSort.sortBy,
|
|
84
|
+
sortOrder: currentSort.sortOrder,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (searchQuery) params.search = searchQuery;
|
|
88
|
+
if (categoryId) params.categories = categoryId;
|
|
89
|
+
|
|
90
|
+
const result = await client.getProducts(params);
|
|
91
|
+
|
|
92
|
+
if (append) {
|
|
93
|
+
setProducts((prev) => [...prev, ...result.data]);
|
|
94
|
+
} else {
|
|
95
|
+
setProducts(result.data);
|
|
96
|
+
}
|
|
97
|
+
setTotalPages(result.meta.totalPages);
|
|
98
|
+
setTotal(result.meta.total);
|
|
99
|
+
setPage(pageNum);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error('Failed to load products:', err);
|
|
102
|
+
} finally {
|
|
103
|
+
setLoading(false);
|
|
104
|
+
setLoadingMore(false);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[searchQuery, categoryId, currentSort.sortBy, currentSort.sortOrder]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
loadProducts(1, false);
|
|
112
|
+
}, [loadProducts]);
|
|
113
|
+
|
|
114
|
+
function handleLoadMore() {
|
|
115
|
+
if (page < totalPages && !loadingMore) {
|
|
116
|
+
loadProducts(page + 1, true);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function updateParam(key: string, value: string) {
|
|
121
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
122
|
+
if (value) {
|
|
123
|
+
params.set(key, value);
|
|
124
|
+
} else {
|
|
125
|
+
params.delete(key);
|
|
126
|
+
}
|
|
127
|
+
router.push(`/products?${params.toString()}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
|
132
|
+
{/* Page Header */}
|
|
133
|
+
<div className="mb-8">
|
|
134
|
+
<h1 className="text-foreground text-3xl font-bold">
|
|
135
|
+
{searchQuery ? `${t('searchPrefix')} "${searchQuery}"` : t('allProducts')}
|
|
136
|
+
</h1>
|
|
137
|
+
{!loading && (
|
|
138
|
+
<p className="text-muted-foreground mt-1 text-sm">
|
|
139
|
+
{total} {total === 1 ? tc('product') : tc('products')} {tc('found')}
|
|
140
|
+
</p>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Filters and Sort */}
|
|
145
|
+
<div className="mb-6 flex flex-col items-start gap-4 sm:flex-row sm:items-center">
|
|
146
|
+
{/* Category Filter */}
|
|
147
|
+
{categories.length > 0 && (
|
|
148
|
+
<div className="flex flex-wrap gap-2">
|
|
149
|
+
<button
|
|
150
|
+
onClick={() => updateParam('category', '')}
|
|
151
|
+
className={cn(
|
|
152
|
+
'rounded-full border px-3 py-1.5 text-sm transition-colors',
|
|
153
|
+
!categoryId
|
|
154
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
155
|
+
: 'border-border text-muted-foreground hover:border-primary hover:text-foreground'
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{tc('all')}
|
|
159
|
+
</button>
|
|
160
|
+
{categories.map((cat) => (
|
|
161
|
+
<button
|
|
162
|
+
key={cat.id}
|
|
163
|
+
onClick={() => updateParam('category', cat.id)}
|
|
164
|
+
className={cn(
|
|
165
|
+
'rounded-full border px-3 py-1.5 text-sm transition-colors',
|
|
166
|
+
categoryId === cat.id
|
|
167
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
168
|
+
: 'border-border text-muted-foreground hover:border-primary hover:text-foreground'
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{cat.name}
|
|
172
|
+
</button>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{/* Sort */}
|
|
178
|
+
<div className="flex items-center gap-2 sm:ms-auto">
|
|
179
|
+
<label htmlFor="sort" className="text-muted-foreground whitespace-nowrap text-sm">
|
|
180
|
+
{tc('sortBy')}
|
|
181
|
+
</label>
|
|
182
|
+
<select
|
|
183
|
+
id="sort"
|
|
184
|
+
value={sortIndex}
|
|
185
|
+
onChange={(e) => updateParam('sort', e.target.value)}
|
|
186
|
+
className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-9 rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
187
|
+
>
|
|
188
|
+
{sortOptions.map((opt, idx) => (
|
|
189
|
+
<option key={idx} value={idx}>
|
|
190
|
+
{t(opt.labelKey)}
|
|
191
|
+
</option>
|
|
192
|
+
))}
|
|
193
|
+
</select>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
{/* Products Grid */}
|
|
198
|
+
{loading ? (
|
|
199
|
+
<div className="flex items-center justify-center py-20">
|
|
200
|
+
<LoadingSpinner size="lg" />
|
|
201
|
+
</div>
|
|
202
|
+
) : (
|
|
203
|
+
<>
|
|
204
|
+
<ProductGrid products={products} />
|
|
205
|
+
|
|
206
|
+
{/* Load More */}
|
|
207
|
+
{page < totalPages && (
|
|
208
|
+
<div className="mt-10 flex justify-center">
|
|
209
|
+
<button
|
|
210
|
+
onClick={handleLoadMore}
|
|
211
|
+
disabled={loadingMore}
|
|
212
|
+
className="bg-primary text-primary-foreground inline-flex items-center gap-2 rounded px-6 py-2.5 font-medium transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
213
|
+
>
|
|
214
|
+
{loadingMore ? (
|
|
215
|
+
<>
|
|
216
|
+
<LoadingSpinner
|
|
217
|
+
size="sm"
|
|
218
|
+
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
219
|
+
/>
|
|
220
|
+
{tc('loading')}
|
|
221
|
+
</>
|
|
222
|
+
) : (
|
|
223
|
+
t('loadMore')
|
|
224
|
+
)}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export default function ProductsPage() {
|
|
235
|
+
return (
|
|
236
|
+
<Suspense
|
|
237
|
+
fallback={
|
|
238
|
+
<div className="flex min-h-[60vh] items-center justify-center">
|
|
239
|
+
<LoadingSpinner size="lg" />
|
|
240
|
+
</div>
|
|
241
|
+
}
|
|
242
|
+
>
|
|
243
|
+
<ProductsContent />
|
|
244
|
+
</Suspense>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
@@ -1,66 +1,68 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { useRouter } from 'next/navigation';
|
|
5
|
-
import Link from 'next/link';
|
|
6
|
-
import { getClient } from '@/lib/brainerce';
|
|
7
|
-
import { useAuth } from '@/providers/store-provider';
|
|
8
|
-
import { RegisterForm } from '@/components/auth/register-form';
|
|
9
|
-
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { getClient } from '@/lib/brainerce';
|
|
7
|
+
import { useAuth } from '@/providers/store-provider';
|
|
8
|
+
import { RegisterForm } from '@/components/auth/register-form';
|
|
9
|
+
import { OAuthButtons } from '@/components/auth/oauth-buttons';
|
|
10
|
+
import { useTranslations } from '@/lib/translations';
|
|
11
|
+
|
|
12
|
+
export default function RegisterPage() {
|
|
13
|
+
const router = useRouter();
|
|
14
|
+
const auth = useAuth();
|
|
15
|
+
const t = useTranslations('auth');
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
async function handleRegister(data: {
|
|
19
|
+
firstName: string;
|
|
20
|
+
lastName: string;
|
|
21
|
+
email: string;
|
|
22
|
+
password: string;
|
|
23
|
+
}) {
|
|
24
|
+
try {
|
|
25
|
+
setError(null);
|
|
26
|
+
const client = getClient();
|
|
27
|
+
const result = await client.registerCustomer({
|
|
28
|
+
firstName: data.firstName,
|
|
29
|
+
lastName: data.lastName,
|
|
30
|
+
email: data.email,
|
|
31
|
+
password: data.password,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (result.requiresVerification) {
|
|
35
|
+
router.push(`/verify-email?token=${encodeURIComponent(result.token)}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
auth.login(result.token);
|
|
40
|
+
router.push('/');
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : 'Registration failed. Please try again.';
|
|
43
|
+
setError(message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
49
|
+
<div className="w-full max-w-md space-y-6">
|
|
50
|
+
<div className="text-center">
|
|
51
|
+
<h1 className="text-foreground text-2xl font-bold">{t('createAccountTitle')}</h1>
|
|
52
|
+
<p className="text-muted-foreground mt-1 text-sm">{t('joinSubtitle')}</p>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<RegisterForm onSubmit={handleRegister} error={error} />
|
|
56
|
+
|
|
57
|
+
<OAuthButtons />
|
|
58
|
+
|
|
59
|
+
<p className="text-muted-foreground text-center text-sm">
|
|
60
|
+
{t('alreadyHaveAccount')}{' '}
|
|
61
|
+
<Link href="/login" className="text-primary font-medium hover:underline">
|
|
62
|
+
{t('signIn')}
|
|
63
|
+
</Link>
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|