create-brainerce-store 1.18.0 → 1.20.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 (67) hide show
  1. package/LICENSE +0 -0
  2. package/dist/index.js +31 -9
  3. package/messages/en.json +366 -362
  4. package/messages/he.json +366 -362
  5. package/package.json +8 -8
  6. package/templates/nextjs/base/next.config.ts +31 -31
  7. package/templates/nextjs/base/scripts/fetch-store-info.mjs +81 -81
  8. package/templates/nextjs/base/src/app/.well-known/apple-developer-merchantid-domain-association/route.ts +26 -26
  9. package/templates/nextjs/base/src/app/account/layout.tsx +9 -9
  10. package/templates/nextjs/base/src/app/account/page.tsx +122 -122
  11. package/templates/nextjs/base/src/app/api/auth/logout/route.ts +14 -14
  12. package/templates/nextjs/base/src/app/api/auth/me/route.ts +56 -56
  13. package/templates/nextjs/base/src/app/api/auth/oauth-callback/route.ts +59 -59
  14. package/templates/nextjs/base/src/app/api/auth/reset-callback/route.ts +41 -41
  15. package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +77 -77
  16. package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +198 -198
  17. package/templates/nextjs/base/src/app/auth/callback/page.tsx +92 -92
  18. package/templates/nextjs/base/src/app/cart/layout.tsx +9 -9
  19. package/templates/nextjs/base/src/app/cart/page.tsx +204 -204
  20. package/templates/nextjs/base/src/app/checkout/layout.tsx +9 -9
  21. package/templates/nextjs/base/src/app/checkout/page.tsx +860 -860
  22. package/templates/nextjs/base/src/app/forgot-password/page.tsx +112 -112
  23. package/templates/nextjs/base/src/app/layout.tsx.ejs +75 -0
  24. package/templates/nextjs/base/src/app/login/layout.tsx +9 -9
  25. package/templates/nextjs/base/src/app/login/page.tsx +59 -59
  26. package/templates/nextjs/base/src/app/order-confirmation/layout.tsx +9 -9
  27. package/templates/nextjs/base/src/app/order-confirmation/page.tsx +17 -0
  28. package/templates/nextjs/base/src/app/payment-complete/page.tsx +59 -0
  29. package/templates/nextjs/base/src/app/products/[slug]/page.tsx +67 -67
  30. package/templates/nextjs/base/src/app/products/[slug]/product-client-section.tsx +486 -486
  31. package/templates/nextjs/base/src/app/products/layout.tsx +18 -18
  32. package/templates/nextjs/base/src/app/products/page.tsx +431 -431
  33. package/templates/nextjs/base/src/app/register/layout.tsx +9 -9
  34. package/templates/nextjs/base/src/app/register/page.tsx +65 -65
  35. package/templates/nextjs/base/src/app/reset-password/page.tsx +132 -132
  36. package/templates/nextjs/base/src/app/robots.ts +14 -14
  37. package/templates/nextjs/base/src/app/sitemap.ts +25 -25
  38. package/templates/nextjs/base/src/app/verify-email/page.tsx +258 -258
  39. package/templates/nextjs/base/src/components/account/address-book.tsx +432 -432
  40. package/templates/nextjs/base/src/components/account/order-history.tsx +350 -350
  41. package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +137 -137
  42. package/templates/nextjs/base/src/components/auth/register-form.tsx +232 -232
  43. package/templates/nextjs/base/src/components/cart/cart-bundle-offer.tsx +247 -111
  44. package/templates/nextjs/base/src/components/cart/cart-item.tsx +153 -153
  45. package/templates/nextjs/base/src/components/cart/cart-upgrade-banner.tsx +142 -142
  46. package/templates/nextjs/base/src/components/cart/free-shipping-bar.tsx +59 -59
  47. package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +415 -415
  48. package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +243 -83
  49. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +49 -3
  50. package/templates/nextjs/base/src/components/layout/footer.tsx +41 -41
  51. package/templates/nextjs/base/src/components/layout/header.tsx +336 -336
  52. package/templates/nextjs/base/src/components/layout/language-switcher.tsx.ejs +63 -0
  53. package/templates/nextjs/base/src/components/products/discount-badge.tsx +22 -22
  54. package/templates/nextjs/base/src/components/products/frequently-bought-together.tsx +202 -202
  55. package/templates/nextjs/base/src/components/products/product-card.tsx +218 -218
  56. package/templates/nextjs/base/src/components/products/recommendation-section.tsx +107 -107
  57. package/templates/nextjs/base/src/components/products/stock-badge.tsx +63 -63
  58. package/templates/nextjs/base/src/components/products/variant-selector.tsx +292 -292
  59. package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +72 -72
  60. package/templates/nextjs/base/src/i18n.ts.ejs +21 -0
  61. package/templates/nextjs/base/src/lib/auth.ts +149 -149
  62. package/templates/nextjs/base/src/lib/brainerce.ts.ejs +9 -0
  63. package/templates/nextjs/base/src/lib/translations.ts.ejs +31 -0
  64. package/templates/nextjs/base/src/middleware.ts.ejs +81 -0
  65. package/templates/nextjs/base/src/providers/store-provider.tsx.ejs +41 -0
  66. package/templates/nextjs/base/src/lib/translations.ts +0 -11
  67. 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
+ }