@imansi/templates 0.1.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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +557 -0
  3. package/package.json +35 -0
  4. package/src/auth/ForgotPasswordPage.jsx +176 -0
  5. package/src/auth/LoginPage.jsx +265 -0
  6. package/src/auth/RegisterPage.jsx +302 -0
  7. package/src/auth/ResetPasswordPage.jsx +158 -0
  8. package/src/auth/Verify2FAPage.jsx +175 -0
  9. package/src/dashboard/BillingPage.jsx +345 -0
  10. package/src/dashboard/DashboardHome.jsx +246 -0
  11. package/src/dashboard/NotificationsPage.jsx +230 -0
  12. package/src/dashboard/ProfilePage.jsx +272 -0
  13. package/src/dashboard/SettingsPage.jsx +415 -0
  14. package/src/dashboard/UsersTablePage.jsx +317 -0
  15. package/src/extra/CareersPage.jsx +166 -0
  16. package/src/extra/ChangelogPage.jsx +127 -0
  17. package/src/extra/CheckoutPage.jsx +383 -0
  18. package/src/extra/ComingSoonPage.jsx +147 -0
  19. package/src/extra/ErrorPage.jsx +91 -0
  20. package/src/extra/InvoicePage.jsx +241 -0
  21. package/src/extra/LegalPage.jsx +115 -0
  22. package/src/extra/MaintenancePage.jsx +172 -0
  23. package/src/extra/OnboardingPage.jsx +197 -0
  24. package/src/extra/StatusPage.jsx +181 -0
  25. package/src/index.js +53 -0
  26. package/src/layouts/AuthLayout.jsx +104 -0
  27. package/src/layouts/DashboardLayout.jsx +249 -0
  28. package/src/layouts/MarketingLayout.jsx +184 -0
  29. package/src/marketing/AboutPage.jsx +189 -0
  30. package/src/marketing/BlogPage.jsx +182 -0
  31. package/src/marketing/BlogPostPage.jsx +192 -0
  32. package/src/marketing/ContactPage.jsx +546 -0
  33. package/src/marketing/DocsPage.jsx +256 -0
  34. package/src/marketing/HomePage.jsx +702 -0
  35. package/src/marketing/LandingPage.jsx +755 -0
  36. package/src/marketing/PricingPage.jsx +205 -0
