beba7301a1
- PvP: pestaña Campos de batalla (pvpstats) con resultado, stats por partida y fecha/hora. - Logros: rejilla por categoría con anillos de progreso + detalle por categoría (datos de Achievement.db2 + Achievement_Category.db2, iconos vía wowhead). - Bandas: progreso por dificultad (10/25/heroico) con jefes reales de DungeonEncounter.db2 y tooltip de jefes; agrupado por expansión (Clásico/TBC/WotLK). - Mazmorras: mismo sistema, Normal + Heroico, Clásico/TBC/WotLK. - Página de hermandad estilo armería: cabecera (emblema, puntos = unión de logros de miembros sin duplicar, nº miembros, fundada), pestañas Hermandad/Logros, roster con iconos de clase/raza, columna Función (rol por spec), filtros clase/función, orden por rango/puntos/nivel de objeto/nivel/nombre, paginación. - Barra lateral: mensaje diario con login (iron-session, gated por pertenencia), logros recientes y desgloses de clases y funciones. - Cabecera de personaje: enlace a la hermandad + nombre del reino. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
5.5 KiB
TypeScript
179 lines
5.5 KiB
TypeScript
'use client'
|
||
|
||
import { useEffect, useRef, useState } from 'react'
|
||
import { WowheadRefresh } from './WowheadRefresh'
|
||
import { wowheadUrl, wowheadData } from '@/lib/wowhead'
|
||
|
||
export type ActivityEntry = { id: number; name: string; icon: string; date: number }
|
||
|
||
/**
|
||
* Pestaña de Logros: filtros por ID y por nombre, tabla Logro | Fecha, paginación.
|
||
* Mismo estilo que Misiones. Datos vía /api/armory/activity; 1ª página del servidor.
|
||
*/
|
||
export function ArmoryActivity({
|
||
guid,
|
||
locale,
|
||
initial,
|
||
total: initialTotal,
|
||
grandTotal,
|
||
pageSize,
|
||
labels,
|
||
}: {
|
||
guid: number
|
||
locale: string
|
||
initial: ActivityEntry[]
|
||
total: number
|
||
grandTotal: number
|
||
pageSize: number
|
||
labels: {
|
||
filterId: string
|
||
filterName: string
|
||
empty: string
|
||
logro: string
|
||
date: string
|
||
title: string
|
||
countLine: string
|
||
}
|
||
}) {
|
||
const [entries, setEntries] = useState<ActivityEntry[]>(initial)
|
||
const [total, setTotal] = useState(initialTotal)
|
||
const [page, setPage] = useState(1)
|
||
const [qid, setQid] = useState('')
|
||
const [qname, setQname] = useState('')
|
||
const [loading, setLoading] = useState(false)
|
||
const pages = Math.max(1, Math.ceil(total / pageSize))
|
||
|
||
const load = async (p: number, id: string, name: string) => {
|
||
setLoading(true)
|
||
try {
|
||
const res = await fetch(
|
||
`/api/armory/activity?guid=${guid}&page=${p}&id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}&locale=${locale}`,
|
||
)
|
||
const data = await res.json()
|
||
setEntries(Array.isArray(data.entries) ? data.entries : [])
|
||
setTotal(Number(data.total) || 0)
|
||
} catch {
|
||
setEntries([])
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const skip = useRef(true)
|
||
useEffect(() => {
|
||
if (skip.current) {
|
||
skip.current = false
|
||
return
|
||
}
|
||
const t = setTimeout(() => {
|
||
setPage(1)
|
||
load(1, qid, qname)
|
||
}, 300)
|
||
return () => clearTimeout(t)
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [qid, qname])
|
||
|
||
const goto = (p: number) => {
|
||
if (p < 1 || p > pages || p === page || loading) return
|
||
setPage(p)
|
||
load(p, qid, qname)
|
||
}
|
||
|
||
const fmt = (ts: number) => {
|
||
const d = new Date(ts * 1000)
|
||
const p = (n: number) => String(n).padStart(2, '0')
|
||
return `${p(d.getDate())}/${p(d.getMonth() + 1)}/${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}`
|
||
}
|
||
|
||
const win = 2
|
||
const from = Math.max(1, page - win)
|
||
const to = Math.min(pages, page + win)
|
||
const nums: number[] = []
|
||
for (let p = from; p <= to; p++) nums.push(p)
|
||
|
||
const countLine = labels.countLine
|
||
.replace('{total}', String(total))
|
||
.replace('{page}', String(page))
|
||
.replace('{pages}', String(pages))
|
||
|
||
return (
|
||
<>
|
||
<WowheadRefresh dep={`${page}-${qid}-${qname}-${entries.length}`} />
|
||
|
||
<div className="armory-quest-filters armory-ach-filters">
|
||
<input className="armory-ach-search" value={qid} onChange={(e) => setQid(e.target.value)} placeholder={labels.filterId} inputMode="numeric" />
|
||
<input className="armory-ach-search" value={qname} onChange={(e) => setQname(e.target.value)} placeholder={labels.filterName} />
|
||
</div>
|
||
<p className="armory-quest-count">{countLine}</p>
|
||
|
||
<div className="armory-quest-title">
|
||
<i className="fas fa-trophy" /> {labels.title} <span className="armory-quest-title-count">({grandTotal})</span>
|
||
</div>
|
||
|
||
{entries.length === 0 ? (
|
||
<p className="armory-hint">{labels.empty}</p>
|
||
) : (
|
||
<table className={`armory-quest-table${loading ? ' loading' : ''}`}>
|
||
<thead>
|
||
<tr>
|
||
<th>{labels.logro}</th>
|
||
<th className="armory-quest-th-status">{labels.date}</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{entries.map((a) => (
|
||
<tr key={`${a.id}-${a.date}`}>
|
||
<td>
|
||
<a
|
||
className="armory-quest-link armory-ach-link"
|
||
href={wowheadUrl('achievement', a.id, locale)}
|
||
data-wowhead={wowheadData('achievement', a.id, locale)}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
>
|
||
{a.name}
|
||
</a>
|
||
<span className="armory-quest-id"> ({a.id})</span>
|
||
</td>
|
||
<td className="armory-quest-td-status armory-ach-date">{fmt(a.date)}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
|
||
{pages > 1 && (
|
||
<div className="armory-pager">
|
||
<button onClick={() => goto(page - 1)} disabled={page <= 1} type="button" aria-label="Anterior">
|
||
‹
|
||
</button>
|
||
{from > 1 && (
|
||
<>
|
||
<button onClick={() => goto(1)} type="button">
|
||
1
|
||
</button>
|
||
{from > 2 && <span className="armory-pager-dots">…</span>}
|
||
</>
|
||
)}
|
||
{nums.map((p) => (
|
||
<button key={p} className={p === page ? 'active' : ''} onClick={() => goto(p)} type="button">
|
||
{p}
|
||
</button>
|
||
))}
|
||
{to < pages && (
|
||
<>
|
||
{to < pages - 1 && <span className="armory-pager-dots">…</span>}
|
||
<button onClick={() => goto(pages)} type="button">
|
||
{pages}
|
||
</button>
|
||
</>
|
||
)}
|
||
<button onClick={() => goto(page + 1)} disabled={page >= pages} type="button" aria-label="Siguiente">
|
||
›
|
||
</button>
|
||
</div>
|
||
)}
|
||
</>
|
||
)
|
||
}
|