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:
@@ -9,26 +9,42 @@ import {
|
||||
getCharacterProfessions,
|
||||
getItemIcons,
|
||||
getAchievementPoints,
|
||||
getCharacterTalents,
|
||||
getCharacterTalentGroups,
|
||||
getCharacterSpecs,
|
||||
getSpellIcons,
|
||||
getActivityPage,
|
||||
getAchievementOverview,
|
||||
getRaidProgress,
|
||||
getDungeonProgress,
|
||||
getRealmName,
|
||||
getQuestCount,
|
||||
getQuestPage,
|
||||
getQuestStatusCounts,
|
||||
getCharacterCollection,
|
||||
getReputations,
|
||||
getPvpRank,
|
||||
getCharacterArena,
|
||||
getPvpCurrency,
|
||||
getBattlegroundHistory,
|
||||
} from '@/lib/armory'
|
||||
import { raceName, className, classColor, factionOf, classPower } from '@/lib/wow-data'
|
||||
import { raceName, className, classColor, factionOf, classPower, XP_FOR_LEVEL } from '@/lib/wow-data'
|
||||
import { PageShell } from '@/components/PageShell'
|
||||
import { ArmoryPaperdoll, type SheetItem } from '@/components/ArmoryPaperdoll'
|
||||
import { wowheadIcon } from '@/lib/wowhead'
|
||||
import { ArmoryTalents } from '@/components/ArmoryTalents'
|
||||
import { ArmoryActivity } from '@/components/ArmoryActivity'
|
||||
import { WowheadRefresh } from '@/components/WowheadRefresh'
|
||||
import { ArmoryTalentTab } from '@/components/ArmoryTalentTab'
|
||||
import { ArmoryAchievements } from '@/components/ArmoryAchievements'
|
||||
import { ArmoryRaids } from '@/components/ArmoryRaids'
|
||||
import { ArmoryQuests } from '@/components/ArmoryQuests'
|
||||
import { ArmoryTabs, type ArmoryTab } from '@/components/ArmoryTabs'
|
||||
import { ArmoryReputations } from '@/components/ArmoryReputations'
|
||||
import { ArmoryCollection } from '@/components/ArmoryCollection'
|
||||
import { ArmoryPvpRank } from '@/components/ArmoryPvpRank'
|
||||
import { ArmoryBattlegrounds } from '@/components/ArmoryBattlegrounds'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
const MAX_LEVEL = 80
|
||||
|
||||
export default async function CharacterPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string; guid: string }>
|
||||
}) {
|
||||
export default async function CharacterPage({ params }: { params: Promise<{ locale: string; guid: string }> }) {
|
||||
const { locale, guid } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations('Armory')
|
||||
@@ -37,18 +53,36 @@ export default async function CharacterPage({
|
||||
const character = await getCharacter(id)
|
||||
if (!character) notFound()
|
||||
|
||||
const [equipment, customizations, stats, professions, achievementPoints, talents, specs, activityData] = await Promise.all([
|
||||
getCharacterEquipment(id, locale),
|
||||
getCharacterCustomizations(id),
|
||||
getCharacterStats(id),
|
||||
getCharacterProfessions(id, locale),
|
||||
getAchievementPoints(id),
|
||||
getCharacterTalents(id, character.class, locale),
|
||||
getCharacterSpecs(id, character.class, locale),
|
||||
getActivityPage(id, locale, 1, 10, ''),
|
||||
const [equipment, customizations, stats, professions, achievementPoints, talentGroups, specs, activityData, questCount] =
|
||||
await Promise.all([
|
||||
getCharacterEquipment(id, locale),
|
||||
getCharacterCustomizations(id),
|
||||
getCharacterStats(id),
|
||||
getCharacterProfessions(id, locale),
|
||||
getAchievementPoints(id),
|
||||
getCharacterTalentGroups(id, character.class, locale),
|
||||
getCharacterSpecs(id, character.class, locale),
|
||||
getActivityPage(id, locale, 1, 10, '', ''),
|
||||
getQuestCount(id),
|
||||
])
|
||||
const [questData, mounts, pets, questCounts, reputations, pvpRankData, arena, pvpCurrency, bgHistory, achOverview, raidProgress, dungeonProgress, realmName] = await Promise.all([
|
||||
getQuestPage(id, locale, 1, 15, '', '', 'all'),
|
||||
getCharacterCollection(id, locale, 'mounts'),
|
||||
getCharacterCollection(id, locale, 'pets'),
|
||||
getQuestStatusCounts(id),
|
||||
getReputations(id, locale),
|
||||
getPvpRank(1, 30, ''),
|
||||
getCharacterArena(id),
|
||||
getPvpCurrency(id),
|
||||
getBattlegroundHistory(id),
|
||||
getAchievementOverview(id, factionOf(character.race), locale),
|
||||
getRaidProgress(id, locale),
|
||||
getDungeonProgress(id, locale),
|
||||
getRealmName(),
|
||||
])
|
||||
const qGrand = questCounts.rewarded + questCounts.complete + questCounts.inprogress + questCounts.failed
|
||||
const icons = await getItemIcons(equipment.map((e) => e.entry))
|
||||
const spellIcons = talents ? await getSpellIcons(talents.trees.flatMap((tr) => tr.talents.map((c) => c.spell))) : {}
|
||||
const spellIcons = await getSpellIcons(talentGroups.spellIds)
|
||||
|
||||
const items: SheetItem[] = equipment.map((e) => ({
|
||||
slot: e.slot,
|
||||
@@ -70,6 +104,10 @@ export default async function CharacterPage({
|
||||
const factionLabel = faction === 'alliance' ? t('alliance') : faction === 'horde' ? t('horde') : ''
|
||||
const power = classPower(character.class, locale)
|
||||
|
||||
const xpNext = character.level < MAX_LEVEL ? XP_FOR_LEVEL[character.level] ?? 0 : 0
|
||||
const xpPct =
|
||||
character.level >= MAX_LEVEL ? 100 : xpNext > 0 ? Math.min(100, Math.round((character.xp / xpNext) * 100)) : 0
|
||||
|
||||
const statRows = stats
|
||||
? [
|
||||
{ label: t('strength'), value: `${stats.strength}` },
|
||||
@@ -89,6 +127,431 @@ export default async function CharacterPage({
|
||||
]
|
||||
: []
|
||||
|
||||
// ---- Pestaña Equipación: modelo + spec + stats/profesiones/PvP ----
|
||||
const equipTab = (
|
||||
<>
|
||||
<ArmoryPaperdoll
|
||||
race={character.race}
|
||||
gender={character.gender}
|
||||
customizations={customizations}
|
||||
items={items}
|
||||
health={character.health}
|
||||
power={character.power}
|
||||
powerLabel={power.label}
|
||||
powerColor={power.color}
|
||||
locale={locale}
|
||||
labels={{
|
||||
loading: t('modelLoading'),
|
||||
unavailable: t('modelUnavailable'),
|
||||
health: t('health'),
|
||||
itemLevel: t('itemLevel'),
|
||||
model: t('model3d'),
|
||||
list: t('equipment'),
|
||||
}}
|
||||
/>
|
||||
|
||||
{specs.primary && (
|
||||
<div className="armory-panel armory-spec-panel">
|
||||
<h2 className="armory-section-title">{t('specialization')}</h2>
|
||||
<div className="armory-specs">
|
||||
<div className="armory-spec-box active">
|
||||
<span
|
||||
className="armory-spec-box-icon"
|
||||
style={{ backgroundImage: `url(${wowheadIcon(specs.primary.icon, 'medium')})` }}
|
||||
/>
|
||||
<div className="armory-spec-box-info">
|
||||
<div className="armory-spec-box-name">{specs.primary.name}</div>
|
||||
<div className="armory-spec-box-dist">{specs.primary.dist.join(' / ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`armory-spec-box ${specs.secondary ? '' : 'na'}`}>
|
||||
{specs.secondary ? (
|
||||
<>
|
||||
<span
|
||||
className="armory-spec-box-icon"
|
||||
style={{ backgroundImage: `url(${wowheadIcon(specs.secondary.icon, 'medium')})` }}
|
||||
/>
|
||||
<div className="armory-spec-box-info">
|
||||
<div className="armory-spec-box-name">{specs.secondary.name}</div>
|
||||
<div className="armory-spec-box-dist">{specs.secondary.dist.join(' / ')}</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="armory-spec-box-na">N/A</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="armory-panels">
|
||||
<div className="armory-panel">
|
||||
<h2 className="armory-section-title">{t('stats')}</h2>
|
||||
{stats ? (
|
||||
<div className="armory-stats-grid">
|
||||
{statRows.map((r) => (
|
||||
<div className="armory-stat" key={r.label}>
|
||||
<span className="armory-stat-label">{r.label}</span>
|
||||
<span className="armory-stat-value">{r.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="armory-hint armory-stats-pending">{t('statsPending')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="armory-panel">
|
||||
<h2 className="armory-section-title">{t('professions')}</h2>
|
||||
{professions.length ? (
|
||||
<div className="armory-prof-groups">
|
||||
{(['primary', 'secondary'] as const).map((kind) => {
|
||||
const list = professions.filter((p) => p.kind === kind)
|
||||
if (!list.length) return null
|
||||
return (
|
||||
<div className="armory-prof-group" key={kind}>
|
||||
<h3 className="armory-prof-grouptitle">
|
||||
{kind === 'primary' ? t('primaryProfs') : t('secondaryProfs')}
|
||||
</h3>
|
||||
{list.map((p) => (
|
||||
<div className="armory-prof-row" key={p.skill}>
|
||||
<span
|
||||
className="armory-prof-icon"
|
||||
style={{ backgroundImage: `url(${wowheadIcon(p.icon, 'small')})` }}
|
||||
/>
|
||||
<div className="armory-prof-body">
|
||||
<div className="armory-prof-head">
|
||||
<span className="armory-prof-name">
|
||||
{p.name}
|
||||
{p.rank ? <span className="armory-prof-rank"> · {p.rank}</span> : null}
|
||||
</span>
|
||||
<span className="armory-prof-value">
|
||||
{p.value}
|
||||
<span className="armory-prof-max"> / {p.max}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="armory-prof-bar">
|
||||
<span style={{ width: `${Math.round((p.value / Math.max(1, p.max)) * 100)}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<p className="armory-hint">{t('noProfessions')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
// ---- Pestaña PvP (sub-pestañas: Estadísticas + Rank mundo) ----
|
||||
const pvpStatsSub = (
|
||||
<div className="armory-panel">
|
||||
<div className="armory-pvp-tiles">
|
||||
<div className="armory-pvp-tile">
|
||||
<span className="armory-pvp-tile-num" style={{ color: '#ff5555' }}>
|
||||
{character.totalKills.toLocaleString(locale)}
|
||||
</span>
|
||||
<span className="armory-pvp-tile-lbl">{t('totalKills')}</span>
|
||||
</div>
|
||||
<div className="armory-pvp-tile">
|
||||
<span className="armory-pvp-tile-num" style={{ color: '#ff8888' }}>
|
||||
{character.todayKills.toLocaleString(locale)}
|
||||
</span>
|
||||
<span className="armory-pvp-tile-lbl">{t('todayKills')}</span>
|
||||
</div>
|
||||
<div className="armory-pvp-tile">
|
||||
<span className="armory-pvp-tile-num" style={{ color: '#e08a3a' }}>
|
||||
{character.yesterdayKills.toLocaleString(locale)}
|
||||
</span>
|
||||
<span className="armory-pvp-tile-lbl">{t('yesterdayKills')}</span>
|
||||
</div>
|
||||
<div className="armory-pvp-tile">
|
||||
<span className="armory-pvp-tile-num" style={{ color: '#ffd100' }}>
|
||||
{pvpCurrency.honor.toLocaleString(locale)}
|
||||
</span>
|
||||
<span className="armory-pvp-tile-lbl">{t('honorPoints')}</span>
|
||||
</div>
|
||||
<div className="armory-pvp-tile">
|
||||
<span className="armory-pvp-tile-num" style={{ color: '#9b59b6' }}>
|
||||
{pvpCurrency.arena.toLocaleString(locale)}
|
||||
</span>
|
||||
<span className="armory-pvp-tile-lbl">{t('arenaPoints')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const arenaSub = (
|
||||
<div className="armory-panel">
|
||||
<div className="armory-arena">
|
||||
{arena.map((b) => {
|
||||
const losses = Math.max(0, b.seasonGames - b.seasonWins)
|
||||
const winRate = b.seasonGames ? Math.round((b.seasonWins / b.seasonGames) * 100) : 0
|
||||
return (
|
||||
<div className="armory-arena-card" key={b.slot}>
|
||||
<div className="armory-arena-bracket">{b.bracket}</div>
|
||||
<span className={`armory-arena-mode ${b.seasonGames > 0 ? 'rated' : 'none'}`}>
|
||||
{b.seasonGames > 0 ? t('arenaRated') : t('arenaUnrated')}
|
||||
</span>
|
||||
<div className="armory-arena-rating">{b.personalRating}</div>
|
||||
<div className="armory-arena-rating-lbl">{t('arenaRating')}</div>
|
||||
{b.seasonGames > 0 ? (
|
||||
<div className="armory-arena-rows">
|
||||
<div className="armory-arena-row">
|
||||
<span>{t('arenaSeason')}</span>
|
||||
<span>
|
||||
<b className="armory-arena-w">{b.seasonWins}</b> / <b className="armory-arena-l">{losses}</b> ·{' '}
|
||||
{winRate}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="armory-arena-row">
|
||||
<span>{t('arenaWeek')}</span>
|
||||
<span>
|
||||
{b.weekWins} / {Math.max(0, b.weekGames - b.weekWins)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="armory-arena-row">
|
||||
<span>MMR</span>
|
||||
<span>{b.mmr}</span>
|
||||
</div>
|
||||
{b.rank > 0 ? (
|
||||
<div className="armory-arena-row">
|
||||
<span>{t('arenaRank')}</span>
|
||||
<span>#{b.rank}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="armory-arena-nogames">{t('arenaNoGames')}</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const pvpTab = (
|
||||
<ArmoryTabs
|
||||
tabs={[
|
||||
{ key: 'pvpstats', label: t('pvpStats'), icon: 'fas fa-skull', content: pvpStatsSub },
|
||||
{ key: 'arena', label: t('arena'), icon: 'fas fa-shield-halved', content: arenaSub },
|
||||
{
|
||||
key: 'battlegrounds',
|
||||
label: t('battlegrounds'),
|
||||
icon: 'fas fa-flag-checkered',
|
||||
content: (
|
||||
<ArmoryBattlegrounds
|
||||
locale={locale}
|
||||
rows={bgHistory.rows}
|
||||
matches={bgHistory.matches}
|
||||
wins={bgHistory.wins}
|
||||
losses={bgHistory.losses}
|
||||
winRate={bgHistory.winRate}
|
||||
labels={{
|
||||
history: t('bgHistory'),
|
||||
desc: t('bgDesc'),
|
||||
matches: t('bgMatches'),
|
||||
wins: t('bgWins'),
|
||||
losses: t('bgLosses'),
|
||||
winRate: t('bgWinRate'),
|
||||
result: t('bgResult'),
|
||||
win: t('bgWin'),
|
||||
loss: t('bgLoss'),
|
||||
kb: t('bgKb'),
|
||||
hk: t('bgHk'),
|
||||
deaths: t('bgDeaths'),
|
||||
damage: t('bgDamage'),
|
||||
healing: t('bgHealing'),
|
||||
honor: t('bgHonor'),
|
||||
date: t('bgDate'),
|
||||
none: t('bgNone'),
|
||||
battleground: t('battlegrounds'),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'pvprank',
|
||||
label: t('pvpRank'),
|
||||
icon: 'fas fa-trophy',
|
||||
content: (
|
||||
<ArmoryPvpRank
|
||||
locale={locale}
|
||||
initial={pvpRankData.entries}
|
||||
total={pvpRankData.total}
|
||||
pageSize={30}
|
||||
labels={{
|
||||
title: t('pvpRankTitle'),
|
||||
desc: t('pvpRankDesc'),
|
||||
search: t('searchPlayer'),
|
||||
empty: t('noPlayers'),
|
||||
character: t('rankPersonaje'),
|
||||
kills: t('rankKills'),
|
||||
playedTime: t('playedTime'),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
const raidsTab = (
|
||||
<ArmoryRaids
|
||||
locale={locale}
|
||||
raids={raidProgress}
|
||||
labels={{
|
||||
bosses: t('raidBosses'),
|
||||
empty: t('raidNoData'),
|
||||
classic: t('expClassic'),
|
||||
tbc: t('expTbc'),
|
||||
wotlk: t('expWotlk'),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const dungeonsTab = (
|
||||
<ArmoryRaids
|
||||
locale={locale}
|
||||
raids={dungeonProgress}
|
||||
labels={{
|
||||
bosses: t('raidBosses'),
|
||||
empty: t('raidNoData'),
|
||||
classic: t('expClassic'),
|
||||
tbc: t('expTbc'),
|
||||
wotlk: t('expWotlk'),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
// ---- Pestaña Talentos (toggle Primaria/Secundaria + árbol + glifos) ----
|
||||
const talentsTab = talentGroups.groups.length ? (
|
||||
<ArmoryTalentTab
|
||||
groups={talentGroups.groups}
|
||||
activeIndex={talentGroups.activeIndex}
|
||||
spellIcons={spellIcons}
|
||||
locale={locale}
|
||||
labels={{
|
||||
distribution: t('distribution'),
|
||||
glyphs: t('glyphs'),
|
||||
majorGlyphs: t('majorGlyphs'),
|
||||
minorGlyphs: t('minorGlyphs'),
|
||||
primary: t('primary'),
|
||||
secondary: t('secondary'),
|
||||
activeSpec: t('activeSpec'),
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="armory-panel">
|
||||
<p className="armory-hint">{t('noTalents')}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
// ---- Pestaña Logros ----
|
||||
const logrosTab = (
|
||||
<ArmoryAchievements
|
||||
locale={locale}
|
||||
guid={id}
|
||||
categories={achOverview.categories}
|
||||
earnedPoints={achOverview.earnedPoints}
|
||||
totalCompleted={achOverview.totalCompleted}
|
||||
labels={{
|
||||
summary: t('achSummaryLine'),
|
||||
back: t('achBack'),
|
||||
reward: t('achReward'),
|
||||
empty: t('achEmptyCat'),
|
||||
of: t('achOf'),
|
||||
feats: t('achFeats'),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
const questsTab = (
|
||||
<div className="armory-panel armory-quests-panel">
|
||||
<ArmoryQuests
|
||||
guid={id}
|
||||
locale={locale}
|
||||
initial={questData.entries}
|
||||
total={questData.total}
|
||||
grandTotal={qGrand}
|
||||
pageSize={15}
|
||||
statuses={[
|
||||
{ key: 'all', label: t('allStatuses'), count: qGrand },
|
||||
{ key: 'inprogress', label: t('qsInProgress'), count: questCounts.inprogress },
|
||||
{ key: 'complete', label: t('qsComplete'), count: questCounts.complete },
|
||||
{ key: 'failed', label: t('qsFailed'), count: questCounts.failed },
|
||||
{ key: 'rewarded', label: t('qsRewarded'), count: questCounts.rewarded },
|
||||
]}
|
||||
badgeLabels={{
|
||||
rewarded: t('bRewarded'),
|
||||
complete: t('bComplete'),
|
||||
inprogress: t('bInProgress'),
|
||||
failed: t('bFailed'),
|
||||
}}
|
||||
labels={{
|
||||
filterId: t('filterId'),
|
||||
filterName: t('filterName'),
|
||||
empty: t('noQuests'),
|
||||
quest: t('quest'),
|
||||
statusCol: t('statusCol'),
|
||||
title: t('quests'),
|
||||
pageOf: '',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
const mountsTab = (
|
||||
<ArmoryCollection
|
||||
items={mounts}
|
||||
locale={locale}
|
||||
pageSize={30}
|
||||
labels={{ title: t('mountsCollected', { n: mounts.length }), search: t('collSearch'), empty: t('noMounts') }}
|
||||
/>
|
||||
)
|
||||
const petsTab = (
|
||||
<ArmoryCollection
|
||||
items={pets}
|
||||
locale={locale}
|
||||
pageSize={30}
|
||||
labels={{ title: t('petsCollected', { n: pets.length }), search: t('collSearch'), empty: t('noPets') }}
|
||||
/>
|
||||
)
|
||||
|
||||
const repsTab = (
|
||||
<ArmoryReputations
|
||||
reputations={reputations}
|
||||
pageSize={30}
|
||||
labels={{ title: t('reputations'), search: t('repSearch'), empty: t('noReps') }}
|
||||
/>
|
||||
)
|
||||
|
||||
const tabs: ArmoryTab[] = [
|
||||
{ key: 'equip', label: t('tabEquip'), icon: 'fas fa-shield-alt', content: equipTab },
|
||||
{
|
||||
key: 'talents',
|
||||
label: t('talents'),
|
||||
icon: 'fas fa-tree',
|
||||
count: talentGroups.groups[talentGroups.activeIndex]?.total,
|
||||
content: talentsTab,
|
||||
},
|
||||
{ key: 'achievements', label: t('achievements'), icon: 'fas fa-trophy', count: activityData.total, content: logrosTab },
|
||||
{ key: 'raids', label: t('raids'), icon: 'fas fa-dragon', content: raidsTab },
|
||||
{ key: 'dungeons', label: t('dungeons'), icon: 'fas fa-dungeon', content: dungeonsTab },
|
||||
{ key: 'quests', label: t('quests'), icon: 'fas fa-scroll', count: questCount, content: questsTab },
|
||||
{ key: 'reputations', label: t('reputations'), icon: 'fas fa-handshake', count: reputations.length, content: repsTab },
|
||||
{ key: 'pvp', label: t('pvp'), icon: 'fas fa-khanda', content: pvpTab },
|
||||
{ key: 'mounts', label: t('mounts'), icon: 'fas fa-horse', count: mounts.length, content: mountsTab },
|
||||
{ key: 'pets', label: t('pets'), icon: 'fas fa-paw', count: pets.length, content: petsTab },
|
||||
]
|
||||
|
||||
return (
|
||||
<PageShell title={character.name}>
|
||||
<div className="main-wide">
|
||||
@@ -121,134 +584,26 @@ export default async function CharacterPage({
|
||||
<{character.guildName}>
|
||||
</Link>
|
||||
)}
|
||||
{realmName && <span className="armory-realm">{realmName}</span>}
|
||||
<span className="armory-achpoints" title={t('achievementPoints')}>
|
||||
<i className="fas fa-star" /> {achievementPoints.toLocaleString(locale)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArmoryPaperdoll
|
||||
race={character.race}
|
||||
gender={character.gender}
|
||||
customizations={customizations}
|
||||
items={items}
|
||||
health={character.health}
|
||||
power={character.power}
|
||||
powerLabel={power.label}
|
||||
powerColor={power.color}
|
||||
locale={locale}
|
||||
labels={{
|
||||
loading: t('modelLoading'),
|
||||
unavailable: t('modelUnavailable'),
|
||||
health: t('health'),
|
||||
itemLevel: t('itemLevel'),
|
||||
model: t('model3d'),
|
||||
list: t('equipment'),
|
||||
}}
|
||||
/>
|
||||
|
||||
{specs.primary && (
|
||||
<div className="armory-panel armory-spec-panel">
|
||||
<h2 className="armory-section-title">{t('specialization')}</h2>
|
||||
<div className="armory-specs">
|
||||
<div className="armory-spec-box active">
|
||||
<span
|
||||
className="armory-spec-box-icon"
|
||||
style={{ backgroundImage: `url(${wowheadIcon(specs.primary.icon, 'medium')})` }}
|
||||
/>
|
||||
<div className="armory-spec-box-info">
|
||||
<div className="armory-spec-box-name">{specs.primary.name}</div>
|
||||
<div className="armory-spec-box-dist">{specs.primary.dist.join(' / ')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`armory-spec-box ${specs.secondary ? '' : 'na'}`}>
|
||||
{specs.secondary ? (
|
||||
<>
|
||||
<span
|
||||
className="armory-spec-box-icon"
|
||||
style={{ backgroundImage: `url(${wowheadIcon(specs.secondary.icon, 'medium')})` }}
|
||||
/>
|
||||
<div className="armory-spec-box-info">
|
||||
<div className="armory-spec-box-name">{specs.secondary.name}</div>
|
||||
<div className="armory-spec-box-dist">{specs.secondary.dist.join(' / ')}</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className="armory-spec-box-na">N/A</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="armory-xpbar">
|
||||
<div className="armory-xpbar-head">
|
||||
<span className="armory-xpbar-label">{t('level')}</span>
|
||||
<span className="armory-xpbar-value">
|
||||
{character.level >= MAX_LEVEL ? `${character.level} (MAX)` : character.level}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{talents && talents.total > 0 && (
|
||||
<div className="armory-panel armory-talents-panel">
|
||||
<h2 className="armory-section-title">{t('talents')}</h2>
|
||||
<WowheadRefresh dep={`talents-${id}`} />
|
||||
<ArmoryTalents
|
||||
trees={talents.trees}
|
||||
spec={talents.spec}
|
||||
total={talents.total}
|
||||
maxPoints={Math.max(0, character.level - 9)}
|
||||
locale={locale}
|
||||
spellIcons={spellIcons}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activityData.entries.length > 0 && (
|
||||
<div className="armory-panel armory-activity-panel">
|
||||
<h2 className="armory-section-title">{t('recentActivity')}</h2>
|
||||
<ArmoryActivity
|
||||
guid={id}
|
||||
locale={locale}
|
||||
initial={activityData.entries}
|
||||
total={activityData.total}
|
||||
pageSize={10}
|
||||
labels={{ earned: t('earned'), search: t('achSearch'), empty: t('noAchievements') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="armory-panels">
|
||||
<div className="armory-panel">
|
||||
<h2 className="armory-section-title">{t('stats')}</h2>
|
||||
{stats ? (
|
||||
<div className="armory-stats-grid">
|
||||
{statRows.map((r) => (
|
||||
<div className="armory-stat" key={r.label}>
|
||||
<span className="armory-stat-label">{r.label}</span>
|
||||
<span className="armory-stat-value">{r.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="armory-hint armory-stats-pending">{t('statsPending')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="armory-panel">
|
||||
<h2 className="armory-section-title">{t('professions')}</h2>
|
||||
{professions.length ? (
|
||||
<div className="armory-prof-list">
|
||||
{professions.map((p) => (
|
||||
<div className="armory-prof" key={p.skill}>
|
||||
<span className="armory-prof-nameblock">
|
||||
<span className="armory-prof-name">{p.name}</span>
|
||||
{p.rank ? <span className="armory-prof-rank">{p.rank}</span> : null}
|
||||
</span>
|
||||
<span className="armory-prof-value">
|
||||
{p.value}
|
||||
<span className="armory-prof-max"> / {p.max}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="armory-hint">{t('noProfessions')}</p>
|
||||
)}
|
||||
<div className="armory-xpbar-track">
|
||||
<span style={{ width: `${xpPct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ArmoryTabs tabs={tabs} />
|
||||
</div>
|
||||
</div>
|
||||
</PageShell>
|
||||
|
||||
Reference in New Issue
Block a user