anima-ds-nucleus 1.0.17 → 1.0.19

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.
@@ -0,0 +1,341 @@
1
+ import { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '../../Atoms/Button/Button';
4
+ import { Typography } from '../../Atoms/Typography/Typography';
5
+ import { Icon } from '../../Atoms/Icon/Icon';
6
+
7
+ export const ForgotPassword = ({
8
+ onBackToLogin,
9
+ onSubmit,
10
+ loading: externalLoading,
11
+ error: externalError,
12
+ className = '',
13
+ variant = 'default',
14
+ ...props
15
+ }) => {
16
+ const { t } = useTranslation();
17
+ const isHexaLogin = variant === 'hexa-login';
18
+
19
+ // Estados internos (si no se pasan como props)
20
+ const [email, setEmail] = useState('');
21
+ const [errors, setErrors] = useState({});
22
+ const [internalLoading, setInternalLoading] = useState(false);
23
+ const [resetLink, setResetLink] = useState(null);
24
+ const [internalError, setInternalError] = useState(null);
25
+
26
+ // Usar loading externo o interno
27
+ const loading = externalLoading !== undefined ? externalLoading : internalLoading;
28
+ // Usar error externo o interno
29
+ const error = externalError !== undefined ? externalError : internalError;
30
+
31
+ // Funciones de validación
32
+ const hasAtSymbol = (email) => {
33
+ return email && email.includes('@');
34
+ };
35
+
36
+ const isValidEmail = (email) => {
37
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
38
+ return emailRegex.test(email);
39
+ };
40
+
41
+ const validateForm = () => {
42
+ const newErrors = {};
43
+
44
+ if (!email) {
45
+ newErrors.email = t('errors.required') ?? 'Este campo es obligatorio';
46
+ } else if (!isValidEmail(email)) {
47
+ newErrors.email = t('errors.invalidEmail') ?? 'El mail ingresado no es válido';
48
+ }
49
+
50
+ setErrors(newErrors);
51
+ return Object.keys(newErrors).length === 0;
52
+ };
53
+
54
+ const handleEmailChange = (e) => {
55
+ const value = e.target.value;
56
+ setEmail(value);
57
+
58
+ // Limpiar errores al cambiar
59
+ if (errors.email) {
60
+ setErrors((prev) => ({ ...prev, email: null }));
61
+ }
62
+ // Si hay error interno, limpiarlo
63
+ if (internalError) {
64
+ setInternalError(null);
65
+ }
66
+ // Si hay error externo, no lo manejamos aquí (el componente padre debe manejarlo)
67
+ };
68
+
69
+ const handleSubmit = async (e) => {
70
+ e.preventDefault();
71
+
72
+ if (!validateForm()) {
73
+ return;
74
+ }
75
+
76
+ if (!onSubmit) {
77
+ console.warn('ForgotPassword: onSubmit prop is required');
78
+ return;
79
+ }
80
+
81
+ // Usar loading interno si no se pasa como prop
82
+ if (externalLoading === undefined) {
83
+ setInternalLoading(true);
84
+ }
85
+ setErrors({});
86
+ setInternalError(null);
87
+
88
+ try {
89
+ const result = await onSubmit({ email, countryCode: null });
90
+
91
+ if (result?.resetLink) {
92
+ setResetLink(result.resetLink);
93
+ } else {
94
+ // Si no hay resetLink pero no hay error, asumir éxito
95
+ setResetLink('success');
96
+ }
97
+ } catch (err) {
98
+ let errorMessage = t('forgotPassword.errors.sendFailed') ?? 'Error al enviar el link. Intenta nuevamente.';
99
+
100
+ if (err?.response?.status === 404) {
101
+ errorMessage = err?.response?.data?.message ||
102
+ err?.message ||
103
+ 'No se encontró una cuenta asociada a este correo electrónico. Por favor, verifica que el correo esté correcto.';
104
+ } else if (err?.response?.status === 400) {
105
+ errorMessage = err?.response?.data?.message ||
106
+ err?.message ||
107
+ 'El correo electrónico ingresado no es válido.';
108
+ } else if (err?.response?.status >= 500) {
109
+ errorMessage = 'Error del servidor. Por favor, intenta más tarde.';
110
+ } else if (err?.message) {
111
+ errorMessage = err.message;
112
+ }
113
+
114
+ if (externalError === undefined) {
115
+ setInternalError(errorMessage);
116
+ }
117
+ // Si el error viene como prop, el componente padre lo maneja
118
+ } finally {
119
+ if (externalLoading === undefined) {
120
+ setInternalLoading(false);
121
+ }
122
+ }
123
+ };
124
+
125
+ return (
126
+ <div
127
+ className={`min-h-screen flex flex-col ${className}`}
128
+ style={{ backgroundColor: '#f5f5f5' }}
129
+ {...props}
130
+ >
131
+ <div className="flex-1 flex items-center justify-center p-4">
132
+ <div className="w-full max-w-md">
133
+ {/* Título y subtítulo */}
134
+ <div className="mb-8 text-center">
135
+ <Typography
136
+ variant="h3"
137
+ className="mb-2 font-semibold"
138
+ style={{
139
+ color: '#2D5C63',
140
+ fontFamily: 'inherit',
141
+ fontWeight: 400,
142
+ fontStyle: 'normal',
143
+ fontSize: '32px',
144
+ lineHeight: '48px',
145
+ letterSpacing: '0px',
146
+ textAlign: 'center',
147
+ }}
148
+ >
149
+ {t('forgotPassword.title') ?? 'Recuperar contraseña'}
150
+ </Typography>
151
+ <Typography
152
+ variant="body2"
153
+ className="text-gray-600"
154
+ style={{
155
+ fontFamily: 'inherit',
156
+ fontWeight: 400,
157
+ fontStyle: 'normal',
158
+ fontSize: '16px',
159
+ lineHeight: '24px',
160
+ letterSpacing: '0px',
161
+ textAlign: 'center',
162
+ }}
163
+ >
164
+ {t('forgotPassword.subtitle') ?? 'Ingresa tu correo electrónico y te enviaremos un link para restablecer tu contraseña.'}
165
+ </Typography>
166
+ </div>
167
+
168
+ {/* Card blanca */}
169
+ <div className="bg-white border border-gray-200 rounded-lg p-6 shadow-md">
170
+ {/* Estado de éxito */}
171
+ {resetLink && !loading && (
172
+ <div className="text-center space-y-4">
173
+ <div
174
+ className="rounded-lg p-4 border"
175
+ style={{
176
+ backgroundColor: '#d1fae5',
177
+ borderColor: '#10b981',
178
+ }}
179
+ >
180
+ <Typography
181
+ variant="body2"
182
+ className="font-medium"
183
+ style={{ color: '#065f46' }}
184
+ >
185
+ {t('forgotPassword.success.title') ?? 'Link generado'}
186
+ </Typography>
187
+ <Typography
188
+ variant="body2"
189
+ className="mt-2"
190
+ style={{ color: '#047857', fontSize: '14px' }}
191
+ >
192
+ {t('forgotPassword.success.message') ?? 'Se ha generado un link de recuperación. Haz click en el enlace a continuación para restablecer tu contraseña.'}
193
+ </Typography>
194
+ </div>
195
+ {resetLink !== 'success' && (
196
+ <a
197
+ href={resetLink}
198
+ className="inline-block px-4 py-2 rounded-lg font-medium text-white hover:opacity-90 transition-opacity"
199
+ style={{ backgroundColor: '#2D5C63' }}
200
+ >
201
+ {t('forgotPassword.success.linkLabel') ?? 'Ir a restablecer contraseña'}
202
+ </a>
203
+ )}
204
+ </div>
205
+ )}
206
+
207
+ {/* Estado de carga */}
208
+ {loading && (
209
+ <div className="text-center space-y-4">
210
+ <div
211
+ className="rounded-lg p-4 border"
212
+ style={{
213
+ backgroundColor: '#dbeafe',
214
+ borderColor: '#3b82f6',
215
+ }}
216
+ >
217
+ <Typography
218
+ variant="body2"
219
+ className="font-medium"
220
+ style={{ color: '#1e40af' }}
221
+ >
222
+ {t('forgotPassword.sending') ?? 'Enviando...'}
223
+ </Typography>
224
+ <Typography
225
+ variant="body2"
226
+ className="mt-2"
227
+ style={{ color: '#1e3a8a', fontSize: '14px' }}
228
+ >
229
+ {t('forgotPassword.waitingMessage') ?? 'Espera, estamos procesando tu solicitud...'}
230
+ </Typography>
231
+ <Typography
232
+ variant="body2"
233
+ className="mt-2"
234
+ style={{ color: '#1e3a8a', fontSize: '13px' }}
235
+ >
236
+ {t('forgotPassword.waitingSubmessage') ?? 'Hemos enviado un link de recuperación a tu correo. Esto puede tardar unos momentos.'}
237
+ </Typography>
238
+ </div>
239
+ </div>
240
+ )}
241
+
242
+ {/* Estado de formulario */}
243
+ {!resetLink && !loading && (
244
+ <>
245
+ {/* Botón volver */}
246
+ {onBackToLogin && (
247
+ <button
248
+ type="button"
249
+ onClick={onBackToLogin}
250
+ className="flex items-center gap-2 mb-6 text-body-md hover:underline font-medium cursor-pointer"
251
+ style={{ color: '#2D5C63' }}
252
+ >
253
+ <Icon name="ArrowLeftIcon" variant="24-outline" size={18} />
254
+ <span>{t('forgotPassword.backToLogin') ?? 'Volver al inicio de sesión'}</span>
255
+ </button>
256
+ )}
257
+
258
+ {/* Formulario */}
259
+ <form onSubmit={handleSubmit} className="space-y-4">
260
+ {/* Input de email */}
261
+ <div>
262
+ <label
263
+ className="block mb-2 font-medium text-sm"
264
+ style={{ color: '#2D5C63' }}
265
+ >
266
+ {t('login.email') ?? t('form.email') ?? 'Correo electrónico'}
267
+ </label>
268
+ <input
269
+ type="email"
270
+ placeholder={t('login.emailPlaceholder') ?? t('placeholder.email') ?? 'Ingresa tu correo electrónico'}
271
+ value={email}
272
+ onChange={handleEmailChange}
273
+ className="w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm text-gray-900"
274
+ style={{
275
+ borderColor: errors.email ? '#ef4444' : '#2D5C63',
276
+ borderWidth: '1px',
277
+ borderStyle: 'solid',
278
+ }}
279
+ />
280
+ {errors.email && (
281
+ <p className="mt-1 text-sm text-red-600">{errors.email}</p>
282
+ )}
283
+ </div>
284
+
285
+ {/* Error general */}
286
+ {error && (
287
+ <div
288
+ className="rounded-lg p-4 border"
289
+ style={{
290
+ backgroundColor: '#fecaca',
291
+ borderColor: '#ef4444',
292
+ }}
293
+ >
294
+ <Typography
295
+ variant="body2"
296
+ className="font-bold"
297
+ style={{ color: '#b91c1c' }}
298
+ >
299
+ {error}
300
+ </Typography>
301
+ </div>
302
+ )}
303
+
304
+ {/* Botón de envío */}
305
+ <div className="pt-4">
306
+ <Button
307
+ tipo="Primary"
308
+ color="Teal"
309
+ tamaño="Default"
310
+ type="submit"
311
+ className="w-full"
312
+ disabled={loading || !hasAtSymbol(email)}
313
+ >
314
+ <span
315
+ style={{
316
+ fontFamily: "'IBM Plex Sans', sans-serif",
317
+ fontWeight: 400,
318
+ fontStyle: 'normal',
319
+ fontSize: '16px',
320
+ lineHeight: '24px',
321
+ letterSpacing: '0%',
322
+ }}
323
+ >
324
+ {loading
325
+ ? t('forgotPassword.sending') ?? 'Enviando...'
326
+ : t('forgotPassword.button') ?? 'Enviar link'}
327
+ </span>
328
+ </Button>
329
+ </div>
330
+ </form>
331
+ </>
332
+ )}
333
+ </div>
334
+ </div>
335
+ </div>
336
+ </div>
337
+ );
338
+ };
339
+
340
+ export default ForgotPassword;
341
+
@@ -0,0 +1,186 @@
1
+ import { ForgotPassword } from './ForgotPassword';
2
+ import { I18nProvider } from '../../../providers/I18nProvider';
3
+
4
+ export default {
5
+ title: 'Views/ForgotPassword',
6
+ component: ForgotPassword,
7
+ tags: ['autodocs'],
8
+ decorators: [
9
+ (Story) => (
10
+ <I18nProvider language="es-AR">
11
+ <Story />
12
+ </I18nProvider>
13
+ ),
14
+ ],
15
+ argTypes: {
16
+ variant: {
17
+ control: 'select',
18
+ options: ['default', 'hexa-login'],
19
+ description: 'Variante del componente',
20
+ },
21
+ loading: {
22
+ control: 'boolean',
23
+ description: 'Estado de carga',
24
+ },
25
+ error: {
26
+ control: 'text',
27
+ description: 'Error general del servidor',
28
+ },
29
+ },
30
+ };
31
+
32
+ // Estado por defecto
33
+ export const Default = {
34
+ args: {
35
+ onBackToLogin: () => {
36
+ console.log('Back to login clicked');
37
+ alert('Volver al login');
38
+ },
39
+ onSubmit: async ({ email, countryCode }) => {
40
+ console.log('Forgot password data:', { email, countryCode });
41
+ await new Promise((resolve) => setTimeout(resolve, 2000));
42
+ return { resetLink: 'https://example.com/reset-password' };
43
+ },
44
+ },
45
+ };
46
+
47
+ // Estado de carga
48
+ export const Loading = {
49
+ args: {
50
+ loading: true,
51
+ onBackToLogin: () => {
52
+ alert('Volver al login');
53
+ },
54
+ onSubmit: async () => {
55
+ await new Promise((resolve) => setTimeout(resolve, 5000));
56
+ },
57
+ },
58
+ };
59
+
60
+ // Estado de éxito
61
+ export const Success = {
62
+ args: {
63
+ onBackToLogin: () => {
64
+ alert('Volver al login');
65
+ },
66
+ onSubmit: async ({ email }) => {
67
+ console.log('Email:', email);
68
+ await new Promise((resolve) => setTimeout(resolve, 1000));
69
+ return { resetLink: 'https://example.com/reset-password?token=abc123' };
70
+ },
71
+ },
72
+ };
73
+
74
+ // Con error del servidor
75
+ export const WithError = {
76
+ args: {
77
+ error: 'No se encontró una cuenta asociada a este correo electrónico.',
78
+ onBackToLogin: () => {
79
+ alert('Volver al login');
80
+ },
81
+ onSubmit: async () => {
82
+ throw new Error('Usuario no encontrado');
83
+ },
84
+ },
85
+ };
86
+
87
+ // Error 404
88
+ export const Error404 = {
89
+ args: {
90
+ onBackToLogin: () => {
91
+ alert('Volver al login');
92
+ },
93
+ onSubmit: async () => {
94
+ const error = new Error('Not found');
95
+ error.response = { status: 404, data: { message: 'Usuario no encontrado' } };
96
+ throw error;
97
+ },
98
+ },
99
+ };
100
+
101
+ // Error 400
102
+ export const Error400 = {
103
+ args: {
104
+ onBackToLogin: () => {
105
+ alert('Volver al login');
106
+ },
107
+ onSubmit: async () => {
108
+ const error = new Error('Bad request');
109
+ error.response = { status: 400, data: { message: 'Email inválido' } };
110
+ throw error;
111
+ },
112
+ },
113
+ };
114
+
115
+ // Error 500
116
+ export const Error500 = {
117
+ args: {
118
+ onBackToLogin: () => {
119
+ alert('Volver al login');
120
+ },
121
+ onSubmit: async () => {
122
+ const error = new Error('Server error');
123
+ error.response = { status: 500 };
124
+ throw error;
125
+ },
126
+ },
127
+ };
128
+
129
+ // Variante hexa-login
130
+ export const HexaLogin = {
131
+ args: {
132
+ variant: 'hexa-login',
133
+ onBackToLogin: () => {
134
+ alert('Volver al login');
135
+ },
136
+ onSubmit: async ({ email }) => {
137
+ console.log('Email:', email);
138
+ await new Promise((resolve) => setTimeout(resolve, 2000));
139
+ return { resetLink: 'https://example.com/reset-password' };
140
+ },
141
+ },
142
+ };
143
+
144
+ // Versión en español
145
+ export const Spanish = {
146
+ decorators: [
147
+ (Story) => (
148
+ <I18nProvider language="es-AR">
149
+ <Story />
150
+ </I18nProvider>
151
+ ),
152
+ ],
153
+ args: {
154
+ onBackToLogin: () => {
155
+ alert('Volver al login');
156
+ },
157
+ onSubmit: async ({ email }) => {
158
+ console.log('Email:', email);
159
+ await new Promise((resolve) => setTimeout(resolve, 2000));
160
+ return { resetLink: 'https://example.com/reset-password' };
161
+ },
162
+ },
163
+ };
164
+
165
+ // Versión en portugués
166
+ export const Portuguese = {
167
+ decorators: [
168
+ (Story) => (
169
+ <I18nProvider language="pt-BR">
170
+ <Story />
171
+ </I18nProvider>
172
+ ),
173
+ ],
174
+ args: {
175
+ onBackToLogin: () => {
176
+ alert('Volver ao login');
177
+ },
178
+ onSubmit: async ({ email }) => {
179
+ console.log('Email:', email);
180
+ await new Promise((resolve) => setTimeout(resolve, 2000));
181
+ return { resetLink: 'https://example.com/reset-password' };
182
+ },
183
+ },
184
+ };
185
+
186
+