Fase 3 (isla): recuperar ítems (restore_items) en React/TSX, flujo de 2 pasos

restore_items_view ahora responde JSON cuando la petición es AJAX (X-Requested-With):
GET con character_name -> {items}; POST recover_id -> {success, message}. Se conserva
el flujo legacy (redirect+messages) para no-AJAX. RestoreItemsForm.tsx: paso 1 elegir
personaje y buscar ítems borrados (SOAP), paso 2 elegir ítem y recuperarlo; refresca la
lista tras recuperar. La vista pasa character_names por json_script.

Verificado: check OK, render de la isla, redirige a login sin sesión.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:01:19 +00:00
parent 08c36619f0
commit 68cfe7594b
5 changed files with 169 additions and 35 deletions
@@ -0,0 +1,124 @@
import { useState } from 'react'
interface DeletedItem {
recover_id: string
item_name: string
item_id: string
count: string
}
interface Props {
url: string
csrfToken: string
characters: string[]
}
export function RestoreItemsForm({ url, csrfToken, characters }: Props) {
const [character, setCharacter] = useState('')
const [items, setItems] = useState<DeletedItem[] | null>(null)
const [recoverId, setRecoverId] = useState('')
const [busy, setBusy] = useState(false)
const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
async function doSearch(char: string, keepMessage = false) {
if (!char) return
setBusy(true)
if (!keepMessage) setMessage(null)
setItems(null)
setRecoverId('')
try {
const r = await fetch(`${url}?character_name=${encodeURIComponent(char)}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json' },
credentials: 'same-origin',
})
const d: { items?: DeletedItem[]; error?: string } = await r.json()
if (d.error) setMessage({ ok: false, html: `<span class="red-form-response">${d.error}</span>` })
else setItems(d.items || [])
} catch {
setMessage({ ok: false, html: '<span class="red-form-response">Error al buscar los ítems.</span>' })
} finally {
setBusy(false)
}
}
function search(e: React.FormEvent) {
e.preventDefault()
if (busy) return
void doSearch(character)
}
async function recover(e: React.FormEvent) {
e.preventDefault()
if (!recoverId || busy) return
setBusy(true)
setMessage(null)
const body = new URLSearchParams()
body.set('recover_id', recoverId)
body.set('character_name', character)
body.set('csrfmiddlewaretoken', csrfToken)
try {
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': csrfToken,
Accept: 'application/json',
},
credentials: 'same-origin',
body: body.toString(),
})
const d: { success?: boolean; message?: string } = await r.json()
setMessage({ ok: !!d.success, html: d.message || '' })
if (d.success) {
// refrescar la lista de ítems tras recuperar uno (conservando el mensaje)
void doSearch(character, true)
}
} catch {
setMessage({ ok: false, html: '<span class="red-form-response">Error al recuperar el ítem.</span>' })
} finally {
setBusy(false)
}
}
return (
<>
<form onSubmit={search}>
<select value={character} onChange={(e) => setCharacter(e.target.value)} required>
<option value="" disabled>Selecciona un personaje</option>
{characters.map((c) => (
<option value={c} key={c}>{c}</option>
))}
</select>
<button type="submit" disabled={busy || !character}>
{busy ? 'Buscando…' : 'Buscar Ítems Eliminados'}
</button>
</form>
{items !== null && (
items.length > 0 ? (
<form onSubmit={recover}>
<input type="hidden" name="character_name" value={character} />
<select value={recoverId} onChange={(e) => setRecoverId(e.target.value)} required>
<option value="" disabled>Selecciona un ítem</option>
{items.map((it) => (
<option value={it.recover_id} key={it.recover_id}>
{it.item_name} (x{it.count})
</option>
))}
</select>
<button type="submit" disabled={busy || !recoverId}>
{busy ? 'Recuperando…' : 'Recuperar Ítem'}
</button>
</form>
) : (
<p>No hay ítems eliminados para este personaje.</p>
)
)}
<div className="alert-message" style={{ display: message ? 'block' : 'none' }}>
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
</div>
</>
)
}