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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { VotePanel } from '../components/VotePanel'
|
||||||
|
|
||||||
|
const el = document.getElementById('vote-points-app')
|
||||||
|
if (el) {
|
||||||
|
const dEl = document.getElementById('vote-points-data')
|
||||||
|
const sites = dEl ? JSON.parse(dEl.textContent || '[]') : []
|
||||||
|
createRoot(el).render(
|
||||||
|
<StrictMode>
|
||||||
|
<VotePanel voteUrl={el.dataset.url || ''} csrfToken={el.dataset.csrf || ''} sites={sites} />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ export default defineConfig({
|
|||||||
gold_character: resolve(__dirname, 'src/entries/gold_character.tsx'),
|
gold_character: resolve(__dirname, 'src/entries/gold_character.tsx'),
|
||||||
transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'),
|
transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'),
|
||||||
rename_guild: resolve(__dirname, 'src/entries/rename_guild.tsx'),
|
rename_guild: resolve(__dirname, 'src/entries/rename_guild.tsx'),
|
||||||
|
vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,39 +48,15 @@
|
|||||||
|
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="title-box-content"><h2>Sitios de votación</h2></div>
|
<div class="title-box-content"><h2>Sitios de votación</h2></div>
|
||||||
<script src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-js-handlers/vote_points_response.js"></script>
|
<div class="body-box-content centered">
|
||||||
<div class="body-box-content centered" id="vote-panel">
|
{% load django_vite %}
|
||||||
<button class="refresh-button" onclick="doRefresh();"><img class="refresh-icon" src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/refresh.webp" title="Refrescar" alt="Refrescar"></button>
|
<!-- Isla React (Vite): panel de votación. Cada botón abre el sitio
|
||||||
<p><span>Nota:</span> Gtop100 puede demorar unos minutos en acreditar el voto.</p>
|
y hace POST del voto (acredita PV o muestra cooldown). -->
|
||||||
<br>
|
<div id="vote-points-app"
|
||||||
<div id="vote-panel">
|
data-csrf="{{ csrf_token }}"
|
||||||
<div id="vote-panel">
|
data-url="{% url 'vote_points' %}"></div>
|
||||||
{% for site in vote_sites %}
|
{{ vote_sites_data|json_script:"vote-points-data" }}
|
||||||
<div class="inline-div">
|
{% vite_asset 'src/entries/vote_points.tsx' %}
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td><img class="img-med-icon" src="{{ site.image_url }}" title="{{ site.name }}" alt="{{ site.name }}"></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>{{ site.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>PV: <span>{{ site.points }}</span></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<form method="POST" class="nw-vote-form" accept-charset="utf-8">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button type="button" class="vote-button" data-url="{{ site.url }}" data-remaining-time="{{ site.remaining_time }}">Votar</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
<div class="alert-message" id="voteResponse"></div>
|
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -59,7 +59,13 @@ def vote_points_view(request):
|
|||||||
return JsonResponse({'success': True, 'message': f'Has votado en {site.name}. Se han acreditado {site.points} PV.'})
|
return JsonResponse({'success': True, 'message': f'Has votado en {site.name}. Se han acreditado {site.points} PV.'})
|
||||||
|
|
||||||
vote_sites = VoteSite.objects.all()
|
vote_sites = VoteSite.objects.all()
|
||||||
return render(request, 'account/vote_points.html', {'vote_sites': vote_sites})
|
return render(request, 'account/vote_points.html', {
|
||||||
|
'vote_sites': vote_sites,
|
||||||
|
'vote_sites_data': [
|
||||||
|
{'name': s.name, 'image_url': s.image_url, 'points': s.points, 'url': s.url}
|
||||||
|
for s in vote_sites
|
||||||
|
],
|
||||||
|
})
|
||||||
def d_points_view(request):
|
def d_points_view(request):
|
||||||
"""
|
"""
|
||||||
Vista para la página de D-Points.
|
Vista para la página de D-Points.
|
||||||
|
|||||||
Reference in New Issue
Block a user