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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user