create-brainerce-store 1.18.0 → 1.19.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 (65) hide show
  1. package/dist/index.js +31 -9
  2. package/messages/en.json +366 -362
  3. package/messages/he.json +366 -362
  4. package/package.json +45 -45
  5. package/templates/nextjs/base/next.config.ts +31 -31
  6. package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
  7. package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
  8. package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
  9. package/templates/nextjs/base/src/app/account/page.tsx +122 -122
  10. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
  11. package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
  12. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
  13. package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
  14. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
  15. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
  16. package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
  17. package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
  18. package/templates/nextjs/base/src/app/cart/page.tsx +204 -204
  19. package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
  20. package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
  21. package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
  22. package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
  23. package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
  24. package/templates/nextjs/base/src/app/login/page.tsx +59 -59
  25. package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
  26. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +254 -254
  27. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
  28. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
  29. package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
  30. package/templates/nextjs/base/src/app/products/page.tsx +431 -431
  31. package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
  32. package/templates/nextjs/base/src/app/register/page.tsx +65 -65
  33. package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
  34. package/templates/nextjs/base/src/app/robots.ts +14 -14
  35. package/templates/nextjs/base/src/app/sitemap.ts +25 -25
  36. package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
  37. package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
  38. package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
  39. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  40. package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
  41. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
  42. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  43. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
  44. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
  45. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
  46. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
  47. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +519 -519
  48. package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
  49. package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
  50. package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
  51. package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
  52. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
  53. package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
  54. package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
  55. package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
  56. package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
  57. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
  58. package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
  59. package/templates/nextjs/base/src/lib/auth.ts +149 -149
  60. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
  61. package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
  62. package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
  63. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
  64. package/templates/nextjs/base/src/lib/translations.ts +0 -11
  65. package/templates/nextjs/base/src/middleware.ts +0 -25
