From 0909567e0d49fd9ced5a0d2c0014435ee5dc29ff Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 20:41:46 +0000 Subject: [PATCH] Fase 2 (isla): formulario de login en React/TSX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El login se migra a una isla React. login_view ya devolvía JSON en POST, así que el backend no cambia: LoginForm.tsx hace fetch (form-urlencoded + CSRF) al mismo {% url 'login' %} y replica el comportamiento del antiguo login_response.js (mostrar/ocultar contraseña, estados del botón, éxito -> redirige a my-account, alert/locked, errores transitorios). - partials/login.html: el
+ el script jQuery se sustituyen por #login-app (con data-csrf/data-login-url/data-success-url) + {% vite_asset %}. La rama "ya conectado" y los enlaces (recover, create-account) siguen en Django. - CSRF vía data-csrf="{{ csrf_token }}" (fija la cookie y da el token); el fetch lo manda como X-CSRFToken y csrfmiddlewaretoken. El navegador adjunta Referer. - Nueva entry 'login' en vite.config.ts. Verificado: check OK, la página monta la isla, POST devuelve JSON correcto. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/components/LoginForm.tsx | 149 ++++++++++++++++++++++++++ frontend/src/entries/login.tsx | 16 +++ frontend/vite.config.ts | 1 + home/templates/partials/login.html | 33 ++---- 4 files changed, 174 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/LoginForm.tsx create mode 100644 frontend/src/entries/login.tsx diff --git a/frontend/src/components/LoginForm.tsx b/frontend/src/components/LoginForm.tsx new file mode 100644 index 0000000..cf4e5de --- /dev/null +++ b/frontend/src/components/LoginForm.tsx @@ -0,0 +1,149 @@ +import { useState } from 'react' + +interface Props { + loginUrl: string + successUrl: string + csrfToken: string +} + +type LoginResponse = { + success?: boolean + alert?: boolean + locked?: boolean + error?: string +} + +export function LoginForm({ loginUrl, successUrl, csrfToken }: Props) { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const [buttonText, setButtonText] = useState('Conectar') + const [busy, setBusy] = useState(false) + const [message, setMessage] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null) + + function transientError(text: string) { + setMessage({ kind: 'err', text }) + setBusy(false) + setButtonText('Conectar') + setTimeout(() => setMessage(null), 5000) + } + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault() + if (busy) return + + if (email.trim() === '' || password.trim() === '') { + transientError('Por favor rellene todos los campos.') + return + } + + setBusy(true) + setButtonText('Conectando') + setMessage(null) + + const body = new URLSearchParams() + body.set('email', email.trim()) + body.set('password', password.trim()) + body.set('csrfmiddlewaretoken', csrfToken) + + try { + const resp = await fetch(loginUrl, { + 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: LoginResponse = await resp.json() + + if (data.success === true) { + setButtonText('Conectado') + setMessage({ kind: 'ok', text: 'Conexión exitosa. Redirigiendo…' }) + setTimeout(() => { + window.location.href = successUrl + }, 3000) + } else if (data.alert === true) { + setButtonText('Sancionado') + setBusy(false) + } else if (data.locked === true) { + setButtonText('Seguridad') + setBusy(false) + } else { + transientError(data.error || 'Nombre de usuario / Contraseña incorrectos.') + } + } catch { + setTimeout(() => { + alert('Algo ha salido mal. Por favor intente más tarde') + window.location.reload() + }, 2000) + } + } + + return ( + <> + + + + + + + + + + + + + +
+ setEmail(e.target.value)} + /> +
+ setPassword(e.target.value)} + /> + setShowPassword((v) => !v)} + /> +
+ +
+ +
+
+ {message && ( + + {message.text} + + )} +
+ + ) +} diff --git a/frontend/src/entries/login.tsx b/frontend/src/entries/login.tsx new file mode 100644 index 0000000..6ed65ae --- /dev/null +++ b/frontend/src/entries/login.tsx @@ -0,0 +1,16 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { LoginForm } from '../components/LoginForm' + +const el = document.getElementById('login-app') +if (el) { + createRoot(el).render( + + + , + ) +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3052d8b..65638b4 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ rollupOptions: { input: { home: resolve(__dirname, 'src/entries/home.tsx'), + login: resolve(__dirname, 'src/entries/login.tsx'), }, }, }, diff --git a/home/templates/partials/login.html b/home/templates/partials/login.html index 30d21df..91ad321 100644 --- a/home/templates/partials/login.html +++ b/home/templates/partials/login.html @@ -22,31 +22,14 @@
- -
- {% csrf_token %} - - - - - - - - - - - - - -
- - -
- -
-
-
-
+ {% load django_vite %} + +
+ {% vite_asset 'src/entries/login.tsx' %}

He olvidado mi contraseña/usuario

No me ha llegado el enlace de activación