421 lines
16 KiB
TypeScript
421 lines
16 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Eye, EyeOff, Loader2, Check } from 'lucide-react';
|
|
import toast from 'react-hot-toast';
|
|
|
|
import {
|
|
useAuth,
|
|
RegisterRequest
|
|
} from '../../api';
|
|
|
|
interface RegisterPageProps {
|
|
onLogin: (user: any, token: string) => void;
|
|
onNavigateToLogin: () => void;
|
|
}
|
|
|
|
interface RegisterForm {
|
|
fullName: string;
|
|
email: string;
|
|
password: string;
|
|
confirmPassword: string;
|
|
acceptTerms: boolean;
|
|
}
|
|
|
|
const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin }) => {
|
|
const { register, isLoading, error } = useAuth();
|
|
|
|
const [formData, setFormData] = useState<RegisterForm>({
|
|
fullName: '',
|
|
email: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
acceptTerms: false
|
|
});
|
|
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
const [errors, setErrors] = useState<Partial<RegisterForm>>({});
|
|
|
|
const validateForm = (): boolean => {
|
|
const newErrors: Partial<RegisterForm> = {};
|
|
|
|
if (!formData.fullName.trim()) {
|
|
newErrors.fullName = 'El nombre completo es obligatorio';
|
|
} else if (formData.fullName.trim().length < 2) {
|
|
newErrors.fullName = 'El nombre debe tener al menos 2 caracteres';
|
|
}
|
|
|
|
if (!formData.email) {
|
|
newErrors.email = 'El email es obligatorio';
|
|
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
|
newErrors.email = 'El email no es válido';
|
|
}
|
|
|
|
if (!formData.password) {
|
|
newErrors.password = 'La contraseña es obligatoria';
|
|
} else if (formData.password.length < 8) {
|
|
newErrors.password = 'La contraseña debe tener al menos 8 caracteres';
|
|
}
|
|
|
|
if (formData.password !== formData.confirmPassword) {
|
|
newErrors.confirmPassword = 'Las contraseñas no coinciden';
|
|
}
|
|
|
|
if (!formData.acceptTerms) {
|
|
newErrors.acceptTerms = 'Debes aceptar los términos y condiciones';
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!validateForm()) return;
|
|
|
|
try {
|
|
const registerData: RegisterRequest = {
|
|
email: formData.email,
|
|
password: formData.password,
|
|
full_name: formData.fullName,
|
|
role: 'user' // Default role
|
|
};
|
|
|
|
await register(registerData);
|
|
|
|
toast.success('¡Registro exitoso! Bienvenido a PanIA');
|
|
|
|
// The useAuth hook handles auto-login after registration
|
|
// Get the user data from localStorage since useAuth auto-logs in
|
|
const userData = localStorage.getItem('user_data');
|
|
const token = localStorage.getItem('auth_token');
|
|
|
|
if (userData && token) {
|
|
onLogin(JSON.parse(userData), token);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Registration error:', error);
|
|
toast.error(error instanceof Error ? error.message : 'Error en el registro');
|
|
}
|
|
};
|
|
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const { name, value, type, checked } = e.target;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[name]: type === 'checkbox' ? checked : value
|
|
}));
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[name as keyof RegisterForm]) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
[name]: undefined
|
|
}));
|
|
}
|
|
};
|
|
|
|
const getPasswordStrength = (password: string) => {
|
|
let strength = 0;
|
|
if (password.length >= 8) strength++;
|
|
if (/[a-z]/.test(password)) strength++;
|
|
if (/[A-Z]/.test(password)) strength++;
|
|
if (/\d/.test(password)) strength++;
|
|
if (/[^A-Za-z0-9]/.test(password)) strength++;
|
|
return strength;
|
|
};
|
|
|
|
const passwordStrength = getPasswordStrength(formData.password);
|
|
const strengthLabels = ['Muy débil', 'Débil', 'Regular', 'Buena', 'Excelente'];
|
|
const strengthColors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-blue-500', 'bg-green-500'];
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-orange-100 px-4 sm:px-6 lg:px-8">
|
|
<div className="max-w-md w-full space-y-8">
|
|
{/* Logo and Header */}
|
|
<div className="text-center">
|
|
<div className="mx-auto h-20 w-20 bg-primary-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
|
|
<span className="text-white text-2xl font-bold">🥖</span>
|
|
</div>
|
|
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
|
|
Únete a PanIA
|
|
</h1>
|
|
<p className="text-gray-600 text-lg">
|
|
Crea tu cuenta y transforma tu panadería
|
|
</p>
|
|
<p className="text-gray-500 text-sm mt-2">
|
|
Únete a más de 500 panaderías en Madrid
|
|
</p>
|
|
</div>
|
|
|
|
{/* Register Form */}
|
|
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
|
|
<form className="space-y-6" onSubmit={handleSubmit}>
|
|
{/* Full Name Field */}
|
|
<div>
|
|
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nombre completo
|
|
</label>
|
|
<input
|
|
id="fullName"
|
|
name="fullName"
|
|
type="text"
|
|
autoComplete="name"
|
|
required
|
|
value={formData.fullName}
|
|
onChange={handleInputChange}
|
|
className={`
|
|
appearance-none relative block w-full px-4 py-3 border rounded-xl
|
|
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
|
|
focus:ring-primary-500 focus:border-primary-500 focus:z-10
|
|
transition-all duration-200
|
|
${errors.fullName
|
|
? 'border-red-300 bg-red-50'
|
|
: 'border-gray-300 hover:border-gray-400'
|
|
}
|
|
`}
|
|
placeholder="Tu nombre completo"
|
|
/>
|
|
{errors.fullName && (
|
|
<p className="mt-1 text-sm text-red-600">{errors.fullName}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Email Field */}
|
|
<div>
|
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Correo electrónico
|
|
</label>
|
|
<input
|
|
id="email"
|
|
name="email"
|
|
type="email"
|
|
autoComplete="email"
|
|
required
|
|
value={formData.email}
|
|
onChange={handleInputChange}
|
|
className={`
|
|
appearance-none relative block w-full px-4 py-3 border rounded-xl
|
|
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
|
|
focus:ring-primary-500 focus:border-primary-500 focus:z-10
|
|
transition-all duration-200
|
|
${errors.email
|
|
? 'border-red-300 bg-red-50'
|
|
: 'border-gray-300 hover:border-gray-400'
|
|
}
|
|
`}
|
|
placeholder="tu@panaderia.com"
|
|
/>
|
|
{errors.email && (
|
|
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Password Field */}
|
|
<div>
|
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Contraseña
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
id="password"
|
|
name="password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
autoComplete="new-password"
|
|
required
|
|
value={formData.password}
|
|
onChange={handleInputChange}
|
|
className={`
|
|
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
|
|
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
|
|
focus:ring-primary-500 focus:border-primary-500 focus:z-10
|
|
transition-all duration-200
|
|
${errors.password
|
|
? 'border-red-300 bg-red-50'
|
|
: 'border-gray-300 hover:border-gray-400'
|
|
}
|
|
`}
|
|
placeholder="••••••••"
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
|
|
) : (
|
|
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Password Strength Indicator */}
|
|
{formData.password && (
|
|
<div className="mt-2">
|
|
<div className="flex space-x-1">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={`h-1 flex-1 rounded ${
|
|
i < passwordStrength ? strengthColors[passwordStrength - 1] : 'bg-gray-200'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<p className="text-xs text-gray-600 mt-1">
|
|
Seguridad: {strengthLabels[passwordStrength - 1] || 'Muy débil'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{errors.password && (
|
|
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Confirm Password Field */}
|
|
<div>
|
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-2">
|
|
Confirmar contraseña
|
|
</label>
|
|
<div className="relative">
|
|
<input
|
|
id="confirmPassword"
|
|
name="confirmPassword"
|
|
type={showConfirmPassword ? 'text' : 'password'}
|
|
autoComplete="new-password"
|
|
required
|
|
value={formData.confirmPassword}
|
|
onChange={handleInputChange}
|
|
className={`
|
|
appearance-none relative block w-full px-4 py-3 pr-12 border rounded-xl
|
|
placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-2
|
|
focus:ring-primary-500 focus:border-primary-500 focus:z-10
|
|
transition-all duration-200
|
|
${errors.confirmPassword
|
|
? 'border-red-300 bg-red-50'
|
|
: formData.confirmPassword && formData.password === formData.confirmPassword
|
|
? 'border-green-300 bg-green-50'
|
|
: 'border-gray-300 hover:border-gray-400'
|
|
}
|
|
`}
|
|
placeholder="••••••••"
|
|
/>
|
|
<button
|
|
type="button"
|
|
className="absolute inset-y-0 right-0 pr-3 flex items-center"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
>
|
|
{showConfirmPassword ? (
|
|
<EyeOff className="h-5 w-5 text-gray-400 hover:text-gray-600" />
|
|
) : (
|
|
<Eye className="h-5 w-5 text-gray-400 hover:text-gray-600" />
|
|
)}
|
|
</button>
|
|
{formData.confirmPassword && formData.password === formData.confirmPassword && (
|
|
<div className="absolute inset-y-0 right-10 flex items-center">
|
|
<Check className="h-5 w-5 text-green-500" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
{errors.confirmPassword && (
|
|
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Terms and Conditions */}
|
|
<div>
|
|
<div className="flex items-start">
|
|
<input
|
|
id="acceptTerms"
|
|
name="acceptTerms"
|
|
type="checkbox"
|
|
checked={formData.acceptTerms}
|
|
onChange={handleInputChange}
|
|
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded mt-0.5"
|
|
/>
|
|
<label htmlFor="acceptTerms" className="ml-2 block text-sm text-gray-700">
|
|
Acepto los{' '}
|
|
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
|
|
términos y condiciones
|
|
</a>{' '}
|
|
y la{' '}
|
|
<a href="#" className="font-medium text-primary-600 hover:text-primary-500">
|
|
política de privacidad
|
|
</a>
|
|
</label>
|
|
</div>
|
|
{errors.acceptTerms && (
|
|
<p className="mt-1 text-sm text-red-600">{errors.acceptTerms}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<div>
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading}
|
|
className={`
|
|
group relative w-full flex justify-center py-3 px-4 border border-transparent
|
|
text-sm font-medium rounded-xl text-white transition-all duration-200
|
|
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500
|
|
${isLoading
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-primary-500 hover:bg-primary-600 hover:shadow-lg transform hover:-translate-y-0.5'
|
|
}
|
|
`}
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
|
|
Creando cuenta...
|
|
</>
|
|
) : (
|
|
'Crear cuenta gratis'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Login Link */}
|
|
<div className="mt-6 text-center">
|
|
<p className="text-sm text-gray-600">
|
|
¿Ya tienes una cuenta?{' '}
|
|
<button
|
|
onClick={onNavigateToLogin}
|
|
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
|
|
>
|
|
Inicia sesión aquí
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Benefits */}
|
|
<div className="text-center">
|
|
<p className="text-xs text-gray-500 mb-4">
|
|
Al registrarte obtienes acceso completo durante 30 días gratis
|
|
</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 text-xs text-gray-400">
|
|
<div className="flex items-center justify-center">
|
|
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
|
Predicciones IA
|
|
</div>
|
|
<div className="flex items-center justify-center">
|
|
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
|
Soporte 24/7
|
|
</div>
|
|
<div className="flex items-center justify-center">
|
|
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
|
Sin compromiso
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default RegisterPage; |