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,206 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { WowheadRefresh } from './WowheadRefresh'
|
||||
import { wowheadUrl, wowheadData } from '@/lib/wowhead'
|
||||
|
||||
export type QuestStatus = 'rewarded' | 'complete' | 'inprogress' | 'failed'
|
||||
export type QuestEntry = { id: number; name: string; status: QuestStatus }
|
||||
|
||||
// Color + icono por estado (mismo criterio para nombre, texto de estado e icono del filtro).
|
||||
const STATUS: Record<string, { color: string; icon: string }> = {
|
||||
all: { color: '#d4cdbb', icon: '' },
|
||||
inprogress: { color: '#d79602', icon: 'fas fa-scroll' },
|
||||
complete: { color: '#1eff00', icon: 'fas fa-check' },
|
||||
failed: { color: '#ff5555', icon: 'fas fa-times' },
|
||||
rewarded: { color: '#a335ee', icon: 'fas fa-gift' },
|
||||
}
|
||||
|
||||
export function ArmoryQuests({
|
||||
guid,
|
||||
locale,
|
||||
initial,
|
||||
total: initialTotal,
|
||||
grandTotal,
|
||||
pageSize,
|
||||
statuses,
|
||||
badgeLabels,
|
||||
labels,
|
||||
}: {
|
||||
guid: number
|
||||
locale: string
|
||||
initial: QuestEntry[]
|
||||
total: number
|
||||
grandTotal: number
|
||||
pageSize: number
|
||||
statuses: { key: string; label: string; count: number }[]
|
||||
badgeLabels: Record<string, string>
|
||||
labels: { filterId: string; filterName: string; empty: string; quest: string; statusCol: string; title: string; pageOf: string }
|
||||
}) {
|
||||
const [entries, setEntries] = useState<QuestEntry[]>(initial)
|
||||
const [total, setTotal] = useState(initialTotal)
|
||||
const [page, setPage] = useState(1)
|
||||
const [qid, setQid] = useState('')
|
||||
const [qname, setQname] = useState('')
|
||||
const [status, setStatus] = useState('all')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const pages = Math.max(1, Math.ceil(total / pageSize))
|
||||
|
||||
const load = async (p: number, id: string, name: string, st: string) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/armory/quests?guid=${guid}&page=${p}&id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}&status=${st}&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, status)
|
||||
}, 300)
|
||||
return () => clearTimeout(t)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [qid, qname, status])
|
||||
|
||||
const goto = (p: number) => {
|
||||
if (p < 1 || p > pages || p === page || loading) return
|
||||
setPage(p)
|
||||
load(p, qid, qname, status)
|
||||
}
|
||||
const pick = (key: string) => {
|
||||
setOpen(false)
|
||||
setStatus(key)
|
||||
}
|
||||
|
||||
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 current = statuses.find((s) => s.key === status) ?? statuses[0]
|
||||
const optLabel = (s: { key: string; label: string; count: number }) =>
|
||||
s.key === 'all' ? `${s.label}` : `${s.label} (${s.count})`
|
||||
|
||||
return (
|
||||
<>
|
||||
<WowheadRefresh dep={`${page}-${qid}-${qname}-${status}-${entries.length}`} />
|
||||
|
||||
<div className="armory-quest-title">
|
||||
<i className="fas fa-scroll" /> {labels.title} <span className="armory-quest-title-count">({grandTotal})</span>
|
||||
</div>
|
||||
|
||||
<div className="armory-quest-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 className="armory-dropdown">
|
||||
<button className="armory-dropdown-btn" onClick={() => setOpen((o) => !o)} type="button">
|
||||
{current && STATUS[current.key]?.icon ? <i className={STATUS[current.key].icon} style={{ color: STATUS[current.key].color }} /> : null}
|
||||
<span>{current ? optLabel(current) : ''}</span>
|
||||
<i className="fas fa-caret-down armory-dropdown-caret" />
|
||||
</button>
|
||||
{open && (
|
||||
<div className="armory-dropdown-list">
|
||||
{statuses.map((s) => (
|
||||
<button
|
||||
key={s.key}
|
||||
className={`armory-dropdown-item${s.key === status ? ' active' : ''}`}
|
||||
onClick={() => pick(s.key)}
|
||||
type="button"
|
||||
>
|
||||
{STATUS[s.key]?.icon ? <i className={STATUS[s.key].icon} style={{ color: STATUS[s.key].color }} /> : <span className="armory-dropdown-noicon" />}
|
||||
<span>{optLabel(s)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entries.length === 0 ? (
|
||||
<p className="armory-hint">{labels.empty}</p>
|
||||
) : (
|
||||
<table className={`armory-quest-table${loading ? ' loading' : ''}`}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{labels.quest}</th>
|
||||
<th className="armory-quest-th-status">{labels.statusCol}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((q) => {
|
||||
const color = STATUS[q.status]?.color ?? '#d4cdbb'
|
||||
return (
|
||||
<tr key={q.id}>
|
||||
<td>
|
||||
<a
|
||||
className="armory-quest-link"
|
||||
style={{ color }}
|
||||
href={wowheadUrl('quest', q.id, locale)}
|
||||
data-wowhead={wowheadData('quest', q.id, locale)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{q.name}
|
||||
</a>
|
||||
<span className="armory-quest-id"> ({q.id})</span>
|
||||
</td>
|
||||
<td className="armory-quest-td-status" style={{ color }}>
|
||||
{badgeLabels[q.status] ?? q.status}
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user