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
+103
View File
@@ -0,0 +1,103 @@
'use client'
import { useState } from 'react'
import { WowheadRefresh } from './WowheadRefresh'
import { wowheadIcon, wowheadUrl, wowheadData } from '@/lib/wowhead'
export type CollectionEntry = { id: number; name: string; icon: string }
/** Colección (monturas/mascotas) con búsqueda en tiempo real y paginación en cliente. */
export function ArmoryCollection({
items,
locale,
pageSize,
labels,
}: {
items: CollectionEntry[]
locale: string
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 ? items.filter((i) => i.name.toLowerCase().includes(query)) : items
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">
<WowheadRefresh dep={`${p}-${q}-${pageItems.length}`} />
<div className="armory-collection-head">{labels.title}</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-collection">
{pageItems.map((c) => (
<a
className="armory-coll-card"
key={c.id}
href={wowheadUrl('spell', c.id, locale)}
data-wowhead={wowheadData('spell', c.id, locale)}
data-wh-icon-size="false"
data-wh-rename-link="false"
target="_blank"
rel="noopener noreferrer"
>
<span className="armory-coll-icon" style={{ backgroundImage: `url(${wowheadIcon(c.icon, 'small')})` }} />
<span className="armory-coll-name">{c.name}</span>
</a>
))}
</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>
)
}