@@ -0,0 +1,302 @@
1
+ import { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import {
4
+ Alert,
5
+ Button,
6
+ Card,
7
+ CardContent,
8
+ Checkbox,
9
+ Input,
10
+ Label,
11
+ } from '@imansi/ui';
12
+ import { AuthLayout } from '../layouts/AuthLayout.jsx';
13
+
14
+ export function RegisterPage({
15
+ title = 'Crear cuenta',
16
+ subtitle = 'Completá tus datos para empezar. Es gratis.',
17
+ submitText = 'Crear cuenta',
18
+ submitLoadingText = 'Creando cuenta...',
19
+ nameLabel = 'Nombre completo',
20
+ emailLabel = 'Correo electrónico',
21
+ passwordLabel = 'Contraseña',
22
+ passwordConfirmLabel = 'Confirmar contraseña',
23
+ termsText = 'Acepto los Términos y la Política de Privacidad',
24
+ loginText = '¿Ya tenés cuenta?',
25
+ loginLinkText = 'Iniciar sesión',
26
+
27
+ loginHref = '/login',
28
+
29
+ brandName,
30
+ brandTagline,
31
+ brandLogo,
32
+
33
+ oauthProviders = [],
34
+ oauthTexts = {
35
+ github: 'Registrarme con GitHub',
36
+ google: 'Registrarme con Google',
37
+ o: 'o registrate con',
38
+ },
39
+
40
+ onSubmit,
41
+ onOAuthClick,
42
+ error: externalError,
43
+ loading = false,
44
+ showName = true,
45
+ showTerms = true,
46
+
47
+ footer,
48
+ className,
49
+ }) {
50
+ const [nombre, setNombre] = useState('');
51
+ const [correo, setCorreo] = useState('');
52
+ const [contrasena, setContrasena] = useState('');
53
+ const [contrasenaRepetir, setContrasenaRepetir] = useState('');
54
+ const [aceptaTerminos, setAceptaTerminos] = useState(false);
55
+ const [localError, setLocalError] = useState(null);
56
+
57
+ async function handleSubmit(e) {
58
+ e.preventDefault();
59
+ setLocalError(null);
60
+
61
+ if (!correo || !contrasena) {
62
+ setLocalError('Completá todos los campos obligatorios.');
63
+ return;
64
+ }
65
+
66
+ if (contrasena.length < 8) {
67
+ setLocalError('La contraseña debe tener al menos 8 caracteres.');
68
+ return;
69
+ }
70
+
71
+ if (contrasena !== contrasenaRepetir) {
72
+ setLocalError('Las contraseñas no coinciden.');
73
+ return;
74
+ }
75
+
76
+ if (showTerms && !aceptaTerminos) {
77
+ setLocalError('Debés aceptar los términos para continuar.');
78
+ return;
79
+ }
80
+
81
+ if (onSubmit) {
82
+ await onSubmit({ nombre, correo, contrasena, aceptaTerminos });
83
+ }
84
+ }
85
+
86
+ function handleOAuth(provider) {
87
+ if (onOAuthClick) onOAuthClick(provider);
88
+ }
89
+
90
+ const error = localError || externalError;
91
+
92
+ return (
93
+ <AuthLayout
94
+ brandName={brandName}
95
+ brandTagline={brandTagline}
96
+ brandLogo={brandLogo}
97
+ className={className}
98
+ >
99
+ <div className="lg:hidden flex items-center gap-3 justify-center mb-6">
100
+ {brandLogo || (
101
+ <div className="h-10 w-10 rounded-lg bg-primary text-primary-foreground flex items-center justify-center font-bold">
102
+ {brandName?.charAt(0) || 'I'}
103
+ </div>
104
+ )}
105
+ <span className="text-h4 font-semibold">{brandName}</span>
106
+ </div>
107
+
108
+ <Card className="shadow-lg border-border/50">
109
+ <CardContent className="p-6 sm:p-7">
110
+ <div className="space-y-5">
111
+ <div className="space-y-1.5">
112
+ <h1 className="text-h3 font-semibold tracking-tight">{title}</h1>
113
+ <p className="text-small text-muted-foreground leading-snug">
114
+ {subtitle}
115
+ </p>
116
+ </div>
117
+
118
+ {error && (
119
+ <Alert variant="destructive" title="Error">
120
+ {error}
121
+ </Alert>
122
+ )}
123
+
124
+ {oauthProviders.length > 0 && (
125
+ <div className="space-y-4">
126
+ <div className="grid gap-2.5">
127
+ {oauthProviders.includes('github') && (
128
+ <Button
129
+ type="button"
130
+ variant="outline"
131
+ fullWidth
132
+ onClick={() => handleOAuth('github')}
133
+ disabled={loading}
134
+ >
135
+ <GitHubIcon />
136
+ {oauthTexts.github}
137
+ </Button>
138
+ )}
139
+ {oauthProviders.includes('google') && (
140
+ <Button
141
+ type="button"
142
+ variant="outline"
143
+ fullWidth
144
+ onClick={() => handleOAuth('google')}
145
+ disabled={loading}
146
+ >
147
+ <GoogleIcon />
148
+ {oauthTexts.google}
149
+ </Button>
150
+ )}
151
+ </div>
152
+
153
+ <div className="relative">
154
+ <div className="absolute inset-0 flex items-center">
155
+ <span className="w-full border-t border-border" />
156
+ </div>
157
+ <div className="relative flex justify-center text-caption uppercase">
158
+ <span className="bg-card px-3 text-muted-foreground tracking-widest text-[10px]">
159
+ {oauthTexts.o}
160
+ </span>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ )}
165
+
166
+ <form onSubmit={handleSubmit} className="space-y-4">
167
+ {showName && (
168
+ <div className="space-y-1.5">
169
+ <Label htmlFor="reg-nombre" className="text-small font-medium">
170
+ {nameLabel}
171
+ </Label>
172
+ <Input
173
+ id="reg-nombre"
174
+ type="text"
175
+ placeholder="Juan Pérez"
176
+ value={nombre}
177
+ onChange={(e) => setNombre(e.target.value)}
178
+ autoComplete="name"
179
+ autoFocus
180
+ disabled={loading}
181
+ />
182
+ </div>
183
+ )}
184
+
185
+ <div className="space-y-1.5">
186
+ <Label htmlFor="reg-correo" className="text-small font-medium">
187
+ {emailLabel}
188
+ </Label>
189
+ <Input
190
+ id="reg-correo"
191
+ type="email"
192
+ placeholder="correo@ejemplo.com"
193
+ value={correo}
194
+ onChange={(e) => setCorreo(e.target.value)}
195
+ autoComplete="email"
196
+ disabled={loading}
197
+ />
198
+ </div>
199
+
200
+ <div className="grid gap-4 sm:grid-cols-2">
201
+ <div className="space-y-1.5">
202
+ <Label
203
+ htmlFor="reg-contrasena"
204
+ className="text-small font-medium"
205
+ >
206
+ {passwordLabel}
207
+ </Label>
208
+ <Input
209
+ id="reg-contrasena"
210
+ type="password"
211
+ placeholder="••••••••"
212
+ value={contrasena}
213
+ onChange={(e) => setContrasena(e.target.value)}
214
+ autoComplete="new-password"
215
+ disabled={loading}
216
+ />
217
+ </div>
218
+
219
+ <div className="space-y-1.5">
220
+ <Label
221
+ htmlFor="reg-contrasena-repetir"
222
+ className="text-small font-medium"
223
+ >
224
+ {passwordConfirmLabel}
225
+ </Label>
226
+ <Input
227
+ id="reg-contrasena-repetir"
228
+ type="password"
229
+ placeholder="••••••••"
230
+ value={contrasenaRepetir}
231
+ onChange={(e) => setContrasenaRepetir(e.target.value)}
232
+ autoComplete="new-password"
233
+ disabled={loading}
234
+ />
235
+ </div>
236
+ </div>
237
+
238
+ {showTerms && (
239
+ <div className="flex items-start gap-2.5">
240
+ <Checkbox
241
+ id="reg-terminos"
242
+ checked={aceptaTerminos}
243
+ onCheckedChange={setAceptaTerminos}
244
+ disabled={loading}
245
+ className="mt-0.5"
246
+ />
247
+ <Label
248
+ htmlFor="reg-terminos"
249
+ className="cursor-pointer font-normal text-small text-muted-foreground leading-snug"
250
+ >
251
+ {termsText}
252
+ </Label>
253
+ </div>
254
+ )}
255
+
256
+ <Button type="submit" fullWidth disabled={loading}>
257
+ {loading ? submitLoadingText : submitText}
258
+ </Button>
259
+ </form>
260
+ </div>
261
+ </CardContent>
262
+ </Card>
263
+
264
+ {loginHref && (
265
+ <p className="text-center text-small text-muted-foreground mt-5">
266
+ {loginText}{' '}
267
+ <Link
268
+ to={loginHref}
269
+ className="text-primary font-medium hover:underline underline-offset-4"
270
+ >
271
+ {loginLinkText}
272
+ </Link>
273
+ </p>
274
+ )}
275
+
276
+ {footer && (
277
+ <p className="text-center text-caption text-muted-foreground mt-3 px-2">
278
+ {footer}
279
+ </p>
280
+ )}
281
+ </AuthLayout>
282
+ );
283
+ }
284
+
285
+ function GitHubIcon() {
286
+ return (
287
+ <svg viewBox="0 0 24 24" fill="currentColor" className="size-4">
288
+ <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.55v-1.94c-3.2.7-3.87-1.54-3.87-1.54-.52-1.33-1.28-1.68-1.28-1.68-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.23-1.28-5.23-5.7 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.05 11.05 0 0 1 5.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.69 5.4-5.25 5.69.41.35.78 1.05.78 2.12v3.15c0 .3.21.66.8.55C20.21 21.39 23.5 17.08 23.5 12 23.5 5.65 18.35.5 12 .5Z" />
289
+ </svg>
290
+ );
291
+ }
292
+
293
+ function GoogleIcon() {
294
+ return (
295
+ <svg viewBox="0 0 24 24" className="size-4">
296
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1Z" />
297
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A11 11 0 0 0 12 23Z" />
298
+ <path fill="#FBBC05" d="M5.84 14.1a6.6 6.6 0 0 1 0-4.2V7.06H2.18a11 11 0 0 0 0 9.88l3.66-2.84Z" />
299
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.06l3.66 2.84C6.71 7.31 9.14 5.38 12 5.38Z" />
300
+ </svg>
301
+ );
302
+ }
@@ -0,0 +1,158 @@
1
+ import { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import {
4
+ Alert,
5
+ Button,
6
+ Card,
7
+ CardContent,
8
+ Input,
9
+ Label,
10
+ } from '@imansi/ui';
11
+ import { AuthLayout } from '../layouts/AuthLayout.jsx';
12
+
13
+ export function ResetPasswordPage({
14
+ title = 'Nueva contraseña',
15
+ subtitle = 'Elegí una contraseña segura de al menos 8 caracteres.',
16
+ submitText = 'Restablecer contraseña',
17
+ submitLoadingText = 'Actualizando...',
18
+ passwordLabel = 'Nueva contraseña',
19
+ passwordConfirmLabel = 'Confirmar contraseña',
20
+
21
+ loginHref = '/login',
22
+ loginText = 'Volver al inicio de sesión',
23
+
24
+ brandName,
25
+ brandTagline,
26
+ brandLogo,
27
+
28
+ onSubmit,
29
+ error: externalError,
30
+ loading = false,
31
+
32
+ footer,
33
+ className,
34
+ }) {
35
+ const [contrasena, setContrasena] = useState('');
36
+ const [contrasenaRepetir, setContrasenaRepetir] = useState('');
37
+ const [localError, setLocalError] = useState(null);
38
+
39
+ async function handleSubmit(e) {
40
+ e.preventDefault();
41
+ setLocalError(null);
42
+
43
+ if (!contrasena || !contrasenaRepetir) {
44
+ setLocalError('Completá ambos campos.');
45
+ return;
46
+ }
47
+
48
+ if (contrasena.length < 8) {
49
+ setLocalError('La contraseña debe tener al menos 8 caracteres.');
50
+ return;
51
+ }
52
+
53
+ if (contrasena !== contrasenaRepetir) {
54
+ setLocalError('Las contraseñas no coinciden.');
55
+ return;
56
+ }
57
+
58
+ if (onSubmit) {
59
+ await onSubmit({ contrasena });
60
+ }
61
+ }
62
+
63
+ const error = localError || externalError;
64
+
65
+ return (
66
+ <AuthLayout
67
+ brandName={brandName}
68
+ brandTagline={brandTagline}
69
+ brandLogo={brandLogo}
70
+ className={className}
71
+ >
72
+ <div className="lg:hidden flex items-center gap-3 justify-center mb-6">
73
+ {brandLogo || (
74
+ <div className="h-10 w-10 rounded-lg bg-primary text-primary-foreground flex items-center justify-center font-bold">
75
+ {brandName?.charAt(0) || 'I'}
76
+ </div>
77
+ )}
78
+ <span className="text-h4 font-semibold">{brandName}</span>
79
+ </div>
80
+
81
+ <Card className="shadow-lg border-border/50">
82
+ <CardContent className="p-6 sm:p-7">
83
+ <div className="space-y-5">
84
+ <div className="space-y-1.5">
85
+ <h1 className="text-h3 font-semibold tracking-tight">{title}</h1>
86
+ <p className="text-small text-muted-foreground leading-snug">
87
+ {subtitle}
88
+ </p>
89
+ </div>
90
+
91
+ {error && (
92
+ <Alert variant="destructive" title="Error">
93
+ {error}
94
+ </Alert>
95
+ )}
96
+
97
+ <form onSubmit={handleSubmit} className="space-y-4">
98
+ <div className="space-y-1.5">
99
+ <Label htmlFor="reset-pass" className="text-small font-medium">
100
+ {passwordLabel}
101
+ </Label>
102
+ <Input
103
+ id="reset-pass"
104
+ type="password"
105
+ placeholder="••••••••"
106
+ value={contrasena}
107
+ onChange={(e) => setContrasena(e.target.value)}
108
+ autoComplete="new-password"
109
+ autoFocus
110
+ disabled={loading}
111
+ />
112
+ </div>
113
+
114
+ <div className="space-y-1.5">
115
+ <Label
116
+ htmlFor="reset-pass-repetir"
117
+ className="text-small font-medium"
118
+ >
119
+ {passwordConfirmLabel}
120
+ </Label>
121
+ <Input
122
+ id="reset-pass-repetir"
123
+ type="password"
124
+ placeholder="••••••••"
125
+ value={contrasenaRepetir}
126
+ onChange={(e) => setContrasenaRepetir(e.target.value)}
127
+ autoComplete="new-password"
128
+ disabled={loading}
129
+ />
130
+ </div>
131
+
132
+ <Button type="submit" fullWidth disabled={loading}>
133
+ {loading ? submitLoadingText : submitText}
134
+ </Button>
135
+ </form>
136
+ </div>
137
+ </CardContent>
138
+ </Card>
139
+
140
+ {loginHref && (
141
+ <p className="text-center text-small text-muted-foreground mt-5">
142
+ <Link
143
+ to={loginHref}
144
+ className="text-primary font-medium hover:underline underline-offset-4"
145
+ >
146
+ {loginText}
147
+ </Link>
148
+ </p>
149
+ )}
150
+
151
+ {footer && (
152
+ <p className="text-center text-caption text-muted-foreground mt-3 px-2">
153
+ {footer}
154
+ </p>
155
+ )}
156
+ </AuthLayout>
157
+ );
158
+ }
@@ -0,0 +1,175 @@
1
+ import { useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import {
4
+ Alert,
5
+ Button,
6
+ Card,
7
+ CardContent,
8
+ Input,
9
+ Label,
10
+ } from '@imansi/ui';
11
+ import { AuthLayout } from '../layouts/AuthLayout.jsx';
12
+
13
+ export function Verify2FAPage({
14
+ title = 'Verificación en dos pasos',
15
+ subtitle = 'Ingresá el código de 6 dígitos que enviamos a tu correo.',
16
+ submitText = 'Verificar',
17
+ submitLoadingText = 'Verificando...',
18
+ resendText = 'Reenviar código',
19
+ cancelText = 'Cancelar',
20
+ codeLabel = 'Código de verificación',
21
+ codeLength = 6,
22
+
23
+ cancelHref = '/login',
24
+
25
+ brandName,
26
+ brandTagline,
27
+ brandLogo,
28
+
29
+ onSubmit,
30
+ onResend,
31
+ error: externalError,
32
+ loading = false,
33
+
34
+ footer,
35
+ className,
36
+ }) {
37
+ const [codigo, setCodigo] = useState('');
38
+ const [localError, setLocalError] = useState(null);
39
+ const [resendMessage, setResendMessage] = useState(null);
40
+
41
+ // Solo acepta dígitos, máximo codeLength caracteres
42
+ function handleChange(e) {
43
+ const valor = e.target.value.replace(/\D/g, '').slice(0, codeLength);
44
+ setCodigo(valor);
45
+ }
46
+
47
+ async function handleSubmit(e) {
48
+ e.preventDefault();
49
+ setLocalError(null);
50
+
51
+ if (codigo.length !== codeLength) {
52
+ setLocalError(`Ingresá los ${codeLength} dígitos completos.`);
53
+ return;
54
+ }
55
+
56
+ if (onSubmit) {
57
+ await onSubmit({ codigo });
58
+ }
59
+ }
60
+
61
+ async function handleResend() {
62
+ setResendMessage(null);
63
+ if (onResend) {
64
+ await onResend();
65
+ }
66
+ setResendMessage('Código reenviado. Revisá tu correo.');
67
+ }
68
+
69
+ const error = localError || externalError;
70
+
71
+ return (
72
+ <AuthLayout
73
+ brandName={brandName}
74
+ brandTagline={brandTagline}
75
+ brandLogo={brandLogo}
76
+ className={className}
77
+ >
78
+ <div className="lg:hidden flex items-center gap-3 justify-center mb-6">
79
+ {brandLogo || (
80
+ <div className="h-10 w-10 rounded-lg bg-primary text-primary-foreground flex items-center justify-center font-bold">
81
+ {brandName?.charAt(0) || 'I'}
82
+ </div>
83
+ )}
84
+ <span className="text-h4 font-semibold">{brandName}</span>
85
+ </div>
86
+
87
+ <Card className="shadow-lg border-border/50">
88
+ <CardContent className="p-6 sm:p-7">
89
+ <div className="space-y-5">
90
+ <div className="space-y-1.5 text-center sm:text-left">
91
+ <h1 className="text-h3 font-semibold tracking-tight">{title}</h1>
92
+ <p className="text-small text-muted-foreground leading-snug">
93
+ {subtitle}
94
+ </p>
95
+ </div>
96
+
97
+ {error && (
98
+ <Alert variant="destructive" title="Error">
99
+ {error}
100
+ </Alert>
101
+ )}
102
+
103
+ {resendMessage && !error && (
104
+ <Alert variant="success" title="Listo">
105
+ {resendMessage}
106
+ </Alert>
107
+ )}
108
+
109
+ <form onSubmit={handleSubmit} className="space-y-4">
110
+ <div className="space-y-1.5">
111
+ <Label htmlFor="verify-codigo" className="text-small font-medium">
112
+ {codeLabel}
113
+ </Label>
114
+ <Input
115
+ id="verify-codigo"
116
+ type="text"
117
+ inputMode="numeric"
118
+ autoComplete="one-time-code"
119
+ pattern="[0-9]*"
120
+ maxLength={codeLength}
121
+ placeholder={'•'.repeat(codeLength)}
122
+ value={codigo}
123
+ onChange={handleChange}
124
+ autoFocus
125
+ disabled={loading}
126
+ className="text-center text-h4 tracking-[0.5em] font-mono"
127
+ />
128
+ <p className="text-caption text-muted-foreground">
129
+ {codigo.length} / {codeLength} dígitos
130
+ </p>
131
+ </div>
132
+
133
+ <Button
134
+ type="submit"
135
+ fullWidth
136
+ disabled={loading || codigo.length !== codeLength}
137
+ >
138
+ {loading ? submitLoadingText : submitText}
139
+ </Button>
140
+ </form>
141
+
142
+ <div className="flex flex-col sm:flex-row gap-3 sm:gap-2">
143
+ <Button
144
+ type="button"
145
+ variant="ghost"
146
+ fullWidth
147
+ onClick={handleResend}
148
+ disabled={loading}
149
+ >
150
+ {resendText}
151
+ </Button>
152
+ {cancelHref && (
153
+ <Button
154
+ type="button"
155
+ variant="ghost"
156
+ fullWidth
157
+ asChild
158
+ disabled={loading}
159
+ >
160
+ <Link to={cancelHref}>{cancelText}</Link>
161
+ </Button>
162
+ )}
163
+ </div>
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+
168
+ {footer && (
169
+ <p className="text-center text-caption text-muted-foreground mt-3 px-2">
170
+ {footer}
171
+ </p>
172
+ )}
173
+ </AuthLayout>
174
+ );
175
+ }