Armería: PvP campos de batalla, logros por categoría, bandas/mazmorras y página de hermandad

- 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>
This commit is contained in:
2026-07-17 01:18:30 +00:00
parent 476e5dba87
commit beba7301a1
203 changed files with 31304 additions and 269 deletions
+118
View File
@@ -0,0 +1,118 @@
'use client'
import { useState } from 'react'
export type ReputationEntry = {
faction: number
name: string
standing: number
rank: string
rankIndex: number
color: string
cur: number
max: number
}
/** Pestaña de Reputaciones: búsqueda en tiempo real + paginación + barra de rango/progreso. */
export function ArmoryReputations({
reputations,
pageSize,
labels,
}: {
reputations: ReputationEntry[]
pageSize: number
labels: { title: string; search: string; empty: string }
}) {
const [q, setQ] = useState('')
const [page, setPage] = useState(1)
const query = q.trim().toLowerCase()
const filtered = query ? reputations.filter((r) => r.name.toLowerCase().includes(query)) : reputations
const pages = Math.max(1, Math.ceil(filtered.length / pageSize))
const p = Math.min(page, pages)
const pageItems = filtered.slice((p - 1) * pageSize, (p - 1) * pageSize + pageSize)
const win = 2
const from = Math.max(1, p - win)
const to = Math.min(pages, p + win)
const nums: number[] = []
for (let i = from; i <= to; i++) nums.push(i)
return (
<div className="armory-panel armory-reps-panel">
<div className="armory-quest-title">
<i className="fas fa-handshake" /> {labels.title}{' '}
<span className="armory-quest-title-count">({reputations.length})</span>
</div>
<input
className="armory-ach-search"
value={q}
onChange={(e) => {
setQ(e.target.value)
setPage(1)
}}
placeholder={labels.search}
/>
{filtered.length === 0 ? (
<p className="armory-hint">{labels.empty}</p>
) : (
<div className="armory-reps">
{pageItems.map((r) => (
<div className="armory-rep" key={r.faction}>
<div className="armory-rep-head">
<span className="armory-rep-name">{r.name}</span>
<span className="armory-rep-standing">
<span className="armory-rep-rank" style={{ color: r.color }}>
{r.rank}
</span>
{r.rankIndex < 7 ? (
<span className="armory-rep-nums">
{' '}
{r.cur.toLocaleString()} / {r.max.toLocaleString()}
</span>
) : null}
</span>
</div>
<div className="armory-rep-bar">
<span style={{ width: `${Math.round((r.cur / Math.max(1, r.max)) * 100)}%`, background: r.color }} />
</div>
</div>
))}
</div>
)}
{pages > 1 && (
<div className="armory-pager">
<button onClick={() => setPage(p - 1)} disabled={p <= 1} type="button" aria-label="Anterior">
</button>
{from > 1 && (
<>
<button onClick={() => setPage(1)} type="button">
1
</button>
{from > 2 && <span className="armory-pager-dots"></span>}
</>
)}
{nums.map((n) => (
<button key={n} className={n === p ? 'active' : ''} onClick={() => setPage(n)} type="button">
{n}
</button>
))}
{to < pages && (
<>
{to < pages - 1 && <span className="armory-pager-dots"></span>}
<button onClick={() => setPage(pages)} type="button">
{pages}
</button>
</>
)}
<button onClick={() => setPage(p + 1)} disabled={p >= pages} type="button" aria-label="Siguiente">
</button>
</div>
)}
</div>
)
}