From cefa30dabc64275d0472a25c162d46742dd892cd Mon Sep 17 00:00:00 2001 From: adevopg Date: Sun, 12 Jul 2026 21:48:32 +0000 Subject: [PATCH] =?UTF-8?q?Fase=203=20(isla):=20panel=20de=20votaci=C3=B3n?= =?UTF-8?q?=20(vote=5Fpoints)=20en=20React/TSX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/components/VotePanel.tsx | 101 +++++++++++++++++++++++ frontend/src/entries/vote_points.tsx | 14 ++++ frontend/vite.config.ts | 1 + home/templates/partials/vote_points.html | 42 ++-------- home/views/points.py | 8 +- 5 files changed, 132 insertions(+), 34 deletions(-) create mode 100644 frontend/src/components/VotePanel.tsx create mode 100644 frontend/src/entries/vote_points.tsx diff --git a/frontend/src/components/VotePanel.tsx b/frontend/src/components/VotePanel.tsx new file mode 100644 index 0000000..39dbcc1 --- /dev/null +++ b/frontend/src/components/VotePanel.tsx @@ -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(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 ? `${data.message ?? ''}` : (data.message ?? '') + setMessage({ ok: !!data.success, html }) + } catch { + setMessage({ ok: false, html: 'Error al registrar el voto. Inténtalo más tarde.' }) + } finally { + setBusyUrl(null) + } + } + + return ( + <> +

+ Nota: Gtop100 puede demorar unos minutos en acreditar el voto. +

+
+
+ {sites.map((s) => ( +
+ + + + + + + + + + + + + + + +
+ {s.name} +
{s.name}
+ PV: {s.points} +
+ +
+
+ ))} +
+
+
+ {message && } +
+ + ) +} diff --git a/frontend/src/entries/vote_points.tsx b/frontend/src/entries/vote_points.tsx new file mode 100644 index 0000000..e5582bc --- /dev/null +++ b/frontend/src/entries/vote_points.tsx @@ -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( + + + , + ) +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b11f59..0814158 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -28,6 +28,7 @@ export default defineConfig({ gold_character: resolve(__dirname, 'src/entries/gold_character.tsx'), transfer_character: resolve(__dirname, 'src/entries/transfer_character.tsx'), rename_guild: resolve(__dirname, 'src/entries/rename_guild.tsx'), + vote_points: resolve(__dirname, 'src/entries/vote_points.tsx'), }, }, }, diff --git a/home/templates/partials/vote_points.html b/home/templates/partials/vote_points.html index f560656..9756792 100644 --- a/home/templates/partials/vote_points.html +++ b/home/templates/partials/vote_points.html @@ -48,39 +48,15 @@

Sitios de votación

- -
- -

Nota: Gtop100 puede demorar unos minutos en acreditar el voto.

-
-
-
- {% for site in vote_sites %} -
- - - - - - - - - - - - - -
{{ site.name }}
{{ site.name }}
PV: {{ site.points }}
-
- {% csrf_token %} - -
-
-
-{% endfor %} - -
-
+
+ {% load django_vite %} + +
+ {{ vote_sites_data|json_script:"vote-points-data" }} + {% vite_asset 'src/entries/vote_points.tsx' %}
diff --git a/home/views/points.py b/home/views/points.py index ff6265b..1d8375f 100644 --- a/home/views/points.py +++ b/home/views/points.py @@ -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.'}) 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): """ Vista para la página de D-Points.