Fase 3 (isla): cambiar correo en React/TSX
change_email_view ya devolvía JSON en POST (sesión requerida). ChangeEmailForm.tsx replica el form (contraseña actual con mostrar-ocultar, correo actual/nuevo/confirmar, token) y hace fetch form-urlencoded + CSRF a change-email; mensaje HTML. Sustituye el <form> + change_email_response.js por #change-email-app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
url: string
|
||||
csrfToken: string
|
||||
}
|
||||
|
||||
interface Field {
|
||||
name: string
|
||||
type: 'password' | 'email' | 'text'
|
||||
placeholder: string
|
||||
maxLength?: number
|
||||
toggle?: boolean
|
||||
}
|
||||
|
||||
const FIELDS: Field[] = [
|
||||
{ name: 'cur-password', type: 'password', placeholder: 'Contraseña actual', maxLength: 16, toggle: true },
|
||||
{ name: 'cur-email', type: 'email', placeholder: 'Correo actual' },
|
||||
{ name: 'new-email', type: 'email', placeholder: 'Nuevo correo' },
|
||||
{ name: 'conf-new-email', type: 'email', placeholder: 'Confirmar nuevo correo' },
|
||||
{ name: 'security-token', type: 'text', placeholder: 'Token de seguridad', maxLength: 6 },
|
||||
]
|
||||
|
||||
export function ChangeEmailForm({ url, csrfToken }: Props) {
|
||||
const [values, setValues] = useState<Record<string, string>>({})
|
||||
const [showPw, setShowPw] = useState(false)
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
if (busy) return
|
||||
setBusy(true)
|
||||
setMessage(null)
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const f of FIELDS) body.set(f.name, (values[f.name] || '').trim())
|
||||
body.set('csrfmiddlewaretoken', csrfToken)
|
||||
|
||||
try {
|
||||
const resp = await fetch(url, {
|
||||
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: { success?: boolean; message?: string } = await resp.json()
|
||||
setMessage({ ok: !!data.success, html: data.message || '' })
|
||||
} catch {
|
||||
setMessage({ ok: false, html: '<span class="red-form-response">Error en el servidor. Inténtalo más tarde.</span>' })
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form id="nw-change-email-form" onSubmit={handleSubmit} acceptCharset="utf-8">
|
||||
<table className="middle-center-table">
|
||||
<tbody>
|
||||
{FIELDS.map((f) => (
|
||||
<tr key={f.name}>
|
||||
<td>
|
||||
<input
|
||||
type={f.toggle ? (showPw ? 'text' : 'password') : f.type}
|
||||
name={f.name}
|
||||
id={f.name}
|
||||
placeholder={f.placeholder}
|
||||
maxLength={f.maxLength}
|
||||
required
|
||||
value={values[f.name] || ''}
|
||||
onChange={(e) => setValues((v) => ({ ...v, [f.name]: e.target.value }))}
|
||||
/>
|
||||
{f.toggle && (
|
||||
<span
|
||||
className={`far toggle-password ${showPw ? 'fa-eye-slash' : 'fa-eye'}`}
|
||||
onClick={() => setShowPw((s) => !s)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<td>
|
||||
<button type="submit" className="change-email-button" disabled={busy}>
|
||||
{busy ? 'Cambiando…' : 'Cambiar Correo'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<div className="alert-message" id="change-email-response" style={{ display: message ? 'block' : 'none' }}>
|
||||
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user