From c1f865dfa5dc518fccff5816aed7d1c219853172 Mon Sep 17 00:00:00 2001 From: adevopg Date: Tue, 14 Jul 2026 20:50:30 +0000 Subject: [PATCH] =?UTF-8?q?create-account:=20validaci=C3=B3n=20en=20client?= =?UTF-8?q?e=20con=20red-response=20inmediato?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antes los errores (contraseñas/correos que no coinciden, campos vacíos) solo se mostraban en rojo tras el envío al servidor. Ahora se validan en el cliente y se muestran al instante como red-form-response, sin llamada al servidor. Nueva clave emailMismatch. El servidor sigue haciendo la validación final (Gmail, existe, etc.). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../[locale]/create-account/RegisterForm.tsx | 113 ++++++++++++++++-- web-next/messages/en.json | 15 ++- web-next/messages/es.json | 15 ++- 3 files changed, 124 insertions(+), 19 deletions(-) diff --git a/web-next/app/[locale]/create-account/RegisterForm.tsx b/web-next/app/[locale]/create-account/RegisterForm.tsx index 3ab12f4..5f565df 100644 --- a/web-next/app/[locale]/create-account/RegisterForm.tsx +++ b/web-next/app/[locale]/create-account/RegisterForm.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useTranslations } from 'next-intl' +import { Link } from '@/i18n/navigation' import { Turnstile } from '@/components/Turnstile' const ERROR_KEYS = [ @@ -14,21 +15,78 @@ const ERROR_KEYS = [ 'captchaFailed', ] as const +/** Campo de contraseña con ojo para mostrar/ocultar. */ +function PasswordInput({ + value, + onChange, + placeholder, + ariaLabel, + blockPaste, +}: { + value: string + onChange: (v: string) => void + placeholder: string + ariaLabel: string + blockPaste?: boolean +}) { + const [show, setShow] = useState(false) + const noPaste = blockPaste ? (e: React.ClipboardEvent) => e.preventDefault() : undefined + return ( + <> + onChange(e.target.value)} + onPaste={noPaste} + onCopy={noPaste} + onCut={noPaste} + />{' '} + setShow((s) => !s)} + /> + + ) +} + export function RegisterForm() { const t = useTranslations('Register') const [form, setForm] = useState({ password: '', confPassword: '', email: '', confEmail: '', recruiter: '' }) const [accepted, setAccepted] = useState(false) + const [notUs, setNotUs] = useState(false) const [busy, setBusy] = useState(false) const [captcha, setCaptcha] = useState('') const [message, setMessage] = useState<{ ok: boolean; text: string } | null>(null) + const canSubmit = accepted && notUs && !busy + function upd(k: string, v: string) { setForm((f) => ({ ...f, [k]: v })) } + /** Validación en cliente: muestra el error en rojo al instante, sin ir al servidor. */ + function clientError(): string | null { + if (!form.password || !form.confPassword || !form.email || !form.confEmail) return 'missingFields' + if (form.password !== form.confPassword) return 'passwordMismatch' + if (form.password.length > 16) return 'passwordTooLong' + if (form.email.trim() !== form.confEmail.trim()) return 'emailMismatch' + return null + } + async function handleSubmit(e: React.FormEvent) { e.preventDefault() - if (busy || !accepted) return + if (!canSubmit) return + const err = clientError() + if (err) { + setMessage({ ok: false, text: t(err) }) + return + } setBusy(true) setMessage(null) try { @@ -52,6 +110,8 @@ export function RegisterForm() { } } + const noPaste = (e: React.ClipboardEvent) => e.preventDefault() + return ( <>
@@ -59,12 +119,12 @@ export function RegisterForm() { - upd('password', e.target.value)} /> + upd('password', v)} placeholder={t('password')} ariaLabel={t('showPassword')} /> - upd('confPassword', e.target.value)} /> + upd('confPassword', v)} placeholder={t('confPassword')} ariaLabel={t('showPassword')} blockPaste /> @@ -74,19 +134,30 @@ export function RegisterForm() { - upd('confEmail', e.target.value)} /> + upd('confEmail', e.target.value)} + onPaste={noPaste} + onCopy={noPaste} + onCut={noPaste} + /> - upd('recruiter', e.target.value)} /> +
- - + {t('recruiterOptional')} + + + + upd('recruiter', e.target.value)} /> @@ -94,9 +165,27 @@ export function RegisterForm() { + + + + + + + + + + - @@ -106,9 +195,7 @@ export function RegisterForm() {
- {message && ( - {message.text} - )} + {message && {message.text}}
) diff --git a/web-next/messages/en.json b/web-next/messages/en.json index 5cec9b6..060fa64 100644 --- a/web-next/messages/en.json +++ b/web-next/messages/en.json @@ -170,13 +170,11 @@ }, "Register": { "title": "Create account", - "info": "Your account is Battle.net type: it is identified by your email. The password must be alphanumeric and up to 16 characters. The email must be a Gmail address.", "password": "Password", "confPassword": "Confirm password", "email": "Gmail email address", "confEmail": "Confirm email address", "recruiter": "Recruiter (optional)", - "terms": "I accept the Rules, Terms and Conditions and Privacy Policy", "submit": "Create account", "creating": "Creating…", "success": "The account has been created. Check your email to activate it.", @@ -187,7 +185,18 @@ "emailExists": "An account with that email already exists.", "recruiterNotFound": "The recruiter entered does not exist.", "genericError": "Something went wrong. Please try again later.", - "captchaFailed": "Anti-bot verification failed. Please retry." + "captchaFailed": "Anti-bot verification failed. Please retry.", + "bnetType": "Your account is a Battle.net account: it is identified by your email address.", + "passwordRule": "The password must be alphanumeric and up to 16 characters long.", + "emailRule": "The email must be a Gmail address you have access to.", + "activation1": "Once the account has been created successfully, an email with an activation link will be sent.", + "activation2": "If you do not use the activation link, the account cannot be used to connect to the server.", + "loginHint": "When logging in, use your EMAIL address.", + "recruiterOptional": "Optional: only if a friend recruited you.", + "notUs": "I confirm that I am not accessing this service from the United States.", + "termsRich": "I accept the Terms and Conditions and the Privacy Policy.", + "showPassword": "Show/hide password", + "emailMismatch": "The email addresses do not match." }, "Activate": { "activating": "Activating your account…", diff --git a/web-next/messages/es.json b/web-next/messages/es.json index 5ca4b49..64b30c2 100644 --- a/web-next/messages/es.json +++ b/web-next/messages/es.json @@ -170,13 +170,11 @@ }, "Register": { "title": "Creación de cuenta", - "info": "Tu cuenta es de tipo Battle.net: se identifica con tu correo electrónico. La contraseña debe ser alfanumérica y de hasta 16 caracteres. El correo debe ser de Gmail.", "password": "Contraseña", "confPassword": "Confirmar contraseña", "email": "Correo electrónico de Gmail", "confEmail": "Confirmar correo electrónico", "recruiter": "Reclutante (opcional)", - "terms": "Acepto las Normas, los Términos y Condiciones y la Política de Privacidad", "submit": "Crear cuenta", "creating": "Creando…", "success": "La cuenta se ha creado. Revisa tu correo para activarla.", @@ -187,7 +185,18 @@ "emailExists": "Ya existe una cuenta con ese correo electrónico.", "recruiterNotFound": "El reclutador ingresado no existe.", "genericError": "Algo ha salido mal. Inténtalo más tarde.", - "captchaFailed": "Verificación anti-bots fallida. Reintenta." + "captchaFailed": "Verificación anti-bots fallida. Reintenta.", + "bnetType": "Tu cuenta es de tipo Battle.net: se identifica con tu correo electrónico.", + "passwordRule": "La contraseña debe ser alfanumérica y con una longitud de hasta 16 caracteres.", + "emailRule": "El correo debe pertenecer a Gmail y con acceso al mismo.", + "activation1": "Una vez que la cuenta se haya creado exitosamente, se enviará un correo con un enlace de activación.", + "activation2": "De no usar el enlace de activación, la cuenta no podrá usarse para conectar al servidor.", + "loginHint": "Al iniciar sesión, ingresa con tu CORREO electrónico.", + "recruiterOptional": "Opcional: solo si te ha reclutado un amigo.", + "notUs": "Confirmo que no estoy accediendo a este servicio desde los Estados Unidos.", + "termsRich": "Acepto los Términos y Condiciones y la Política de Privacidad.", + "showPassword": "Mostrar/ocultar contraseña", + "emailMismatch": "Los correos electrónicos no coinciden." }, "Activate": { "activating": "Activando tu cuenta…",