614 lines
22 KiB
TypeScript
614 lines
22 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import { Button, Input, Card } from '../../ui';
|
|
import { PasswordCriteria, validatePassword, getPasswordErrors } from '../../ui/PasswordCriteria';
|
|
import { useAuthActions } from '../../../stores/auth.store';
|
|
import { useToast } from '../../../hooks/ui/useToast';
|
|
|
|
interface PasswordResetFormProps {
|
|
token?: string;
|
|
onSuccess?: () => void;
|
|
onBackClick?: () => void;
|
|
className?: string;
|
|
autoFocus?: boolean;
|
|
mode?: 'request' | 'reset';
|
|
}
|
|
|
|
export const PasswordResetForm: React.FC<PasswordResetFormProps> = ({
|
|
token,
|
|
onSuccess,
|
|
onBackClick,
|
|
className,
|
|
autoFocus = true,
|
|
mode
|
|
}) => {
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [isEmailSent, setIsEmailSent] = useState(false);
|
|
const [passwordStrength, setPasswordStrength] = useState(0);
|
|
const [isTokenValid, setIsTokenValid] = useState<boolean | null>(null);
|
|
|
|
const emailInputRef = useRef<HTMLInputElement>(null);
|
|
const passwordInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
// TODO: Implement password reset in Zustand auth store
|
|
// const { requestPasswordReset, resetPassword, isLoading, error } = useAuth();
|
|
const isLoading = false;
|
|
const error = null;
|
|
const { showToast } = useToast();
|
|
|
|
const isResetMode = Boolean(token) || mode === 'reset';
|
|
|
|
// Auto-focus on appropriate field when component mounts
|
|
useEffect(() => {
|
|
if (autoFocus) {
|
|
if (isResetMode && passwordInputRef.current) {
|
|
passwordInputRef.current.focus();
|
|
} else if (!isResetMode && emailInputRef.current) {
|
|
emailInputRef.current.focus();
|
|
}
|
|
}
|
|
}, [autoFocus, isResetMode]);
|
|
|
|
// Token validation effect
|
|
useEffect(() => {
|
|
if (token) {
|
|
// Simple token format validation (you might want to add actual API validation)
|
|
const isValidFormat = /^[A-Za-z0-9+/=_-]+$/.test(token) && token.length > 20;
|
|
setIsTokenValid(isValidFormat);
|
|
|
|
if (!isValidFormat) {
|
|
showToast({
|
|
type: 'error',
|
|
title: 'Token inválido',
|
|
message: 'El enlace de restablecimiento no es válido o ha expirado'
|
|
});
|
|
}
|
|
}
|
|
}, [token, showToast]);
|
|
|
|
// Calculate password strength
|
|
const calculatePasswordStrength = (password: string): number => {
|
|
let strength = 0;
|
|
if (password.length >= 8) strength += 25;
|
|
if (/[a-z]/.test(password)) strength += 25;
|
|
if (/[A-Z]/.test(password)) strength += 25;
|
|
if (/[0-9]/.test(password)) strength += 15;
|
|
if (/[^A-Za-z0-9]/.test(password)) strength += 10;
|
|
return Math.min(strength, 100);
|
|
};
|
|
|
|
const getPasswordStrengthLabel = (strength: number): string => {
|
|
if (strength < 25) return 'Muy débil';
|
|
if (strength < 50) return 'Débil';
|
|
if (strength < 75) return 'Media';
|
|
return 'Fuerte';
|
|
};
|
|
|
|
const getPasswordStrengthColor = (strength: number): string => {
|
|
if (strength < 25) return 'bg-color-error';
|
|
if (strength < 50) return 'bg-yellow-500';
|
|
if (strength < 75) return 'bg-blue-500';
|
|
return 'bg-color-success';
|
|
};
|
|
|
|
const validateEmail = (): boolean => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
if (!email.trim()) {
|
|
newErrors.email = 'El email es requerido';
|
|
} else if (!/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email)) {
|
|
newErrors.email = 'Por favor, ingrese un email válido';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const validatePasswords = (): boolean => {
|
|
const newErrors: Record<string, string> = {};
|
|
|
|
if (!password) {
|
|
newErrors.password = 'La contraseña es requerida';
|
|
} else {
|
|
const passwordErrors = getPasswordErrors(password);
|
|
if (passwordErrors.length > 0) {
|
|
newErrors.password = passwordErrors[0]; // Show first error
|
|
}
|
|
}
|
|
|
|
if (!confirmPassword) {
|
|
newErrors.confirmPassword = 'Confirma tu nueva contraseña';
|
|
} else if (password !== confirmPassword) {
|
|
newErrors.confirmPassword = 'Las contraseñas no coinciden';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
// Handle password change to update strength
|
|
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
setPassword(value);
|
|
setPasswordStrength(calculatePasswordStrength(value));
|
|
clearError('password');
|
|
};
|
|
|
|
const handleRequestReset = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateEmail()) {
|
|
// Focus on email field if validation fails
|
|
if (emailInputRef.current) {
|
|
emailInputRef.current.focus();
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// TODO: Implement password reset request
|
|
// const success = await requestPasswordReset(email);
|
|
const success = false; // Placeholder
|
|
if (success) {
|
|
setIsEmailSent(true);
|
|
showToast({
|
|
type: 'success',
|
|
title: 'Email enviado correctamente',
|
|
message: 'Te hemos enviado las instrucciones para restablecer tu contraseña'
|
|
});
|
|
} else {
|
|
showToast({
|
|
type: 'error',
|
|
title: 'Error al enviar email',
|
|
message: error || 'No se pudo enviar el email. Verifica que la dirección sea correcta.'
|
|
});
|
|
}
|
|
} catch (err) {
|
|
showToast({
|
|
type: 'error',
|
|
title: 'Error de conexión',
|
|
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleResetPassword = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validatePasswords()) {
|
|
// Focus on password field if validation fails
|
|
if (passwordInputRef.current) {
|
|
passwordInputRef.current.focus();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!token || isTokenValid === false) {
|
|
showToast({
|
|
type: 'error',
|
|
title: 'Token inválido',
|
|
message: 'El enlace de restablecimiento no es válido. Solicita uno nuevo.'
|
|
});
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// TODO: Implement password reset
|
|
// const success = await resetPassword(token, password);
|
|
const success = false; // Placeholder
|
|
if (success) {
|
|
showToast({
|
|
type: 'success',
|
|
title: 'Contraseña actualizada',
|
|
message: '¡Tu contraseña ha sido restablecida exitosamente! Ya puedes iniciar sesión.'
|
|
});
|
|
onSuccess?.();
|
|
} else {
|
|
showToast({
|
|
type: 'error',
|
|
title: 'Error al restablecer contraseña',
|
|
message: error || 'El enlace ha expirado o no es válido. Solicita un nuevo restablecimiento.'
|
|
});
|
|
}
|
|
} catch (err) {
|
|
showToast({
|
|
type: 'error',
|
|
title: 'Error de conexión',
|
|
message: 'No se pudo conectar con el servidor. Verifica tu conexión a internet.'
|
|
});
|
|
}
|
|
};
|
|
|
|
const clearError = (field: string) => {
|
|
if (errors[field]) {
|
|
setErrors(prev => ({ ...prev, [field]: undefined }));
|
|
}
|
|
};
|
|
|
|
// Handle Enter key submission
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && !isLoading) {
|
|
if (isResetMode) {
|
|
handleResetPassword(e as any);
|
|
} else {
|
|
handleRequestReset(e as any);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Success screen for email sent
|
|
if (isEmailSent && !isResetMode) {
|
|
return (
|
|
<Card className={`p-8 w-full max-w-md text-center ${className || ''}`} role="main">
|
|
<div className="mb-8">
|
|
<div className="w-20 h-20 bg-color-success/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<svg
|
|
className="w-10 h-10 text-color-success"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.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" />
|
|
</svg>
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-text-primary mb-4">
|
|
¡Email enviado correctamente!
|
|
</h1>
|
|
<div className="bg-background-secondary rounded-lg p-6 mb-6">
|
|
<p className="text-text-secondary mb-2">
|
|
Hemos enviado las instrucciones de restablecimiento a:
|
|
</p>
|
|
<p className="text-text-primary font-semibold text-lg mb-4">
|
|
{email}
|
|
</p>
|
|
<p className="text-text-secondary text-sm">
|
|
Revisa tu bandeja de entrada (y la carpeta de spam). El enlace es válido por 24 horas.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => {
|
|
setIsEmailSent(false);
|
|
setEmail('');
|
|
setErrors({});
|
|
if (emailInputRef.current) {
|
|
emailInputRef.current.focus();
|
|
}
|
|
}}
|
|
className="w-full"
|
|
>
|
|
Enviar a Otra Dirección
|
|
</Button>
|
|
|
|
{onBackClick && (
|
|
<Button
|
|
variant="outline"
|
|
onClick={onBackClick}
|
|
className="w-full"
|
|
>
|
|
Volver al Login
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Show token validation error
|
|
if (isResetMode && isTokenValid === false) {
|
|
return (
|
|
<Card className={`p-8 w-full max-w-md text-center ${className || ''}`} role="main">
|
|
<div className="mb-8">
|
|
<div className="w-20 h-20 bg-color-error/10 rounded-full flex items-center justify-center mx-auto mb-6">
|
|
<svg
|
|
className="w-10 h-10 text-color-error"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
|
</svg>
|
|
</div>
|
|
<h1 className="text-2xl font-bold text-text-primary mb-4">
|
|
Enlace No Válido
|
|
</h1>
|
|
<p className="text-text-secondary mb-6">
|
|
El enlace de restablecimiento no es válido o ha expirado.
|
|
Solicita un nuevo enlace para restablecer tu contraseña.
|
|
</p>
|
|
</div>
|
|
|
|
{onBackClick && (
|
|
<Button
|
|
variant="primary"
|
|
onClick={onBackClick}
|
|
className="w-full"
|
|
>
|
|
Volver al Login
|
|
</Button>
|
|
)}
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className={`p-8 w-full max-w-md ${className || ''}`} role="main">
|
|
<div className="text-center mb-8">
|
|
<h1 className="text-2xl font-bold text-text-primary mb-2">
|
|
{isResetMode ? 'Crear Nueva Contraseña' : 'Restablecer Contraseña'}
|
|
</h1>
|
|
<p className="text-text-secondary">
|
|
{isResetMode
|
|
? 'Crea una contraseña segura para acceder a tu cuenta'
|
|
: 'Te enviaremos un enlace seguro para restablecer tu contraseña'
|
|
}
|
|
</p>
|
|
</div>
|
|
|
|
<form
|
|
onSubmit={isResetMode ? handleResetPassword : handleRequestReset}
|
|
className="space-y-6"
|
|
noValidate
|
|
onKeyDown={handleKeyDown}
|
|
aria-label={isResetMode ? 'Formulario de nueva contraseña' : 'Formulario de solicitud de restablecimiento'}
|
|
>
|
|
{!isResetMode && (
|
|
<div>
|
|
<Input
|
|
ref={emailInputRef}
|
|
type="email"
|
|
label="Correo Electrónico"
|
|
placeholder="tu.email@panaderia.com"
|
|
value={email}
|
|
onChange={(e) => {
|
|
setEmail(e.target.value);
|
|
clearError('email');
|
|
}}
|
|
error={errors.email}
|
|
disabled={isLoading}
|
|
autoComplete="email"
|
|
required
|
|
aria-describedby={errors.email ? 'email-error' : undefined}
|
|
leftIcon={
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
|
</svg>
|
|
}
|
|
/>
|
|
{errors.email && (
|
|
<p id="email-error" className="text-color-error text-sm mt-1" role="alert">
|
|
{errors.email}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isResetMode && (
|
|
<>
|
|
<div>
|
|
<Input
|
|
ref={passwordInputRef}
|
|
type={showPassword ? 'text' : 'password'}
|
|
label="Nueva Contraseña"
|
|
placeholder="Contraseña segura"
|
|
value={password}
|
|
onChange={handlePasswordChange}
|
|
error={errors.password}
|
|
disabled={isLoading}
|
|
autoComplete="new-password"
|
|
required
|
|
aria-describedby={`${errors.password ? 'password-error' : ''} password-strength`}
|
|
leftIcon={
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
}
|
|
rightIcon={
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
|
aria-label={showPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
|
tabIndex={0}
|
|
>
|
|
{showPassword ? (
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
}
|
|
/>
|
|
|
|
{/* Password Strength Indicator */}
|
|
{password && (
|
|
<div className="mt-2">
|
|
<div className="flex justify-between text-xs mb-1">
|
|
<span className="text-text-secondary">Seguridad:</span>
|
|
<span className={`font-medium ${passwordStrength < 50 ? 'text-color-error' : passwordStrength < 75 ? 'text-yellow-600' : 'text-color-success'}`}>
|
|
{getPasswordStrengthLabel(passwordStrength)}
|
|
</span>
|
|
</div>
|
|
<div className="w-full bg-background-secondary rounded-full h-2">
|
|
<div
|
|
className={`h-2 rounded-full transition-all duration-300 ${getPasswordStrengthColor(passwordStrength)}`}
|
|
style={{ width: `${passwordStrength}%` }}
|
|
role="progressbar"
|
|
aria-valuenow={passwordStrength}
|
|
aria-valuemin={0}
|
|
aria-valuemax={100}
|
|
aria-label={`Fuerza de la contraseña: ${getPasswordStrengthLabel(passwordStrength)}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{errors.password && (
|
|
<p id="password-error" className="text-color-error text-sm mt-1" role="alert">
|
|
{errors.password}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<Input
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
label="Confirmar Nueva Contraseña"
|
|
placeholder="Repite tu nueva contraseña"
|
|
value={confirmPassword}
|
|
onChange={(e) => {
|
|
setConfirmPassword(e.target.value);
|
|
clearError('confirmPassword');
|
|
}}
|
|
error={errors.confirmPassword}
|
|
disabled={isLoading}
|
|
autoComplete="new-password"
|
|
required
|
|
aria-describedby={errors.confirmPassword ? 'confirm-password-error' : undefined}
|
|
leftIcon={
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
rightIcon={
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
|
aria-label={showConfirmPassword ? 'Ocultar contraseña' : 'Mostrar contraseña'}
|
|
tabIndex={0}
|
|
>
|
|
{showConfirmPassword ? (
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
aria-hidden="true"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
}
|
|
/>
|
|
{errors.confirmPassword && (
|
|
<p id="confirm-password-error" className="text-color-error text-sm mt-1" role="alert">
|
|
{errors.confirmPassword}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{error && (
|
|
<div
|
|
className="bg-color-error/10 border border-color-error/20 text-color-error px-4 py-3 rounded-lg text-sm flex items-start space-x-3"
|
|
role="alert"
|
|
aria-live="polite"
|
|
>
|
|
<svg
|
|
className="w-5 h-5 mt-0.5 flex-shrink-0"
|
|
fill="currentColor"
|
|
viewBox="0 0 20 20"
|
|
aria-hidden="true"
|
|
>
|
|
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
size="lg"
|
|
isFullWidth
|
|
isLoading={isLoading}
|
|
loadingText={isResetMode ? 'Restableciendo contraseña...' : 'Enviando enlace...'}
|
|
disabled={isLoading || (isResetMode && isTokenValid === false)}
|
|
aria-describedby={isResetMode ? 'reset-button-description' : 'request-button-description'}
|
|
>
|
|
{isResetMode ? 'Actualizar Contraseña' : 'Enviar Enlace de Restablecimiento'}
|
|
</Button>
|
|
|
|
<div id={isResetMode ? 'reset-button-description' : 'request-button-description'} className="sr-only">
|
|
{isResetMode
|
|
? 'Presiona Enter o haz clic para actualizar tu contraseña'
|
|
: 'Presiona Enter o haz clic para enviar el enlace de restablecimiento'
|
|
}
|
|
</div>
|
|
</form>
|
|
|
|
{onBackClick && (
|
|
<div className="mt-8 text-center border-t border-border-primary pt-6">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onBackClick}
|
|
disabled={isLoading}
|
|
className="w-full focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-offset-2"
|
|
>
|
|
Volver al Inicio de Sesión
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
);
|
|
};
|
|
|
|
export default PasswordResetForm; |