Fase 3 (isla): dashboard de «Mi cuenta» en React/TSX
De las ~508 líneas de partials/my-account.html, solo la parte superior es dinámica (datos de cuenta, estado, puntos, token, personajes); el resto son paneles de navegación estáticos (enlaces), que se quedan en Django. - API: account_me_api -> /api/account/me/ (protegido por sesión, 401 si no hay): user_info, account_status, dp/vp, créditos Battlepay, token de seguridad y personajes. Reutiliza get_account_characters/get_account_status/formatear_fecha. Resiliente si acore no está disponible. - React: AccountDashboard.tsx (fieldsets de datos + estado con contador de baneo en vivo + rejilla de personajes). Entry my_account. - partials/my-account.html: los 2 fieldsets + la rejilla de personajes + el script del contador se sustituyen por #account-dashboard-app; los paneles de utilidades e historiales siguen en Django. Verificado: check OK, /api/account/me/ 401 sin sesión, my-account redirige a login. (El render con datos reales requiere sesión + BD AzerothCore.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,176 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
interface UserInfo {
|
||||||
|
bnet_email: string
|
||||||
|
username: string
|
||||||
|
reg_mail: string
|
||||||
|
email: string
|
||||||
|
last_ip: string
|
||||||
|
last_attempt_ip: string
|
||||||
|
joindate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountStatus {
|
||||||
|
is_banned: boolean
|
||||||
|
unban_date?: string
|
||||||
|
remaining_time?: number
|
||||||
|
is_recruited?: boolean
|
||||||
|
recruited_count?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Character {
|
||||||
|
name: string
|
||||||
|
level: number
|
||||||
|
gold: number
|
||||||
|
silver: number
|
||||||
|
copper: number
|
||||||
|
zone: string
|
||||||
|
class_css: string
|
||||||
|
image_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountData {
|
||||||
|
authenticated: boolean
|
||||||
|
error?: string
|
||||||
|
user_info: UserInfo
|
||||||
|
account_status: AccountStatus
|
||||||
|
dp: number
|
||||||
|
vp: number
|
||||||
|
battlepay_credits: number
|
||||||
|
token_status: string
|
||||||
|
token_date: string | null
|
||||||
|
characters: Character[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtRemaining(secs: number): string {
|
||||||
|
if (secs <= 0) return '0h 0m 0s'
|
||||||
|
const h = Math.floor(secs / 3600)
|
||||||
|
const m = Math.floor((secs % 3600) / 60)
|
||||||
|
const s = secs % 60
|
||||||
|
return `${h}h ${m}m ${s}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AccountDashboard() {
|
||||||
|
const [data, setData] = useState<AccountData | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [remaining, setRemaining] = useState<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch('/api/account/me/', { headers: { Accept: 'application/json' }, credentials: 'same-origin' })
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d: AccountData) => {
|
||||||
|
if (cancelled) return
|
||||||
|
if (d.error) setError(d.error)
|
||||||
|
else {
|
||||||
|
setData(d)
|
||||||
|
if (d.account_status?.is_banned && d.account_status.remaining_time) {
|
||||||
|
setRemaining(d.account_status.remaining_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => !cancelled && setError('No se pudieron cargar los datos de la cuenta.'))
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (remaining === null) return
|
||||||
|
if (remaining <= 0) {
|
||||||
|
window.location.reload()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const id = setInterval(() => setRemaining((r) => (r === null ? null : r - 1)), 1000)
|
||||||
|
return () => clearInterval(id)
|
||||||
|
}, [remaining])
|
||||||
|
|
||||||
|
if (error) return <p className="red-form-response">{error}</p>
|
||||||
|
if (!data) return <p>Cargando datos de la cuenta…</p>
|
||||||
|
|
||||||
|
const { user_info: u, account_status: st } = data
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<fieldset className="account-fieldset">
|
||||||
|
<legend>Datos básicos</legend>
|
||||||
|
<div className="separate">
|
||||||
|
<p>Cuenta (Battle.net): <span>{u.bnet_email}</span></p>
|
||||||
|
<p>Cuenta de juego: <span>{u.username}</span></p>
|
||||||
|
<p>Correo de registro: <span>{u.reg_mail}</span></p>
|
||||||
|
<p>Correo actual: <span>{u.email}</span></p>
|
||||||
|
<p>Fecha de registro: <span>{u.joindate}</span></p>
|
||||||
|
<p>Última IP (web): <span>{u.last_ip}</span></p>
|
||||||
|
<p>Última IP (Servidor): <span>{u.last_attempt_ip}</span></p>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset className="account-fieldset">
|
||||||
|
<legend>Estado de la cuenta</legend>
|
||||||
|
<div className="separate">
|
||||||
|
<img
|
||||||
|
src="/static/nw-themes/nw-ryu/nw-images/nw-ranks/1_Newbie.svg"
|
||||||
|
className="nw-rank"
|
||||||
|
alt="Nivel 1"
|
||||||
|
title="Nivel 1"
|
||||||
|
/>
|
||||||
|
<p>PD: <span>{data.dp}</span></p>
|
||||||
|
<p>PV: <span>{data.vp}</span></p>
|
||||||
|
<p>Créditos Battlepay: <span>{data.battlepay_credits}</span></p>
|
||||||
|
<p>
|
||||||
|
Token de Seguridad:{' '}
|
||||||
|
<span>
|
||||||
|
{data.token_status === 'Solicitado' ? `${data.token_status} - ${data.token_date}` : data.token_status}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{st?.is_banned ? (
|
||||||
|
<>
|
||||||
|
<p>Cuenta baneada: <span>Sí</span></p>
|
||||||
|
<p>Fin de la suspensión: <span>{st.unban_date}</span></p>
|
||||||
|
<p id="ban-timer">
|
||||||
|
Tiempo restante: <span>{remaining !== null ? fmtRemaining(remaining) : ''}</span>
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>Cuenta baneada: <span>No</span> (<a href="ban-history">Consultar historial</a>)</p>
|
||||||
|
<p>Cuenta reclutada: <span>{st?.is_recruited ? 'Sí' : 'No'}</span></p>
|
||||||
|
<p>Amigos reclutados: <span>{st?.recruited_count ?? 0}</span></p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{data.characters.length > 0 && (
|
||||||
|
<div className="box-content">
|
||||||
|
<div className="title-box-content">
|
||||||
|
<h2>Mis personajes</h2>
|
||||||
|
</div>
|
||||||
|
<div className="characters-grid">
|
||||||
|
{data.characters.map((c) => (
|
||||||
|
<div className={`char-box char-box-${c.class_css}`} key={c.name}>
|
||||||
|
<div className="char-text">
|
||||||
|
<img className="img-small-icon char-icon" src={`/static/${c.image_url}`} alt={c.name} />
|
||||||
|
<p className={`${c.class_css} big-font`}>{c.name}</p>
|
||||||
|
<p><span>Nivel {c.level}</span></p>
|
||||||
|
<p className="second-brown">{c.zone}</p>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
{c.gold}
|
||||||
|
<img src="/static/nw-themes/nw-ryu/nw-images/nw-icons/money-gold.gif" width="10" />{' '}
|
||||||
|
{c.silver}
|
||||||
|
<img src="/static/nw-themes/nw-ryu/nw-images/nw-icons/money-silver.webp" width="10" />{' '}
|
||||||
|
{c.copper}
|
||||||
|
<img src="/static/nw-themes/nw-ryu/nw-images/nw-icons/money-copper.gif" width="10" />
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { AccountDashboard } from '../components/AccountDashboard'
|
||||||
|
|
||||||
|
const el = document.getElementById('account-dashboard-app')
|
||||||
|
if (el) {
|
||||||
|
createRoot(el).render(
|
||||||
|
<StrictMode>
|
||||||
|
<AccountDashboard />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export default defineConfig({
|
|||||||
select_account: resolve(__dirname, 'src/entries/select_account.tsx'),
|
select_account: resolve(__dirname, 'src/entries/select_account.tsx'),
|
||||||
recover: resolve(__dirname, 'src/entries/recover.tsx'),
|
recover: resolve(__dirname, 'src/entries/recover.tsx'),
|
||||||
reset_password: resolve(__dirname, 'src/entries/reset_password.tsx'),
|
reset_password: resolve(__dirname, 'src/entries/reset_password.tsx'),
|
||||||
|
my_account: resolve(__dirname, 'src/entries/my_account.tsx'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ urlpatterns = [
|
|||||||
path('home/news/', views.home_news_api, name='api_home_news'),
|
path('home/news/', views.home_news_api, name='api_home_news'),
|
||||||
path('home/status/', views.home_status_api, name='api_home_status'),
|
path('home/status/', views.home_status_api, name='api_home_status'),
|
||||||
path('account/select/', views.account_select_api, name='api_account_select'),
|
path('account/select/', views.account_select_api, name='api_account_select'),
|
||||||
|
path('account/me/', views.account_me_api, name='api_account_me'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% load django_vite %}
|
||||||
<div class="main-page">
|
<div class="main-page">
|
||||||
<div class="middle-content">
|
<div class="middle-content">
|
||||||
<div class="body-content">
|
<div class="body-content">
|
||||||
@@ -6,89 +7,11 @@
|
|||||||
<div class="title-box-content"><h2>Información</h2></div>
|
<div class="title-box-content"><h2>Información</h2></div>
|
||||||
<div class="body-box-content">
|
<div class="body-box-content">
|
||||||
<script src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-js-handlers/my_account_response.js"></script>
|
<script src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-js-handlers/my_account_response.js"></script>
|
||||||
<fieldset class="account-fieldset">
|
<div id="account-dashboard-app"></div>
|
||||||
<legend>Datos básicos</legend>
|
{% vite_asset 'src/entries/my_account.tsx' %}
|
||||||
<div class="separate">
|
</div>
|
||||||
<p>Cuenta (Battle.net): <span>{{ request.session.bnet_email|default:user_info.email }}</span></p>
|
|
||||||
<p>Cuenta de juego: <span>{{ user_info.username }}</span></p>
|
|
||||||
<p>Correo de registro: <span>{{ user_info.reg_mail }}</span></p>
|
|
||||||
<p>Correo actual: <span>{{ user_info.email }}</span></p>
|
|
||||||
<p>Fecha de registro: <span>{{ user_info.joindate }}</span></p>
|
|
||||||
<p>Última IP (web): <span>{{ user_info.last_ip }}</span></p>
|
|
||||||
<p>Última IP (Servidor): <span>{{ user_info.last_attempt_ip }}</span></p>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
|
||||||
<fieldset class="account-fieldset">
|
|
||||||
<legend>Estado de la cuenta</legend>
|
|
||||||
<div class="separate">
|
|
||||||
<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-ranks/1_Newbie.svg" class="nw-rank" alt="Nivel 1" title="Nivel 1">
|
|
||||||
<p>PD: <span>{{ dp }}</span></p>
|
|
||||||
<p>PV: <span>{{ vp }}</span></p>
|
|
||||||
<p>Créditos Battlepay: <span>{{ battlepay_credits }}</span></p>
|
|
||||||
<p>Token de Seguridad:
|
|
||||||
<span>
|
|
||||||
{% if token_status == "Solicitado" %}
|
|
||||||
{{ token_status }} - {{ token_date }}
|
|
||||||
{% else %}
|
|
||||||
{{ token_status }}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{% if account_status.is_banned %}
|
|
||||||
<p>Cuenta baneada: <span>Sí</span></p>
|
|
||||||
<p>Fin de la suspensión: <span>{{ account_status.unban_date }}</span></p>
|
|
||||||
<p id="ban-timer">Tiempo restante: <span id="remaining-time">{{ account_status.remaining_time }}</span></p>
|
|
||||||
{% else %}
|
|
||||||
<p>Cuenta baneada: <span>No</span> (<a href="ban-history">Consultar historial</a>)</p>
|
|
||||||
|
|
||||||
{% if account_status.is_recruited %}
|
|
||||||
<p>Cuenta reclutada: <span>Sí</span></p>
|
|
||||||
{% else %}
|
|
||||||
<p>Cuenta reclutada: <span>No</span></p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<p>Amigos reclutados: <span>{{ account_status.recruited_count }}</span></p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% if has_characters %}
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="title-box-content">
|
|
||||||
<h2>Mis personajes</h2>
|
|
||||||
</div>
|
|
||||||
<div class="characters-grid">
|
|
||||||
{% for character in characters %}
|
|
||||||
<div class="char-box char-box-{{ character.class_css }}">
|
|
||||||
<div class="char-text">
|
|
||||||
<!-- Imagen del personaje basada en raza, clase y género -->
|
|
||||||
<img class="img-small-icon char-icon" src="{{ URL_PRINCIPAL }}/static/{{ character.image_url }}" alt="{{ character.name }}">
|
|
||||||
|
|
||||||
<!-- Información del personaje -->
|
|
||||||
<p class="{{ character.class_css }} big-font">{{ character.name }}</p>
|
|
||||||
<p><span>Nivel {{ character.level }}</span></p>
|
|
||||||
|
|
||||||
<!-- Mostrar zona del personaje -->
|
|
||||||
<p class="second-brown">
|
|
||||||
{{ character.zone }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Mostrar monedas del personaje -->
|
|
||||||
<p>
|
|
||||||
<span>
|
|
||||||
{{ character.gold }}<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/money-gold.gif" width="10px">
|
|
||||||
{{ character.silver }}<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/money-silver.webp" width="10px">
|
|
||||||
{{ character.copper }}<img src="{{ URL_PRINCIPAL }}/static/nw-themes/nw-ryu/nw-images/nw-icons/money-copper.gif" width="10px">
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<style>
|
<style>
|
||||||
.title-box-content {
|
.title-box-content {
|
||||||
margin-bottom: 20px; /* Separación entre el título y la cuadrícula */
|
margin-bottom: 20px; /* Separación entre el título y la cuadrícula */
|
||||||
@@ -484,25 +407,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
let remainingTime = parseInt(document.getElementById('remaining-time').textContent);
|
|
||||||
|
|
||||||
function updateBanTimer() {
|
|
||||||
if (remainingTime > 0) {
|
|
||||||
remainingTime--;
|
|
||||||
const hours = Math.floor(remainingTime / 3600);
|
|
||||||
const minutes = Math.floor((remainingTime % 3600) / 60);
|
|
||||||
const seconds = remainingTime % 60;
|
|
||||||
document.getElementById('ban-timer').textContent = `Tiempo restante: ${hours}h ${minutes}m ${seconds}s`;
|
|
||||||
|
|
||||||
if (remainingTime === 0) {
|
|
||||||
location.reload(); // Recargar la página cuando el tiempo llegue a 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Actualizar el contador cada segundo
|
|
||||||
setInterval(updateBanTimer, 1000);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from ._base import *
|
from ._base import *
|
||||||
from .pages import check_server_status, get_online_characters_count
|
from .pages import check_server_status, get_online_characters_count
|
||||||
from .auth import _set_game_account_session
|
from .auth import _set_game_account_session
|
||||||
|
from .account import get_account_characters, get_account_status, formatear_fecha
|
||||||
|
|
||||||
# Endpoints JSON que alimentan las islas React/TSX del frontend (Vite).
|
# Endpoints JSON que alimentan las islas React/TSX del frontend (Vite).
|
||||||
# Se montan bajo /api/ (fuera de i18n_patterns). De momento con JsonResponse;
|
# Se montan bajo /api/ (fuera de i18n_patterns). De momento con JsonResponse;
|
||||||
@@ -60,3 +61,49 @@ def account_select_api(request):
|
|||||||
return JsonResponse({"success": False, "error": "Cuenta de juego no válida."})
|
return JsonResponse({"success": False, "error": "Cuenta de juego no válida."})
|
||||||
_set_game_account_session(request, chosen)
|
_set_game_account_session(request, chosen)
|
||||||
return JsonResponse({"success": True, "redirect": reverse("my-account")})
|
return JsonResponse({"success": True, "redirect": reverse("my-account")})
|
||||||
|
|
||||||
|
|
||||||
|
def account_me_api(request):
|
||||||
|
"""Datos del panel «Mi cuenta» (isla): info de cuenta, estado, puntos,
|
||||||
|
créditos Battlepay, token de seguridad y personajes. Requiere sesión."""
|
||||||
|
username = request.session.get('username')
|
||||||
|
if not username:
|
||||||
|
return JsonResponse({"authenticated": False}, status=401)
|
||||||
|
try:
|
||||||
|
with connections['acore_auth'].cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"SELECT id, username, reg_mail, email, last_ip, last_attempt_ip, joindate "
|
||||||
|
"FROM account WHERE username = %s", [username])
|
||||||
|
row = cursor.fetchone()
|
||||||
|
if not row:
|
||||||
|
return JsonResponse({"authenticated": False}, status=404)
|
||||||
|
account_id = row[0]
|
||||||
|
characters = get_account_characters(account_id)
|
||||||
|
status = get_account_status(account_id)
|
||||||
|
points = HomeApiPoints.objects.filter(accountID=account_id).first()
|
||||||
|
sectoken = SecurityToken.objects.filter(user_id=account_id).first()
|
||||||
|
with connections['acore_auth'].cursor() as cursor:
|
||||||
|
credits = bnet.get_battlepay_credits(cursor, request.session.get('bnet_id'))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("account_me_api: %s", e)
|
||||||
|
return JsonResponse({"authenticated": True, "error": "No se pudieron cargar los datos de la cuenta."}, status=200)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
"authenticated": True,
|
||||||
|
"user_info": {
|
||||||
|
"bnet_email": request.session.get('bnet_email') or row[3],
|
||||||
|
"username": row[1],
|
||||||
|
"reg_mail": row[2],
|
||||||
|
"email": row[3],
|
||||||
|
"last_ip": row[4],
|
||||||
|
"last_attempt_ip": row[5],
|
||||||
|
"joindate": formatear_fecha(row[6]),
|
||||||
|
},
|
||||||
|
"account_status": status,
|
||||||
|
"dp": points.dp if points else 0,
|
||||||
|
"vp": points.vp if points else 0,
|
||||||
|
"battlepay_credits": credits,
|
||||||
|
"token_status": "Solicitado" if sectoken else "Sin solicitar",
|
||||||
|
"token_date": sectoken.created_at.strftime('%H:%M:%S %d-%m-%Y') if sectoken else None,
|
||||||
|
"characters": characters,
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user