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.
Files changed (25) hide show
  1. package/dist/index.js +1 -1
  2. package/messages/en.json +390 -390
  3. package/messages/he.json +390 -390
  4. package/package.json +1 -1
  5. package/templates/nextjs/base/next.config.ts +47 -47
  6. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +15 -15
  7. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +66 -66
  8. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +76 -76
  9. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +6 -0
  10. package/templates/nextjs/base/src/app/checkout/page.tsx +975 -975
  11. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +7 -16
  12. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +529 -529
  13. package/templates/nextjs/base/src/app/products/page.tsx +6 -13
  14. package/templates/nextjs/base/src/app/reset-password/page.tsx +138 -138
  15. package/templates/nextjs/base/src/components/auth/register-form.tsx +245 -245
  16. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +416 -416
  17. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +656 -656
  18. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +88 -88
  19. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +6 -2
  20. package/templates/nextjs/base/src/lib/csrf.ts +11 -11
  21. package/templates/nextjs/base/src/lib/navigation.tsx.ejs +17 -6
  22. package/templates/nextjs/base/src/lib/nonce.ts +10 -10
  23. package/templates/nextjs/base/src/lib/safe-redirect.ts +45 -45
  24. package/templates/nextjs/base/src/lib/sanitize-html.ts +93 -93
  25. 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, useParams } from 'next/navigation';
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({ locale }),
258
- client.getBrands({ locale }),
259
- client.getTags({ locale }),
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
- }, [locale]);
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, locale]
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
+ }