create-brainerce-store 1.28.15 → 1.28.18
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 +1 -1
- package/messages/en.json +390 -390
- package/messages/he.json +390 -390
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +47 -47
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -15
- package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -66
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -76
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +6 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +975 -975
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +7 -16
- package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +529 -529
- package/templates/nextjs/base/src/app/products/page.tsx +6 -13
- package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -138
- package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -245
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -416
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -656
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +88 -88
- package/templates/nextjs/base/src/lib/brainerce.ts.ejs +6 -2
- package/templates/nextjs/base/src/lib/csrf.ts +11 -11
- package/templates/nextjs/base/src/lib/navigation.tsx.ejs +17 -6
- package/templates/nextjs/base/src/lib/nonce.ts +10 -10
- package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -45
- package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -93
- package/templates/nextjs/base/src/lib/validation.ts +37 -37
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Suspense, useEffect, useState, useCallback, useRef } from 'react';
|
|
4
|
-
import { useSearchParams
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
5
|
import { useRouter } from '@/lib/navigation';
|
|
6
6
|
import type { Product } from 'brainerce';
|
|
7
7
|
import type { ProductQueryParams } from 'brainerce';
|
|
@@ -221,12 +221,6 @@ function CategoryChip({
|
|
|
221
221
|
function ProductsContent() {
|
|
222
222
|
const searchParams = useSearchParams();
|
|
223
223
|
const router = useRouter();
|
|
224
|
-
// When i18n is enabled this file lives under `app/[locale]/products/` (the
|
|
225
|
-
// scaffold script moves it). `useParams().locale` is populated then and
|
|
226
|
-
// undefined otherwise, so passing it to the SDK is a no-op for single-locale
|
|
227
|
-
// stores and avoids the StoreProvider-setLocale timing race for multi-locale.
|
|
228
|
-
const routeParams = useParams<{ locale?: string }>();
|
|
229
|
-
const locale = (routeParams?.locale as string | undefined) || undefined;
|
|
230
224
|
const t = useTranslations('products');
|
|
231
225
|
const tc = useTranslations('common');
|
|
232
226
|
|
|
@@ -254,16 +248,16 @@ function ProductsContent() {
|
|
|
254
248
|
async function loadFilters() {
|
|
255
249
|
const client = getClient();
|
|
256
250
|
const [catRes, brandRes, tagRes] = await Promise.allSettled([
|
|
257
|
-
client.getCategories(
|
|
258
|
-
client.getBrands(
|
|
259
|
-
client.getTags(
|
|
251
|
+
client.getCategories(),
|
|
252
|
+
client.getBrands(),
|
|
253
|
+
client.getTags(),
|
|
260
254
|
]);
|
|
261
255
|
if (catRes.status === 'fulfilled') setCategories(catRes.value.categories as CategoryNode[]);
|
|
262
256
|
if (brandRes.status === 'fulfilled') setBrands(brandRes.value.brands);
|
|
263
257
|
if (tagRes.status === 'fulfilled') setTags(tagRes.value.tags);
|
|
264
258
|
}
|
|
265
259
|
loadFilters();
|
|
266
|
-
}, [
|
|
260
|
+
}, []);
|
|
267
261
|
|
|
268
262
|
// Load products when filters change
|
|
269
263
|
const loadProducts = useCallback(
|
|
@@ -281,7 +275,6 @@ function ProductsContent() {
|
|
|
281
275
|
limit: PAGE_SIZE,
|
|
282
276
|
sortBy: currentSort.sortBy,
|
|
283
277
|
sortOrder: currentSort.sortOrder,
|
|
284
|
-
locale,
|
|
285
278
|
};
|
|
286
279
|
|
|
287
280
|
if (searchQuery) params.search = searchQuery;
|
|
@@ -306,7 +299,7 @@ function ProductsContent() {
|
|
|
306
299
|
setLoadingMore(false);
|
|
307
300
|
}
|
|
308
301
|
},
|
|
309
|
-
[searchQuery, categoryId, brandId, tagId, currentSort.sortBy, currentSort.sortOrder
|
|
302
|
+
[searchQuery, categoryId, brandId, tagId, currentSort.sortBy, currentSort.sortOrder]
|
|
310
303
|
);
|
|
311
304
|
|
|
312
305
|
useEffect(() => {
|
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState } from 'react';
|
|
4
|
-
import { useRouter, Link } from '@/lib/navigation';
|
|
5
|
-
import { proxyResetPassword } from '@/lib/auth';
|
|
6
|
-
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
-
import { useTranslations } from '@/lib/translations';
|
|
8
|
-
import { getPasswordError } from '@/lib/validation';
|
|
9
|
-
|
|
10
|
-
export default function ResetPasswordPage() {
|
|
11
|
-
const router = useRouter();
|
|
12
|
-
const t = useTranslations('auth');
|
|
13
|
-
|
|
14
|
-
const [newPassword, setNewPassword] = useState('');
|
|
15
|
-
const [confirmPassword, setConfirmPassword] = useState('');
|
|
16
|
-
const [loading, setLoading] = useState(false);
|
|
17
|
-
const [error, setError] = useState<string | null>(null);
|
|
18
|
-
const [success, setSuccess] = useState(false);
|
|
19
|
-
|
|
20
|
-
async function handleSubmit(e: React.FormEvent) {
|
|
21
|
-
e.preventDefault();
|
|
22
|
-
if (loading) return;
|
|
23
|
-
|
|
24
|
-
const pwCode = getPasswordError(newPassword);
|
|
25
|
-
if (pwCode) {
|
|
26
|
-
setError(t(pwCode));
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (newPassword !== confirmPassword) {
|
|
31
|
-
setError(t('passwordsMustMatch'));
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
setLoading(true);
|
|
37
|
-
setError(null);
|
|
38
|
-
await proxyResetPassword(newPassword);
|
|
39
|
-
setSuccess(true);
|
|
40
|
-
setTimeout(() => router.push('/login'), 2000);
|
|
41
|
-
} catch (err) {
|
|
42
|
-
const message =
|
|
43
|
-
err instanceof Error ? err.message : 'Something went wrong. Please try again.';
|
|
44
|
-
setError(message);
|
|
45
|
-
} finally {
|
|
46
|
-
setLoading(false);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
52
|
-
<div className="w-full max-w-md space-y-6">
|
|
53
|
-
<div className="text-center">
|
|
54
|
-
<h1 className="text-foreground text-2xl font-bold">{t('resetPasswordTitle')}</h1>
|
|
55
|
-
<p className="text-muted-foreground mt-1 text-sm">{t('resetPasswordSubtitle')}</p>
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
{error && (
|
|
59
|
-
<div className="bg-destructive/10 border-destructive/20 text-destructive space-y-2 rounded-lg border px-4 py-3 text-sm">
|
|
60
|
-
<p>{error}</p>
|
|
61
|
-
<Link
|
|
62
|
-
href="/forgot-password"
|
|
63
|
-
className="text-primary block font-medium hover:underline"
|
|
64
|
-
>
|
|
65
|
-
{t('sendResetLink')}
|
|
66
|
-
</Link>
|
|
67
|
-
</div>
|
|
68
|
-
)}
|
|
69
|
-
|
|
70
|
-
{success ? (
|
|
71
|
-
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
|
|
72
|
-
{t('passwordResetSuccess')}
|
|
73
|
-
</div>
|
|
74
|
-
) : (
|
|
75
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
76
|
-
<div>
|
|
77
|
-
<label
|
|
78
|
-
htmlFor="new-password"
|
|
79
|
-
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
80
|
-
>
|
|
81
|
-
{t('newPassword')}
|
|
82
|
-
</label>
|
|
83
|
-
<input
|
|
84
|
-
id="new-password"
|
|
85
|
-
type="password"
|
|
86
|
-
required
|
|
87
|
-
minLength={8}
|
|
88
|
-
value={newPassword}
|
|
89
|
-
onChange={(e) => setNewPassword(e.target.value)}
|
|
90
|
-
placeholder={t('newPasswordPlaceholder')}
|
|
91
|
-
autoComplete="new-password"
|
|
92
|
-
className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div>
|
|
97
|
-
<label
|
|
98
|
-
htmlFor="confirm-password"
|
|
99
|
-
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
100
|
-
>
|
|
101
|
-
{t('confirmPassword')}
|
|
102
|
-
</label>
|
|
103
|
-
<input
|
|
104
|
-
id="confirm-password"
|
|
105
|
-
type="password"
|
|
106
|
-
required
|
|
107
|
-
minLength={8}
|
|
108
|
-
value={confirmPassword}
|
|
109
|
-
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
110
|
-
placeholder={t('confirmPasswordPlaceholder')}
|
|
111
|
-
autoComplete="new-password"
|
|
112
|
-
className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
|
|
116
|
-
<button
|
|
117
|
-
type="submit"
|
|
118
|
-
disabled={loading}
|
|
119
|
-
className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
120
|
-
>
|
|
121
|
-
{loading ? (
|
|
122
|
-
<>
|
|
123
|
-
<LoadingSpinner
|
|
124
|
-
size="sm"
|
|
125
|
-
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
126
|
-
/>
|
|
127
|
-
{t('resettingPassword')}
|
|
128
|
-
</>
|
|
129
|
-
) : (
|
|
130
|
-
t('resetPassword')
|
|
131
|
-
)}
|
|
132
|
-
</button>
|
|
133
|
-
</form>
|
|
134
|
-
)}
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useRouter, Link } from '@/lib/navigation';
|
|
5
|
+
import { proxyResetPassword } from '@/lib/auth';
|
|
6
|
+
import { LoadingSpinner } from '@/components/shared/loading-spinner';
|
|
7
|
+
import { useTranslations } from '@/lib/translations';
|
|
8
|
+
import { getPasswordError } from '@/lib/validation';
|
|
9
|
+
|
|
10
|
+
export default function ResetPasswordPage() {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const t = useTranslations('auth');
|
|
13
|
+
|
|
14
|
+
const [newPassword, setNewPassword] = useState('');
|
|
15
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState<string | null>(null);
|
|
18
|
+
const [success, setSuccess] = useState(false);
|
|
19
|
+
|
|
20
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
if (loading) return;
|
|
23
|
+
|
|
24
|
+
const pwCode = getPasswordError(newPassword);
|
|
25
|
+
if (pwCode) {
|
|
26
|
+
setError(t(pwCode));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (newPassword !== confirmPassword) {
|
|
31
|
+
setError(t('passwordsMustMatch'));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
setLoading(true);
|
|
37
|
+
setError(null);
|
|
38
|
+
await proxyResetPassword(newPassword);
|
|
39
|
+
setSuccess(true);
|
|
40
|
+
setTimeout(() => router.push('/login'), 2000);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message =
|
|
43
|
+
err instanceof Error ? err.message : 'Something went wrong. Please try again.';
|
|
44
|
+
setError(message);
|
|
45
|
+
} finally {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
52
|
+
<div className="w-full max-w-md space-y-6">
|
|
53
|
+
<div className="text-center">
|
|
54
|
+
<h1 className="text-foreground text-2xl font-bold">{t('resetPasswordTitle')}</h1>
|
|
55
|
+
<p className="text-muted-foreground mt-1 text-sm">{t('resetPasswordSubtitle')}</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{error && (
|
|
59
|
+
<div className="bg-destructive/10 border-destructive/20 text-destructive space-y-2 rounded-lg border px-4 py-3 text-sm">
|
|
60
|
+
<p>{error}</p>
|
|
61
|
+
<Link
|
|
62
|
+
href="/forgot-password"
|
|
63
|
+
className="text-primary block font-medium hover:underline"
|
|
64
|
+
>
|
|
65
|
+
{t('sendResetLink')}
|
|
66
|
+
</Link>
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{success ? (
|
|
71
|
+
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-800 dark:border-green-800 dark:bg-green-950/30 dark:text-green-300">
|
|
72
|
+
{t('passwordResetSuccess')}
|
|
73
|
+
</div>
|
|
74
|
+
) : (
|
|
75
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
76
|
+
<div>
|
|
77
|
+
<label
|
|
78
|
+
htmlFor="new-password"
|
|
79
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
80
|
+
>
|
|
81
|
+
{t('newPassword')}
|
|
82
|
+
</label>
|
|
83
|
+
<input
|
|
84
|
+
id="new-password"
|
|
85
|
+
type="password"
|
|
86
|
+
required
|
|
87
|
+
minLength={8}
|
|
88
|
+
value={newPassword}
|
|
89
|
+
onChange={(e) => setNewPassword(e.target.value)}
|
|
90
|
+
placeholder={t('newPasswordPlaceholder')}
|
|
91
|
+
autoComplete="new-password"
|
|
92
|
+
className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div>
|
|
97
|
+
<label
|
|
98
|
+
htmlFor="confirm-password"
|
|
99
|
+
className="text-foreground mb-1.5 block text-sm font-medium"
|
|
100
|
+
>
|
|
101
|
+
{t('confirmPassword')}
|
|
102
|
+
</label>
|
|
103
|
+
<input
|
|
104
|
+
id="confirm-password"
|
|
105
|
+
type="password"
|
|
106
|
+
required
|
|
107
|
+
minLength={8}
|
|
108
|
+
value={confirmPassword}
|
|
109
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
110
|
+
placeholder={t('confirmPasswordPlaceholder')}
|
|
111
|
+
autoComplete="new-password"
|
|
112
|
+
className="border-border bg-background text-foreground placeholder:text-muted-foreground focus:ring-primary/20 focus:border-primary h-10 w-full rounded border px-3 text-sm focus:outline-none focus:ring-2"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<button
|
|
117
|
+
type="submit"
|
|
118
|
+
disabled={loading}
|
|
119
|
+
className="bg-primary text-primary-foreground flex h-10 w-full items-center justify-center gap-2 rounded text-sm font-medium transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
120
|
+
>
|
|
121
|
+
{loading ? (
|
|
122
|
+
<>
|
|
123
|
+
<LoadingSpinner
|
|
124
|
+
size="sm"
|
|
125
|
+
className="border-primary-foreground/30 border-t-primary-foreground"
|
|
126
|
+
/>
|
|
127
|
+
{t('resettingPassword')}
|
|
128
|
+
</>
|
|
129
|
+
) : (
|
|
130
|
+
t('resetPassword')
|
|
131
|
+
)}
|
|
132
|
+
</button>
|
|
133
|
+
</form>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
}
|