Improve the register page

This commit is contained in:
Urtzi Alfaro
2025-08-11 08:15:13 +02:00
parent 652a850d0f
commit 7c237c0acc
6 changed files with 523 additions and 101 deletions

View File

@@ -1,12 +1,48 @@
import React, { useState } from 'react';
import { Eye, EyeOff, Loader2, Check } from 'lucide-react';
import React, { useState, useEffect } from 'react';
import { Eye, EyeOff, Loader2, Check, CreditCard, Shield, ArrowRight } from 'lucide-react';
import toast from 'react-hot-toast';
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';
import {
useAuth,
RegisterRequest
} from '../../api';
// Development flags
const isDevelopment = import.meta.env.DEV;
const bypassPayment = import.meta.env.VITE_BYPASS_PAYMENT === 'true';
// Initialize Stripe with Spanish market configuration (only if not bypassing)
const stripePromise = !bypassPayment ? loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || '', {
locale: 'es'
}) : null;
// Stripe card element options for Spanish market
const cardElementOptions = {
style: {
base: {
fontSize: '16px',
color: '#374151',
'::placeholder': {
color: '#9CA3AF',
},
},
invalid: {
color: '#EF4444',
},
},
hidePostalCode: false, // Keep postal code for better fraud protection
};
// Subscription pricing (monthly)
const SUBSCRIPTION_PRICE_EUR = 29.99;
interface RegisterPageProps {
onLogin: (user: any, token: string) => void;
onNavigateToLogin: () => void;
@@ -15,27 +51,201 @@ interface RegisterPageProps {
interface RegisterForm {
fullName: string;
email: string;
confirmEmail: string;
password: string;
confirmPassword: string;
acceptTerms: boolean;
paymentCompleted: boolean;
}
const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin }) => {
const { register, isLoading, error } = useAuth();
const { register, isLoading } = useAuth();
const [formData, setFormData] = useState<RegisterForm>({
fullName: '',
email: '',
confirmEmail: '',
password: '',
confirmPassword: '',
acceptTerms: false
acceptTerms: false,
paymentCompleted: false
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [errors, setErrors] = useState<Partial<RegisterForm>>({});
const [passwordStrength, setPasswordStrength] = useState<{
score: number;
checks: { [key: string]: boolean };
message: string;
}>({ score: 0, checks: {}, message: '' });
const [paymentStep, setPaymentStep] = useState<'form' | 'payment' | 'processing' | 'completed'>('form');
const [paymentLoading, setPaymentLoading] = useState(false);
const validateForm = (): boolean => {
// Update password strength in real-time
useEffect(() => {
if (formData.password) {
const validation = validatePassword(formData.password);
setPasswordStrength(validation);
}
}, [formData.password]);
// Payment processing component
const PaymentForm: React.FC<{ onPaymentSuccess: () => void }> = ({ onPaymentSuccess }) => {
const stripe = useStripe();
const elements = useElements();
const handlePayment = async (e: React.FormEvent) => {
e.preventDefault();
if (!stripe || !elements) {
toast.error('Stripe no está cargado correctamente');
return;
}
const card = elements.getElement(CardElement);
if (!card) {
toast.error('Elemento de tarjeta no encontrado');
return;
}
setPaymentLoading(true);
try {
// Create payment method
const { error } = await stripe.createPaymentMethod({
type: 'card',
card,
billing_details: {
name: formData.fullName,
email: formData.email,
},
});
if (error) {
throw new Error(error.message);
}
// Here you would typically create the subscription via your backend
// For now, we'll simulate a successful payment
toast.success('¡Pago procesado correctamente!');
setFormData(prev => ({ ...prev, paymentCompleted: true }));
setPaymentStep('completed');
onPaymentSuccess();
} catch (error) {
console.error('Payment error:', error);
toast.error(error instanceof Error ? error.message : 'Error procesando el pago');
} finally {
setPaymentLoading(false);
}
};
return (
<form onSubmit={handlePayment} className="space-y-6">
<div className="bg-primary-50 border border-primary-200 rounded-xl p-6">
<div className="flex items-center space-x-3 mb-4">
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center">
<CreditCard className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900">Suscripción PanIA Pro</h3>
<p className="text-sm text-gray-600">Facturación mensual</p>
</div>
<div className="ml-auto text-right">
<div className="text-2xl font-bold text-primary-600">{SUBSCRIPTION_PRICE_EUR}</div>
<div className="text-sm text-gray-500">/mes</div>
</div>
</div>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Predicciones de demanda ilimitadas
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Análisis de tendencias avanzado
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Soporte técnico prioritario
</div>
<div className="flex items-center">
<Check className="w-4 h-4 text-green-500 mr-2" />
Integración con sistemas de punto de venta
</div>
</div>
</div>
<div className="space-y-4">
<label className="block">
<span className="text-sm font-medium text-gray-700 mb-2 block">
Información de la tarjeta
</span>
<div className="border border-gray-300 rounded-xl p-4 focus-within:ring-2 focus-within:ring-primary-500 focus-within:border-primary-500">
<CardElement options={cardElementOptions} />
</div>
</label>
<div className="flex items-center space-x-2 text-sm text-gray-500">
<Shield className="w-4 h-4" />
<span>Pago seguro con encriptación SSL. Powered by Stripe.</span>
</div>
</div>
<button
type="submit"
disabled={!stripe || paymentLoading}
className="w-full bg-primary-500 text-white py-3 px-4 rounded-xl font-medium hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center"
>
{paymentLoading ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Procesando pago...
</>
) : (
<>
<CreditCard className="w-5 h-5 mr-2" />
Pagar {SUBSCRIPTION_PRICE_EUR}/mes
</>
)}
</button>
</form>
);
};
// Password validation based on backend rules
const validatePassword = (password: string) => {
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
numbers: /\d/.test(password),
// symbols: /[!@#$%^&*(),.?":{}|<>]/.test(password) // Backend doesn't require symbols
};
const score = Object.values(checks).filter(Boolean).length;
let message = '';
if (score < 4) {
if (!checks.length) message += 'Mínimo 8 caracteres. ';
if (!checks.uppercase) message += 'Una mayúscula. ';
if (!checks.lowercase) message += 'Una minúscula. ';
if (!checks.numbers) message += 'Un número. ';
} else {
message = '¡Contraseña segura!';
}
return { score, checks, message: message.trim() };
};
const handleFormSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate form but exclude payment requirement for first step
const newErrors: Partial<RegisterForm> = {};
if (!formData.fullName.trim()) {
@@ -50,10 +260,19 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
newErrors.email = 'El email no es válido';
}
if (!formData.confirmEmail) {
newErrors.confirmEmail = 'Confirma tu email';
} else if (formData.email !== formData.confirmEmail) {
newErrors.confirmEmail = 'Los emails no coinciden';
}
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 {
const passwordValidation = validatePassword(formData.password);
if (passwordValidation.score < 4) {
newErrors.password = passwordValidation.message;
}
}
if (formData.password !== formData.confirmPassword) {
@@ -65,13 +284,29 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
if (Object.keys(newErrors).length > 0) return;
// Move to payment step, or bypass if in development mode
if (bypassPayment) {
// Development bypass: simulate payment completion
setFormData(prev => ({ ...prev, paymentCompleted: true }));
setPaymentStep('completed');
toast.success('🚀 Modo desarrollo: Pago omitido');
// Proceed directly to registration
setTimeout(() => {
handleRegistrationComplete();
}, 1500);
} else {
setPaymentStep('payment');
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validateForm()) return;
const handleRegistrationComplete = async () => {
if (!bypassPayment && !formData.paymentCompleted) {
toast.error('El pago debe completarse antes del registro');
return;
}
try {
const registerData: RegisterRequest = {
@@ -97,6 +332,9 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
} catch (error) {
console.error('Registration error:', error);
toast.error(error instanceof Error ? error.message : 'Error en el registro');
// Reset payment if registration fails
setFormData(prev => ({ ...prev, paymentCompleted: false }));
setPaymentStep('payment');
}
};
@@ -116,19 +354,68 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
}
};
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'];
// Render different content based on payment step
if (paymentStep === 'payment' && !bypassPayment) {
return (
<Elements stripe={stripePromise}>
<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">
Finalizar Registro
</h1>
<p className="text-gray-600 text-lg">
Solo un paso más para comenzar
</p>
<p className="text-gray-500 text-sm mt-2">
Suscripción segura con Stripe
</p>
</div>
{/* Payment Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<PaymentForm onPaymentSuccess={handleRegistrationComplete} />
<button
onClick={() => setPaymentStep('form')}
className="w-full mt-4 text-sm text-gray-500 hover:text-gray-700 transition-colors"
>
Volver al formulario
</button>
</div>
</div>
</div>
</Elements>
);
}
if (paymentStep === 'completed') {
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">
<div className="text-center">
<div className="mx-auto h-20 w-20 bg-green-500 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
<Check className="text-white text-2xl" />
</div>
<h1 className="text-4xl font-bold font-display text-gray-900 mb-2">
¡Bienvenido a PanIA!
</h1>
<p className="text-gray-600 text-lg">
Tu cuenta ha sido creada exitosamente
</p>
<p className="text-gray-500 text-sm mt-2">
Redirigiendo al panel de control...
</p>
</div>
</div>
</div>
);
}
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">
@@ -151,7 +438,7 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
{/* Register Form */}
<div className="bg-white py-8 px-6 shadow-strong rounded-3xl">
<form className="space-y-6" onSubmit={handleSubmit}>
<form className="space-y-6" onSubmit={handleFormSubmit}>
{/* Full Name Field */}
<div>
<label htmlFor="fullName" className="block text-sm font-medium text-gray-700 mb-2">
@@ -212,6 +499,43 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
)}
</div>
{/* Confirm Email Field */}
<div>
<label htmlFor="confirmEmail" className="block text-sm font-medium text-gray-700 mb-2">
Confirmar correo electrónico
</label>
<input
id="confirmEmail"
name="confirmEmail"
type="email"
autoComplete="email"
required
value={formData.confirmEmail}
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.confirmEmail
? 'border-red-300 bg-red-50'
: formData.confirmEmail && formData.email === formData.confirmEmail
? 'border-green-300 bg-green-50'
: 'border-gray-300 hover:border-gray-400'
}
`}
placeholder="tu@panaderia.com"
/>
{formData.confirmEmail && formData.email === formData.confirmEmail && (
<div className="absolute inset-y-0 right-3 flex items-center mt-8">
<Check className="h-5 w-5 text-green-500" />
</div>
)}
{errors.confirmEmail && (
<p className="mt-1 text-sm text-red-600">{errors.confirmEmail}</p>
)}
</div>
{/* Password Field */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-2">
@@ -251,21 +575,30 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
</button>
</div>
{/* Password Strength Indicator */}
{/* Enhanced 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 className="grid grid-cols-4 gap-1">
{Object.entries(passwordStrength.checks).map(([key, passed], index) => {
const labels = {
length: '8+ caracteres',
uppercase: 'Mayúscula',
lowercase: 'Minúscula',
numbers: 'Número'
};
return (
<div key={key} className={`text-xs p-1 rounded text-center ${
passed ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'
}`}>
{passed ? '✓' : '○'} {labels[key as keyof typeof labels]}
</div>
);
})}
</div>
<p className="text-xs text-gray-600 mt-1">
Seguridad: {strengthLabels[passwordStrength - 1] || 'Muy débil'}
<p className={`text-xs mt-1 ${
passwordStrength.score === 4 ? 'text-green-600' : 'text-gray-600'
}`}>
{passwordStrength.message}
</p>
</div>
)}
@@ -370,10 +703,13 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
{isLoading ? (
<>
<Loader2 className="h-5 w-5 mr-2 animate-spin" />
Creando cuenta...
Validando...
</>
) : (
'Crear cuenta gratis'
<>
{bypassPayment ? 'Crear Cuenta (Dev)' : 'Continuar al Pago'}
<ArrowRight className="h-5 w-5 ml-2" />
</>
)}
</button>
</div>
@@ -395,8 +731,18 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
{/* Benefits */}
<div className="text-center">
{bypassPayment && (
<div className="mb-4 p-2 bg-yellow-100 border border-yellow-300 rounded-lg">
<p className="text-xs text-yellow-800">
🚀 Modo Desarrollo: Pago omitido para pruebas
</p>
</div>
)}
<p className="text-xs text-gray-500 mb-4">
Al registrarte obtienes acceso completo durante 30 días gratis
{bypassPayment
? 'Desarrollo • Pruebas • Sin pago requerido'
: 'Proceso seguro • Cancela en cualquier momento • Soporte 24/7'
}
</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">
@@ -405,11 +751,11 @@ const RegisterPage: React.FC<RegisterPageProps> = ({ onLogin, onNavigateToLogin
</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
Análisis de demanda
</div>
<div className="flex items-center justify-center">
<span className="w-2 h-2 bg-success-500 rounded-full mr-2"></span>
Sin compromiso
Reduce desperdicios
</div>
</div>
</div>