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