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:
@@ -0,0 +1,229 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { wowheadIcon, wowheadUrl, wowheadData } from '@/lib/wowhead'
|
||||
import { WowheadRefresh } from './WowheadRefresh'
|
||||
|
||||
export type AchCat = {
|
||||
id: number
|
||||
name: string
|
||||
total: number
|
||||
completed: number
|
||||
totalPoints: number
|
||||
earnedPoints: number
|
||||
pct: number
|
||||
isFeats: boolean
|
||||
}
|
||||
export type AchDetailItem = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
reward: string
|
||||
icon: string
|
||||
points: number
|
||||
date: number
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
function ringColor(pct: number) {
|
||||
if (pct >= 90) return '#4ade5f'
|
||||
if (pct >= 50) return '#e0902f'
|
||||
return '#d97b28'
|
||||
}
|
||||
|
||||
export function ArmoryAchievements({
|
||||
locale,
|
||||
guid,
|
||||
categories,
|
||||
earnedPoints,
|
||||
totalCompleted,
|
||||
apiPath = '/api/armory/achievements',
|
||||
idKey = 'guid',
|
||||
labels,
|
||||
}: {
|
||||
locale: string
|
||||
guid: number
|
||||
categories: AchCat[]
|
||||
earnedPoints: number
|
||||
totalCompleted: number
|
||||
apiPath?: string
|
||||
idKey?: string
|
||||
labels: {
|
||||
summary: string
|
||||
back: string
|
||||
reward: string
|
||||
empty: string
|
||||
of: string
|
||||
feats: string
|
||||
}
|
||||
}) {
|
||||
const [cat, setCat] = useState<AchCat | null>(null)
|
||||
const [entries, setEntries] = useState<AchDetailItem[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const pages = Math.max(1, Math.ceil(total / PAGE_SIZE))
|
||||
|
||||
const open = async (c: AchCat, p = 1) => {
|
||||
setCat(c)
|
||||
setPage(p)
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`${apiPath}?${idKey}=${guid}&cat=${c.id}&page=${p}&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 goto = (p: number) => {
|
||||
if (!cat || p < 1 || p > pages || p === page || loading) return
|
||||
open(cat, p)
|
||||
}
|
||||
|
||||
const fmtDate = (unix: number) => {
|
||||
if (!unix) return ''
|
||||
const d = new Date(unix * 1000)
|
||||
if (Number.isNaN(d.getTime())) return ''
|
||||
return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
}
|
||||
|
||||
// ---- Vista detalle de una categoría ----
|
||||
if (cat) {
|
||||
const from = Math.max(1, page - 2)
|
||||
const to = Math.min(pages, page + 2)
|
||||
const nums: number[] = []
|
||||
for (let p = from; p <= to; p++) nums.push(p)
|
||||
return (
|
||||
<div className="armory-panel armory-ach2">
|
||||
<WowheadRefresh dep={`${cat.id}:${page}:${entries.length}`} />
|
||||
<div className="armory-ach2-head">
|
||||
<button type="button" className="armory-ach2-back" onClick={() => setCat(null)}>
|
||||
<i className="fas fa-arrow-left" /> {labels.back}
|
||||
</button>
|
||||
<div className="armory-ach2-title">
|
||||
{cat.name}
|
||||
<span className="armory-ach2-sub">
|
||||
{cat.completed} {labels.of} {cat.total}
|
||||
{cat.isFeats ? '' : ` · ${cat.earnedPoints} / ${cat.totalPoints}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entries.length === 0 && !loading ? (
|
||||
<p className="armory-hint">{labels.empty}</p>
|
||||
) : (
|
||||
<ul className={`armory-ach2-list${loading ? ' loading' : ''}`}>
|
||||
{entries.map((a) => (
|
||||
<li className="armory-ach2-item" key={a.id}>
|
||||
<span className="armory-ach2-icon">
|
||||
<img src={wowheadIcon(a.icon, 'large')} alt="" loading="lazy" />
|
||||
</span>
|
||||
<div className="armory-ach2-body">
|
||||
<a
|
||||
className="armory-ach2-name"
|
||||
href={wowheadUrl('achievement', a.id, locale)}
|
||||
data-wowhead={wowheadData('achievement', a.id, locale)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{a.name}
|
||||
</a>
|
||||
{a.description ? <p className="armory-ach2-desc">{a.description}</p> : null}
|
||||
{a.reward ? (
|
||||
<p className="armory-ach2-reward">
|
||||
<i className="fas fa-gift" /> {labels.reward}: {a.reward}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="armory-ach2-meta">
|
||||
{a.points > 0 ? (
|
||||
<span className="armory-ach2-pts">
|
||||
<i className="fas fa-shield" /> {a.points}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="armory-ach2-date">{fmtDate(a.date)}</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- Vista rejilla de categorías ----
|
||||
const totalAch = categories.reduce((s, c) => s + c.total, 0)
|
||||
const summary = labels.summary
|
||||
.replace('{done}', String(totalCompleted))
|
||||
.replace('{total}', String(totalAch))
|
||||
.replace('{points}', String(earnedPoints))
|
||||
|
||||
return (
|
||||
<div className="armory-panel armory-ach2">
|
||||
<p className="armory-ach2-summary">{summary}</p>
|
||||
<div className="armory-ach-grid">
|
||||
{categories.map((c) => {
|
||||
const col = ringColor(c.pct)
|
||||
return (
|
||||
<button type="button" className="armory-ach-card" key={c.id} onClick={() => open(c)}>
|
||||
{c.isFeats ? (
|
||||
<div className="armory-ach-ring feats">
|
||||
<i className="fas fa-award" />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="armory-ach-ring"
|
||||
style={{ background: `conic-gradient(${col} ${c.pct * 3.6}deg, #241f19 0deg)` }}
|
||||
>
|
||||
<span className="armory-ach-ring-pct">{c.pct}%</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="armory-ach-cat-name">{c.name}</span>
|
||||
<span className="armory-ach-cat-pts">
|
||||
<i className="fas fa-shield" /> {c.isFeats ? c.completed : c.earnedPoints}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user