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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
}
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user