Add new Frontend
This commit is contained in:
278
frontend/src/pages/auth/LoginPage.tsx
Normal file
278
frontend/src/pages/auth/LoginPage.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Eye, EyeOff, Loader2 } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface LoginPageProps {
|
||||
onLogin: (user: any, token: string) => void;
|
||||
onNavigateToRegister: () => void;
|
||||
}
|
||||
|
||||
interface LoginForm {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const LoginPage: React.FC<LoginPageProps> = ({ onLogin, onNavigateToRegister }) => {
|
||||
const [formData, setFormData] = useState<LoginForm>({
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Partial<LoginForm>>({});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Partial<LoginForm> = {};
|
||||
|
||||
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 < 6) {
|
||||
newErrors.password = 'La contraseña debe tener al menos 6 caracteres';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Error al iniciar sesión');
|
||||
}
|
||||
|
||||
toast.success('¡Bienvenido a PanIA!');
|
||||
onLogin(data.user, data.access_token);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Login error:', error);
|
||||
toast.error(error.message || 'Error al iniciar sesión');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}));
|
||||
|
||||
// Clear error when user starts typing
|
||||
if (errors[name as keyof LoginForm]) {
|
||||
setErrors(prev => ({
|
||||
...prev,
|
||||
[name]: undefined
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
PanIA
|
||||
</h1>
|
||||
<p className="text-gray-600 text-lg">
|
||||
Inteligencia Artificial para tu Panadería
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm mt-2">
|
||||
Inicia sesión para acceder a tus predicciones
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
|
||||
<form className="space-y-6" onSubmit={handleSubmit}>
|
||||
{/* 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="current-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>
|
||||
{errors.password && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Remember Me & Forgot Password */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="remember-me"
|
||||
name="remember-me"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-700">
|
||||
Recordarme
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="text-sm">
|
||||
<a
|
||||
href="#"
|
||||
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
|
||||
>
|
||||
¿Olvidaste tu contraseña?
|
||||
</a>
|
||||
</div>
|
||||
</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" />
|
||||
Iniciando sesión...
|
||||
</>
|
||||
) : (
|
||||
'Iniciar sesión'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Register Link */}
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-sm text-gray-600">
|
||||
¿No tienes una cuenta?{' '}
|
||||
<button
|
||||
onClick={onNavigateToRegister}
|
||||
className="font-medium text-primary-600 hover:text-primary-500 transition-colors"
|
||||
>
|
||||
Regístrate gratis
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Features Preview */}
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
Más de 500 panaderías en Madrid confían en PanIA
|
||||
</p>
|
||||
<div className="flex justify-center space-x-6 text-xs text-gray-400">
|
||||
<div className="flex items-center">
|
||||
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
||||
Predicciones precisas
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
||||
Reduce desperdicios
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
|
||||
Fácil de usar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
686
frontend/src/pages/auth/RegisterPage.tsx
Normal file
686
frontend/src/pages/auth/RegisterPage.tsx
Normal file
@@ -0,0 +1,686 @@
|
||||
{/* Register Form */}
|
||||
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
|
||||
<div className="space-y-6">
|
||||
{/* 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="button"
|
||||
onClick={handleSubmit}
|
||||
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>
|
||||
</div>
|
||||
|
||||
{/* 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>import React, { useState } from 'react';
|
||||
import { Eye, EyeOff, Loader2, Check } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
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 [formData, setFormData] = useState<RegisterForm>({
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
acceptTerms: false
|
||||
});
|
||||
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = 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';
|
||||
} else if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/.test(formData.password)) {
|
||||
newErrors.password = 'La contraseña debe incluir mayúsculas, minúsculas y números';
|
||||
}
|
||||
|
||||
if (!formData.confirmPassword) {
|
||||
newErrors.confirmPassword = 'Confirma tu contraseña';
|
||||
} else 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;
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
full_name: formData.fullName,
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
role: 'admin' // Default role for bakery owners
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Error al crear la cuenta');
|
||||
}
|
||||
|
||||
// Auto-login after successful registration
|
||||
const loginResponse = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formData.email,
|
||||
password: formData.password,
|
||||
}),
|
||||
});
|
||||
|
||||
const loginData = await loginResponse.json();
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
throw new Error('Cuenta creada, pero error al iniciar sesión');
|
||||
}
|
||||
|
||||
toast.success('¡Cuenta creada exitosamente! Bienvenido a PanIA');
|
||||
onLogin(loginData.user, loginData.access_token);
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Registration error:', error);
|
||||
toast.error(error.message || 'Error al crear la cuenta');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user