import { useState, useCallback } from 'react'; import { Eye, EyeOff, Check, X, AlertCircle, Scale } from 'lucide-react'; /** * Validación de entrada — reglas centralizadas. * Nota: esta es validación de CLIENTE (UX). El servidor debe replicar * estas mismas reglas (p. ej. con class-validator si el backend es NestJS) * porque la validación de cliente nunca sustituye la del servidor. */ const VALIDATORS = { fullName: (value) => { const trimmed = value.trim(); if (!trimmed) return 'El nombre completo es obligatorio'; if (trimmed.length < 3) return 'Debe tener al menos 3 caracteres'; if (trimmed.length > 80) return 'No puede exceder 80 caracteres'; if (!/^[a-zA-ZÀ-ÿñÑ\s'-]+$/.test(trimmed)) return 'Solo se permiten letras y espacios'; return ''; }, email: (value) => { const trimmed = value.trim(); if (!trimmed) return 'El correo electrónico es obligatorio'; if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(trimmed)) return 'Ingresa un correo electrónico válido'; return ''; }, phone: (value) => { const digits = value.replace(/\D/g, ''); if (!digits) return 'El teléfono es obligatorio'; if (digits.length !== 10) return 'Debe tener 10 dígitos'; return ''; }, password: (value) => { if (!value) return 'La contraseña es obligatoria'; if (value.length < 8) return 'Debe tener al menos 8 caracteres'; if (!/[a-z]/.test(value)) return 'Debe incluir al menos una minúscula'; if (!/[A-Z]/.test(value)) return 'Debe incluir al menos una mayúscula'; if (!/[0-9]/.test(value)) return 'Debe incluir al menos un número'; if (!/[^A-Za-z0-9]/.test(value)) return 'Debe incluir al menos un carácter especial'; return ''; }, confirmPassword: (value, password) => { if (!value) return 'Confirma tu contraseña'; if (value !== password) return 'Las contraseñas no coinciden'; return ''; }, }; function passwordStrength(value) { let score = 0; if (value.length >= 8) score++; if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++; if (/[0-9]/.test(value)) score++; if (/[^A-Za-z0-9]/.test(value)) score++; return score; } function formatPhone(digits) { const parts = [digits.slice(0, 2), digits.slice(2, 6), digits.slice(6, 10)].filter(Boolean); return parts.join(' '); } const STRENGTH_LABEL = ['Muy débil', 'Débil', 'Aceptable', 'Fuerte', 'Muy fuerte']; const STRENGTH_COLOR = ['bg-red-500', 'bg-orange-500', 'bg-amber-500', 'bg-lime-500', 'bg-emerald-500']; const FIELDS = ['fullName', 'email', 'phone', 'password', 'confirmPassword']; export default function RegistrationForm() { const [values, setValues] = useState({ fullName: '', email: '', phone: '', userType: 'abogado', password: '', confirmPassword: '', }); const [errors, setErrors] = useState({}); const [touched, setTouched] = useState({}); const [submitted, setSubmitted] = useState(false); const [success, setSuccess] = useState(false); const [showPassword, setShowPassword] = useState(false); const [showConfirm, setShowConfirm] = useState(false); const runValidation = useCallback((field, value, allValues) => { if (field === 'confirmPassword') return VALIDATORS.confirmPassword(value, allValues.password); return VALIDATORS[field] ? VALIDATORS[field](value) : ''; }, []); const setFieldValue = (field, rawValue) => { const value = field === 'phone' ? formatPhone(rawValue.replace(/\D/g, '').slice(0, 10)) : rawValue; const nextValues = { ...values, [field]: value }; setValues(nextValues); if (touched[field] || submitted) { setErrors((prev) => ({ ...prev, [field]: runValidation(field, value, nextValues) })); } if (field === 'password' && (touched.confirmPassword || submitted)) { setErrors((prev) => ({ ...prev, confirmPassword: runValidation('confirmPassword', nextValues.confirmPassword, nextValues), })); } }; const handleBlur = (field) => () => { setTouched((prev) => ({ ...prev, [field]: true })); setErrors((prev) => ({ ...prev, [field]: runValidation(field, values[field], values) })); }; const handleSubmit = (e) => { e.preventDefault(); const nextErrors = {}; FIELDS.forEach((f) => { nextErrors[f] = runValidation(f, values[f], values); }); setErrors(nextErrors); setTouched(FIELDS.reduce((acc, f) => ({ ...acc, [f]: true }), {})); setSubmitted(true); if (!Object.values(nextErrors).some(Boolean)) { setSuccess(true); } }; const handleReset = () => { setValues({ fullName: '', email: '', phone: '', userType: 'abogado', password: '', confirmPassword: '' }); setErrors({}); setTouched({}); setSubmitted(false); setSuccess(false); }; const strength = passwordStrength(values.password); // Válido solo si NINGÚN campo obligatorio produce error con los valores actuales. // Se recalcula en cada render; es la fuente de verdad para inhabilitar el envío. const isFormValid = FIELDS.every((f) => !runValidation(f, values[f], values)); if (success) { return (

Registro completado

La cuenta de {values.fullName.trim()} fue registrada correctamente.

); } return (
Sistema de Gestión Legal

Registro de usuario

{/* Nombre completo */} setFieldValue('fullName', e.target.value)} onBlur={handleBlur('fullName')} aria-required="true" aria-invalid={!!errors.fullName} aria-describedby={errors.fullName ? 'fullName-error' : undefined} className={inputClass(errors.fullName)} placeholder="Nombre y apellidos" /> {/* Correo */} setFieldValue('email', e.target.value)} onBlur={handleBlur('email')} aria-required="true" aria-invalid={!!errors.email} aria-describedby={errors.email ? 'email-error' : undefined} className={inputClass(errors.email)} placeholder="nombre@despacho.com" /> {/* Teléfono */} setFieldValue('phone', e.target.value)} onBlur={handleBlur('phone')} aria-required="true" aria-invalid={!!errors.phone} aria-describedby={errors.phone ? 'phone-error' : undefined} className={inputClass(errors.phone)} placeholder="81 1234 5678" /> {/* Tipo de usuario */} {/* Contraseña */}
setFieldValue('password', e.target.value)} onBlur={handleBlur('password')} aria-required="true" aria-invalid={!!errors.password} aria-describedby={errors.password ? 'password-error' : 'password-strength'} className={inputClass(errors.password) + ' pr-10'} placeholder="Mínimo 8 caracteres" />
{values.password && (
{[0, 1, 2, 3].map((i) => (
))}

{STRENGTH_LABEL[strength]}

)} {/* Confirmar contraseña */}
setFieldValue('confirmPassword', e.target.value)} onBlur={handleBlur('confirmPassword')} aria-required="true" aria-invalid={!!errors.confirmPassword} aria-describedby={errors.confirmPassword ? 'confirmPassword-error' : undefined} className={inputClass(errors.confirmPassword) + ' pr-10'} placeholder="Repite la contraseña" /> {values.confirmPassword && !errors.confirmPassword && touched.confirmPassword && ( )}
{submitted && Object.values(errors).some(Boolean) && (

Corrige los campos marcados antes de continuar.

)}
); } function inputClass(error) { return `w-full border rounded-sm px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 outline-none transition-colors focus:ring-2 ${ error ? 'border-red-400 focus:ring-red-200 focus:border-red-500' : 'border-slate-300 focus:ring-amber-200 focus:border-amber-600' }`; } function Field({ label, htmlFor, required, error, children }) { return (
{children} {error && ( )}
); }