@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.
- package/LICENSE +21 -0
- package/README.md +557 -0
- package/package.json +35 -0
- package/src/auth/ForgotPasswordPage.jsx +176 -0
- package/src/auth/LoginPage.jsx +265 -0
- package/src/auth/RegisterPage.jsx +302 -0
- package/src/auth/ResetPasswordPage.jsx +158 -0
- package/src/auth/Verify2FAPage.jsx +175 -0
- package/src/dashboard/BillingPage.jsx +345 -0
- package/src/dashboard/DashboardHome.jsx +246 -0
- package/src/dashboard/NotificationsPage.jsx +230 -0
- package/src/dashboard/ProfilePage.jsx +272 -0
- package/src/dashboard/SettingsPage.jsx +415 -0
- package/src/dashboard/UsersTablePage.jsx +317 -0
- package/src/extra/CareersPage.jsx +166 -0
- package/src/extra/ChangelogPage.jsx +127 -0
- package/src/extra/CheckoutPage.jsx +383 -0
- package/src/extra/ComingSoonPage.jsx +147 -0
- package/src/extra/ErrorPage.jsx +91 -0
- package/src/extra/InvoicePage.jsx +241 -0
- package/src/extra/LegalPage.jsx +115 -0
- package/src/extra/MaintenancePage.jsx +172 -0
- package/src/extra/OnboardingPage.jsx +197 -0
- package/src/extra/StatusPage.jsx +181 -0
- package/src/index.js +53 -0
- package/src/layouts/AuthLayout.jsx +104 -0
- package/src/layouts/DashboardLayout.jsx +249 -0
- package/src/layouts/MarketingLayout.jsx +184 -0
- package/src/marketing/AboutPage.jsx +189 -0
- package/src/marketing/BlogPage.jsx +182 -0
- package/src/marketing/BlogPostPage.jsx +192 -0
- package/src/marketing/ContactPage.jsx +546 -0
- package/src/marketing/DocsPage.jsx +256 -0
- package/src/marketing/HomePage.jsx +702 -0
- package/src/marketing/LandingPage.jsx +755 -0
- 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
|
+
}
|