@@ -1,258 +1,258 @@
1
- 'use client';
2
-
3
- import { Suspense, useState, useRef, useEffect, useCallback } from 'react';
4
- import { useRouter } from 'next/navigation';
5
- import Link from 'next/link';
6
- import { useAuth } from '@/providers/store-provider';
7
- import { proxyVerifyEmail, proxyResendVerification } from '@/lib/auth';
8
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
9
- import { useTranslations } from '@/lib/translations';
10
-
11
- const CODE_LENGTH = 6;
12
- const RESEND_COOLDOWN_SECONDS = 60;
13
-
14
- function VerifyEmailContent() {
15
- const router = useRouter();
16
- const auth = useAuth();
17
-
18
- const t = useTranslations('auth');
19
-
20
- const [digits, setDigits] = useState<string[]>(Array(CODE_LENGTH).fill(''));
21
- const [loading, setLoading] = useState(false);
22
- const [resending, setResending] = useState(false);
23
- const [error, setError] = useState<string | null>(null);
24
- const [success, setSuccess] = useState<string | null>(null);
25
- const [cooldown, setCooldown] = useState(0);
26
-
27
- const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
28
-
29
- // Auto-focus first input on mount
30
- useEffect(() => {
31
- inputRefs.current[0]?.focus();
32
- }, []);
33
-
34
- // Cooldown timer
35
- useEffect(() => {
36
- if (cooldown <= 0) return;
37
- const timer = setInterval(() => {
38
- setCooldown((prev) => prev - 1);
39
- }, 1000);
40
- return () => clearInterval(timer);
41
- }, [cooldown]);
42
-
43
- const handleSubmit = useCallback(
44
- async (code: string) => {
45
- if (code.length !== CODE_LENGTH || loading) return;
46
-
47
- try {
48
- setLoading(true);
49
- setError(null);
50
- // Auth token is in httpOnly cookie — proxy adds Authorization header
51
- const result = await proxyVerifyEmail(code);
52
-
53
- if (result.verified) {
54
- // Refresh auth state (cookie already set)
55
- await auth.login();
56
- setSuccess('Email verified successfully! Redirecting...');
57
- setTimeout(() => router.push('/'), 1500);
58
- } else {
59
- setError(result.message || 'Verification failed. Please try again.');
60
- }
61
- } catch (err) {
62
- const message =
63
- err instanceof Error ? err.message : 'Verification failed. Please try again.';
64
- setError(message);
65
- } finally {
66
- setLoading(false);
67
- }
68
- },
69
- [loading, auth, router]
70
- );
71
-
72
- function handleDigitChange(index: number, value: string) {
73
- // Allow only single digits
74
- const digit = value.replace(/\D/g, '').slice(-1);
75
-
76
- const newDigits = [...digits];
77
- newDigits[index] = digit;
78
- setDigits(newDigits);
79
-
80
- // Auto-focus next input
81
- if (digit && index < CODE_LENGTH - 1) {
82
- inputRefs.current[index + 1]?.focus();
83
- }
84
-
85
- // Auto-submit when all digits are filled
86
- const fullCode = newDigits.join('');
87
- if (fullCode.length === CODE_LENGTH) {
88
- handleSubmit(fullCode);
89
- }
90
- }
91
-
92
- function handleKeyDown(index: number, e: React.KeyboardEvent<HTMLInputElement>) {
93
- if (e.key === 'Backspace' && !digits[index] && index > 0) {
94
- // Move focus to previous input on backspace when current is empty
95
- inputRefs.current[index - 1]?.focus();
96
- }
97
- }
98
-
99
- function handlePaste(e: React.ClipboardEvent) {
100
- e.preventDefault();
101
- const pastedText = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, CODE_LENGTH);
102
-
103
- if (pastedText.length === 0) return;
104
-
105
- const newDigits = [...digits];
106
- for (let i = 0; i < pastedText.length; i++) {
107
- newDigits[i] = pastedText[i];
108
- }
109
- setDigits(newDigits);
110
-
111
- // Focus the next empty input, or the last filled one
112
- const nextEmptyIndex = newDigits.findIndex((d) => !d);
113
- const focusIndex = nextEmptyIndex === -1 ? CODE_LENGTH - 1 : nextEmptyIndex;
114
- inputRefs.current[focusIndex]?.focus();
115
-
116
- // Auto-submit if all digits pasted
117
- if (pastedText.length === CODE_LENGTH) {
118
- handleSubmit(pastedText);
119
- }
120
- }
121
-
122
- async function handleResend() {
123
- if (resending || cooldown > 0) return;
124
-
125
- try {
126
- setResending(true);
127
- setError(null);
128
- await proxyResendVerification();
129
- setSuccess('Verification code sent! Check your email.');
130
- setCooldown(RESEND_COOLDOWN_SECONDS);
131
- // Clear digits for fresh entry
132
- setDigits(Array(CODE_LENGTH).fill(''));
133
- inputRefs.current[0]?.focus();
134
- } catch (err) {
135
- const message = err instanceof Error ? err.message : 'Failed to resend code.';
136
- setError(message);
137
- } finally {
138
- setResending(false);
139
- }
140
- }
141
-
142
- function handleFormSubmit(e: React.FormEvent) {
143
- e.preventDefault();
144
- const code = digits.join('');
145
- handleSubmit(code);
146
- }
147
-
148
- return (
149
- <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
150
- <div className="w-full max-w-md space-y-6">
151
- <div className="text-center">
152
- <svg
153
- className="text-primary mx-auto mb-3 h-12 w-12"
154
- fill="none"
155
- viewBox="0 0 24 24"
156
- stroke="currentColor"
157
- >
158
- <path
159
- strokeLinecap="round"
160
- strokeLinejoin="round"
161
- strokeWidth={1.5}
162
- d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
163
- />
164
- </svg>
165
- <h1 className="text-foreground text-2xl font-bold">{t('verifyTitle')}</h1>
166
- <p className="text-muted-foreground mt-1 text-sm">{t('verifySubtitle')}</p>
167
- </div>
168
-
169
- {error && (
170
- <div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
171
- {error}
172
- </div>
173
- )}
174
-
175
- {success && (
176
- <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">
177
- {success}
178
- </div>
179
- )}
180
-
181
- <form onSubmit={handleFormSubmit} className="space-y-6">
182
- {/* Digit inputs */}
183
- <div dir="ltr" className="flex justify-center gap-2 sm:gap-3" onPaste={handlePaste}>
184
- {digits.map((digit, index) => (
185
- <input
186
- key={index}
187
- ref={(el) => {
188
- inputRefs.current[index] = el;
189
- }}
190
- type="text"
191
- inputMode="numeric"
192
- autoComplete="one-time-code"
193
- maxLength={1}
194
- value={digit}
195
- onChange={(e) => handleDigitChange(index, e.target.value)}
196
- onKeyDown={(e) => handleKeyDown(index, e)}
197
- disabled={loading}
198
- className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-12 w-11 rounded border text-center text-xl font-semibold focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-14 sm:w-12"
199
- aria-label={`${t('digitAriaLabel')} ${index + 1}`}
200
- />
201
- ))}
202
- </div>
203
-
204
- <button
205
- type="submit"
206
- disabled={loading || digits.join('').length !== CODE_LENGTH}
207
- 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"
208
- >
209
- {loading ? (
210
- <>
211
- <LoadingSpinner
212
- size="sm"
213
- className="border-primary-foreground/30 border-t-primary-foreground"
214
- />
215
- {t('verifying')}
216
- </>
217
- ) : (
218
- t('verifyButton')
219
- )}
220
- </button>
221
- </form>
222
-
223
- {/* Resend code */}
224
- <div className="text-center">
225
- <p className="text-muted-foreground text-sm">
226
- {t('didntReceive')}{' '}
227
- <button
228
- type="button"
229
- onClick={handleResend}
230
- disabled={resending || cooldown > 0}
231
- className="text-primary font-medium hover:underline disabled:cursor-not-allowed disabled:no-underline disabled:opacity-50"
232
- >
233
- {resending
234
- ? t('sending')
235
- : cooldown > 0
236
- ? `${t('resendIn')} ${cooldown}${t('secondsSuffix')}`
237
- : t('resendCode')}
238
- </button>
239
- </p>
240
- </div>
241
- </div>
242
- </div>
243
- );
244
- }
245
-
246
- export default function VerifyEmailPage() {
247
- return (
248
- <Suspense
249
- fallback={
250
- <div className="flex min-h-[60vh] items-center justify-center">
251
- <LoadingSpinner size="lg" />
252
- </div>
253
- }
254
- >
255
- <VerifyEmailContent />
256
- </Suspense>
257
- );
258
- }
1
+ 'use client';
2
+
3
+ import { Suspense, useState, useRef, useEffect, useCallback } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { useAuth } from '@/providers/store-provider';
7
+ import { proxyVerifyEmail, proxyResendVerification } from '@/lib/auth';
8
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
9
+ import { useTranslations } from '@/lib/translations';
10
+
11
+ const CODE_LENGTH = 6;
12
+ const RESEND_COOLDOWN_SECONDS = 60;
13
+
14
+ function VerifyEmailContent() {
15
+ const router = useRouter();
16
+ const auth = useAuth();
17
+
18
+ const t = useTranslations('auth');
19
+
20
+ const [digits, setDigits] = useState<string[]>(Array(CODE_LENGTH).fill(''));
21
+ const [loading, setLoading] = useState(false);
22
+ const [resending, setResending] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+ const [success, setSuccess] = useState<string | null>(null);
25
+ const [cooldown, setCooldown] = useState(0);
26
+
27
+ const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
28
+
29
+ // Auto-focus first input on mount
30
+ useEffect(() => {
31
+ inputRefs.current[0]?.focus();
32
+ }, []);
33
+
34
+ // Cooldown timer
35
+ useEffect(() => {
36
+ if (cooldown <= 0) return;
37
+ const timer = setInterval(() => {
38
+ setCooldown((prev) => prev - 1);
39
+ }, 1000);
40
+ return () => clearInterval(timer);
41
+ }, [cooldown]);
42
+
43
+ const handleSubmit = useCallback(
44
+ async (code: string) => {
45
+ if (code.length !== CODE_LENGTH || loading) return;
46
+
47
+ try {
48
+ setLoading(true);
49
+ setError(null);
50
+ // Auth token is in httpOnly cookie — proxy adds Authorization header
51
+ const result = await proxyVerifyEmail(code);
52
+
53
+ if (result.verified) {
54
+ // Refresh auth state (cookie already set)
55
+ await auth.login();
56
+ setSuccess('Email verified successfully! Redirecting...');
57
+ setTimeout(() => router.push('/'), 1500);
58
+ } else {
59
+ setError(result.message || 'Verification failed. Please try again.');
60
+ }
61
+ } catch (err) {
62
+ const message =
63
+ err instanceof Error ? err.message : 'Verification failed. Please try again.';
64
+ setError(message);
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ },
69
+ [loading, auth, router]
70
+ );
71
+
72
+ function handleDigitChange(index: number, value: string) {
73
+ // Allow only single digits
74
+ const digit = value.replace(/\D/g, '').slice(-1);
75
+
76
+ const newDigits = [...digits];
77
+ newDigits[index] = digit;
78
+ setDigits(newDigits);
79
+
80
+ // Auto-focus next input
81
+ if (digit && index < CODE_LENGTH - 1) {
82
+ inputRefs.current[index + 1]?.focus();
83
+ }
84
+
85
+ // Auto-submit when all digits are filled
86
+ const fullCode = newDigits.join('');
87
+ if (fullCode.length === CODE_LENGTH) {
88
+ handleSubmit(fullCode);
89
+ }
90
+ }
91
+
92
+ function handleKeyDown(index: number, e: React.KeyboardEvent<HTMLInputElement>) {
93
+ if (e.key === 'Backspace' && !digits[index] && index > 0) {
94
+ // Move focus to previous input on backspace when current is empty
95
+ inputRefs.current[index - 1]?.focus();
96
+ }
97
+ }
98
+
99
+ function handlePaste(e: React.ClipboardEvent) {
100
+ e.preventDefault();
101
+ const pastedText = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, CODE_LENGTH);
102
+
103
+ if (pastedText.length === 0) return;
104
+
105
+ const newDigits = [...digits];
106
+ for (let i = 0; i < pastedText.length; i++) {
107
+ newDigits[i] = pastedText[i];
108
+ }
109
+ setDigits(newDigits);
110
+
111
+ // Focus the next empty input, or the last filled one
112
+ const nextEmptyIndex = newDigits.findIndex((d) => !d);
113
+ const focusIndex = nextEmptyIndex === -1 ? CODE_LENGTH - 1 : nextEmptyIndex;
114
+ inputRefs.current[focusIndex]?.focus();
115
+
116
+ // Auto-submit if all digits pasted
117
+ if (pastedText.length === CODE_LENGTH) {
118
+ handleSubmit(pastedText);
119
+ }
120
+ }
121
+
122
+ async function handleResend() {
123
+ if (resending || cooldown > 0) return;
124
+
125
+ try {
126
+ setResending(true);
127
+ setError(null);
128
+ await proxyResendVerification();
129
+ setSuccess('Verification code sent! Check your email.');
130
+ setCooldown(RESEND_COOLDOWN_SECONDS);
131
+ // Clear digits for fresh entry
132
+ setDigits(Array(CODE_LENGTH).fill(''));
133
+ inputRefs.current[0]?.focus();
134
+ } catch (err) {
135
+ const message = err instanceof Error ? err.message : 'Failed to resend code.';
136
+ setError(message);
137
+ } finally {
138
+ setResending(false);
139
+ }
140
+ }
141
+
142
+ function handleFormSubmit(e: React.FormEvent) {
143
+ e.preventDefault();
144
+ const code = digits.join('');
145
+ handleSubmit(code);
146
+ }
147
+
148
+ return (
149
+ <div className="flex min-h-[60vh] items-center justify-center px-4 py-12">
150
+ <div className="w-full max-w-md space-y-6">
151
+ <div className="text-center">
152
+ <svg
153
+ className="text-primary mx-auto mb-3 h-12 w-12"
154
+ fill="none"
155
+ viewBox="0 0 24 24"
156
+ stroke="currentColor"
157
+ >
158
+ <path
159
+ strokeLinecap="round"
160
+ strokeLinejoin="round"
161
+ strokeWidth={1.5}
162
+ d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
163
+ />
164
+ </svg>
165
+ <h1 className="text-foreground text-2xl font-bold">{t('verifyTitle')}</h1>
166
+ <p className="text-muted-foreground mt-1 text-sm">{t('verifySubtitle')}</p>
167
+ </div>
168
+
169
+ {error && (
170
+ <div className="bg-destructive/10 border-destructive/20 text-destructive rounded-lg border px-4 py-3 text-sm">
171
+ {error}
172
+ </div>
173
+ )}
174
+
175
+ {success && (
176
+ <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">
177
+ {success}
178
+ </div>
179
+ )}
180
+
181
+ <form onSubmit={handleFormSubmit} className="space-y-6">
182
+ {/* Digit inputs */}
183
+ <div dir="ltr" className="flex justify-center gap-2 sm:gap-3" onPaste={handlePaste}>
184
+ {digits.map((digit, index) => (
185
+ <input
186
+ key={index}
187
+ ref={(el) => {
188
+ inputRefs.current[index] = el;
189
+ }}
190
+ type="text"
191
+ inputMode="numeric"
192
+ autoComplete="one-time-code"
193
+ maxLength={1}
194
+ value={digit}
195
+ onChange={(e) => handleDigitChange(index, e.target.value)}
196
+ onKeyDown={(e) => handleKeyDown(index, e)}
197
+ disabled={loading}
198
+ className="border-border bg-background text-foreground focus:ring-primary/20 focus:border-primary h-12 w-11 rounded border text-center text-xl font-semibold focus:outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50 sm:h-14 sm:w-12"
199
+ aria-label={`${t('digitAriaLabel')} ${index + 1}`}
200
+ />
201
+ ))}
202
+ </div>
203
+
204
+ <button
205
+ type="submit"
206
+ disabled={loading || digits.join('').length !== CODE_LENGTH}
207
+ 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"
208
+ >
209
+ {loading ? (
210
+ <>
211
+ <LoadingSpinner
212
+ size="sm"
213
+ className="border-primary-foreground/30 border-t-primary-foreground"
214
+ />
215
+ {t('verifying')}
216
+ </>
217
+ ) : (
218
+ t('verifyButton')
219
+ )}
220
+ </button>
221
+ </form>
222
+
223
+ {/* Resend code */}
224
+ <div className="text-center">
225
+ <p className="text-muted-foreground text-sm">
226
+ {t('didntReceive')}{' '}
227
+ <button
228
+ type="button"
229
+ onClick={handleResend}
230
+ disabled={resending || cooldown > 0}
231
+ className="text-primary font-medium hover:underline disabled:cursor-not-allowed disabled:no-underline disabled:opacity-50"
232
+ >
233
+ {resending
234
+ ? t('sending')
235
+ : cooldown > 0
236
+ ? `${t('resendIn')} ${cooldown}${t('secondsSuffix')}`
237
+ : t('resendCode')}
238
+ </button>
239
+ </p>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ );
244
+ }
245
+
246
+ export default function VerifyEmailPage() {
247
+ return (
248
+ <Suspense
249
+ fallback={
250
+ <div className="flex min-h-[60vh] items-center justify-center">
251
+ <LoadingSpinner size="lg" />
252
+ </div>
253
+ }
254
+ >
255
+ <VerifyEmailContent />
256
+ </Suspense>
257
+ );
258
+ }