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:
2026-07-12 20:45:00 +00:00
parent 0909567e0d
commit 8156befe4f
4 changed files with 229 additions and 42 deletions
+206
View File
@@ -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>
</>
)
}
+15
View File
@@ -0,0 +1,15 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RegisterForm } from '../components/RegisterForm'
const el = document.getElementById('register-app')
if (el) {
createRoot(el).render(
<StrictMode>
<RegisterForm
registerUrl={el.dataset.registerUrl || ''}
csrfToken={el.dataset.csrf || ''}
/>
</StrictMode>,
)
}
+1
View File
@@ -16,6 +16,7 @@ export default defineConfig({
input: {
home: resolve(__dirname, 'src/entries/home.tsx'),
login: resolve(__dirname, 'src/entries/login.tsx'),
register: resolve(__dirname, 'src/entries/register.tsx'),
},
},
},
+7 -42
View File
@@ -54,48 +54,13 @@
<div class="box-content">
<div class="body-box-content centered">
<form action="" method="POST" id="nw-create-form" accept-charset="utf-8">
{% csrf_token %}
<table class="middle-center-table">
<script src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-js-handlers/register.js"></script>
<tbody><tr>
<td><input type="password" maxlength="16" name="password" id="password" placeholder="Contraseña">
<span class="far fa-eye toggle-password"></span></td>
</tr>
<tr>
<td><input type="password" maxlength="16" name="conf-password" id="conf-password" placeholder="Confirmar contraseña">
<span class="far fa-eye toggle-password-conf"></span></td>
</tr>
<tr>
<td><input type="email" name="email" id="email" placeholder="Correo electrónico de Gmail"></td>
</tr>
<tr>
<td><input type="email" name="conf-email" id="conf-email" placeholder="Confirmar correo electrónico"></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"></td>
</tr>
<tr>
</tr>
<tr>
<td>
<input type="checkbox" onclick="click_checkbox()" id="accept-terms-cookies" class="terms-check" name="accept-terms-cookies">
<label for="vehicle1"> 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" class="create-button" data-id="CrearCuenta" id="create-account-btn" disabled="true">Crear cuenta</button></td>
</tr>
</tbody></table>
</form>
<hr>
<div class="alert-message" id="create-response"></div>
{% load django_vite %}
<!-- Isla React (Vite): formulario de creación de cuenta. POST a
{% url 'register' %} (que ya devuelve JSON) con CSRF. -->
<div id="register-app"
data-csrf="{{ csrf_token }}"
data-register-url="{% url 'register' %}"></div>
{% vite_asset 'src/entries/register.tsx' %}
<br>
</div>
</div>