Fase 3 (isla): panel de votación (vote_points) en React/TSX
vote_points_view ya devolvía JSON en POST. VotePanel.tsx renderiza los sitios de votación (nombre, icono, PV) y cada botón abre el sitio en otra pestaña y hace POST del voto -> acredita PV o muestra el cooldown que devuelve el backend. La vista pasa vote_sites_data (serializable) por json_script. Se elimina el JS jQuery y el panel duplicado; la sección informativa se queda en Django. Verificado: check OK, render de la isla correcto, redirige a login sin sesión. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface VoteSite {
|
||||
name: string
|
||||
image_url: string
|
||||
points: number
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
voteUrl: string
|
||||
csrfToken: string
|
||||
sites: VoteSite[]
|
||||
}
|
||||
|
||||
export function VotePanel({ voteUrl, csrfToken, sites }: Props) {
|
||||
const [busyUrl, setBusyUrl] = useState<string | null>(null)
|
||||
const [message, setMessage] = useState<{ ok: boolean; html: string } | null>(null)
|
||||
|
||||
async function vote(url: string) {
|
||||
if (busyUrl) return
|
||||
// Abrir el sitio de votación en otra pestaña
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
setBusyUrl(url)
|
||||
setMessage(null)
|
||||
|
||||
const body = new URLSearchParams()
|
||||
body.set('vote', url)
|
||||
body.set('csrfmiddlewaretoken', csrfToken)
|
||||
|
||||
try {
|
||||
const resp = await fetch(voteUrl, {
|
||||
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()
|
||||
// El backend manda mensajes con o sin HTML; los envolvemos en verde si es éxito.
|
||||
const html = data.success ? `<span class="ok-form-response">${data.message ?? ''}</span>` : (data.message ?? '')
|
||||
setMessage({ ok: !!data.success, html })
|
||||
} catch {
|
||||
setMessage({ ok: false, html: '<span class="red-form-response">Error al registrar el voto. Inténtalo más tarde.</span>' })
|
||||
} finally {
|
||||
setBusyUrl(null)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<span>Nota:</span> Gtop100 puede demorar unos minutos en acreditar el voto.
|
||||
</p>
|
||||
<br />
|
||||
<div className="vote-sites">
|
||||
{sites.map((s) => (
|
||||
<div className="inline-div" key={s.url}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img className="img-med-icon" src={s.image_url} title={s.name} alt={s.name} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{s.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
PV: <span>{s.points}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="vote-button"
|
||||
disabled={busyUrl === s.url}
|
||||
onClick={() => vote(s.url)}
|
||||
>
|
||||
{busyUrl === s.url ? 'Votando…' : 'Votar'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<div className="alert-message" id="voteResponse" style={{ display: message ? 'block' : 'none' }}>
|
||||
{message && <span dangerouslySetInnerHTML={{ __html: message.html }} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user