Fase 2 (isla): formulario de registro en React/TSX
register_view ya devolvía JSON en POST, así que sin cambio de backend:
RegisterForm.tsx replica register.js (password/conf con mostrar-ocultar, email/conf,
reclutador opcional, checkbox de términos que habilita el botón) y hace fetch
form-urlencoded + CSRF a {% url 'register' %}. El mensaje de éxito llega como HTML
(dangerouslySetInnerHTML); los de error como texto en rojo.
- partials/register.html: el <form> + register.js se sustituyen por #register-app
(data-csrf/data-register-url) + {% vite_asset %}. Info-box y rama "ya conectado"
siguen en Django.
- Nueva entry 'register' en vite.config.ts.
Verificado: check OK, la página monta la isla, POST con datos inválidos devuelve
el JSON de validación correcto.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
registerUrl: string
|
||||
csrfToken: string
|
||||
}
|
||||
|
||||
type RegisterResponse = {
|
||||
success?: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function RegisterForm({ registerUrl, csrfToken }: Props) {
|
||||
const [password, setPassword] = useState('')
|
||||
const [confPassword, setConfPassword] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [confEmail, setConfEmail] = useState('')
|
||||
const [recruiter, setRecruiter] = useState('')
|
||||
const [accepted, setAccepted] = useState(false)
|
||||
const [showPw, setShowPw] = useState(false)
|
||||
const [showConfPw, setShowConfPw] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [response, setResponse] = useState<{ ok: boolean; html: string } | null>(null)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (busy || !accepted) return
|
||||
|
||||
setBusy(true)
|
||||
setResponse(null)
|
||||
|
||||
const body = new URLSearchParams()
|
||||
body.set('password', password.trim())
|
||||
body.set('conf-password', confPassword.trim())
|
||||
body.set('email', email.trim())
|
||||
body.set('conf-email', confEmail.trim())
|
||||
body.set('recruiter', recruiter.trim())
|
||||
body.set('csrfmiddlewaretoken', csrfToken)
|
||||
|
||||
try {
|
||||
const resp = await fetch(registerUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-CSRFToken': csrfToken,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: body.toString(),
|
||||
})
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
||||
const data: RegisterResponse = await resp.json()
|
||||
const msg = data.message ?? ''
|
||||
// El mensaje de éxito ya viene como HTML; el de error es texto.
|
||||
setResponse({
|
||||
ok: !!data.success,
|
||||
html: data.success ? msg : `<span class="red-form-response">${msg}</span>`,
|
||||
})
|
||||
} catch {
|
||||
setResponse({
|
||||
ok: false,
|
||||
html: '<span class="red-form-response">Error en el servidor. Inténtalo de nuevo más tarde.</span>',
|
||||
})
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form id="nw-create-form" onSubmit={handleSubmit} acceptCharset="utf-8">
|
||||
<table className="middle-center-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type={showPw ? 'text' : 'password'}
|
||||
maxLength={16}
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Contraseña"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<span
|
||||
className={`far toggle-password ${showPw ? 'fa-eye-slash' : 'fa-eye'}`}
|
||||
onClick={() => setShowPw((v) => !v)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type={showConfPw ? 'text' : 'password'}
|
||||
maxLength={16}
|
||||
name="conf-password"
|
||||
id="conf-password"
|
||||
placeholder="Confirmar contraseña"
|
||||
value={confPassword}
|
||||
onChange={(e) => setConfPassword(e.target.value)}
|
||||
/>
|
||||
<span
|
||||
className={`far toggle-password-conf ${showConfPw ? 'fa-eye-slash' : 'fa-eye'}`}
|
||||
onClick={() => setShowConfPw((v) => !v)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="Correo electrónico de Gmail"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="email"
|
||||
name="conf-email"
|
||||
id="conf-email"
|
||||
placeholder="Confirmar correo electrónico"
|
||||
value={confEmail}
|
||||
onChange={(e) => setConfEmail(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<hr />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Opcional: Sólo si eres reclutado por un amigo</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
maxLength={12}
|
||||
name="recruiter"
|
||||
id="recruiter"
|
||||
placeholder="Reclutante"
|
||||
value={recruiter}
|
||||
onChange={(e) => setRecruiter(e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="accept-terms-cookies"
|
||||
className="terms-check"
|
||||
name="accept-terms-cookies"
|
||||
checked={accepted}
|
||||
onChange={(e) => setAccepted(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="accept-terms-cookies">
|
||||
{' '}
|
||||
Acepto las{' '}
|
||||
<a
|
||||
href="https://foro.novawow.com/forum/novawow/normas-de-la-comunidad/16502-normas-del-juego"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Normas
|
||||
</a>
|
||||
,{' '}
|
||||
<a href="terms-and-conditions" target="_blank">
|
||||
Términos y Condiciones
|
||||
</a>{' '}
|
||||
y la{' '}
|
||||
<a href="privacy-policy" target="_blank">
|
||||
Política de Privacidad
|
||||
</a>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
type="submit"
|
||||
name="create"
|
||||
className="create-button"
|
||||
id="create-account-btn"
|
||||
disabled={!accepted || busy}
|
||||
>
|
||||
{busy ? 'Creando…' : 'Crear cuenta'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<hr />
|
||||
<div className="alert-message" id="create-response" style={{ display: response ? 'block' : 'none' }}>
|
||||
{response && <span dangerouslySetInnerHTML={{ __html: response.html }} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user