Polish frontend hero consistency and annual ranking layout

This commit is contained in:
devRaGonSa
2026-06-09 14:30:33 +02:00
parent 5355f84c5d
commit 88964d6126
11 changed files with 392 additions and 116 deletions

View File

@@ -0,0 +1,158 @@
---
id: TASK-206
title: Public frontend visual consistency and annual ranking layout
status: done
type: frontend
team: Frontend Senior
supporting_teams:
- Experto en interfaz
roadmap_item: foundation
priority: medium
---
# TASK-206 - Public frontend visual consistency and annual ranking layout
## Goal
Corregir la segunda pasada visual del frontend publico para unificar definitivamente las cabeceras de `index`, `historico`, `stats` y `ranking`, mejorar la legibilidad del ranking anual y ajustar la presencia visual del logo `BxB` en la seccion de clanes.
## Context
Tras `TASK-203` y la task local no commiteada `TASK-204`, el frontend publico sigue mostrando diferencias visibles entre heroes, etiquetas redundantes dentro de cabecera y un bloque anual demasiado compacto en `stats`. Esta tarea se limita a `frontend/` y a su documentacion de task: no toca backend, endpoints, logica de datos, scripts backend, snapshots, Elo/MMR pausado, Comunidad Hispana #03 ni assets/SVGs de armas.
Preserve the current product identity: Spanish-speaking HLL Vietnam community, military/Vietnam/tactical/sober visual direction and controlled repository evolution.
## Steps
1. Confirmar el estado actual del frontend publico y de la task previa local `TASK-204`.
2. Unificar el patron visual del hero tomando `index` como referencia principal y `historico` como referencia secundaria.
3. Eliminar chips o labels de pagina redundantes dentro del hero manteniendo la navegacion superior comun.
4. Rehacer solo la presentacion frontend del Top 20 anual en `stats` para separar claramente posicion, jugador y metricas.
5. Ajustar la presentacion del logo `BxB` en la seccion de clanes sin reemplazar el asset.
6. Validar por inspeccion y documentar outcome, alcance y exclusiones.
## Files to Read First
- `AGENTS.md`
- `ai/repo-context.md`
- `ai/architecture-index.md`
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/stats.html`
- `frontend/ranking.html`
- `frontend/assets/css/styles.css`
- `frontend/assets/js/stats.js`
- `frontend/assets/js/ranking.js`
- `ai/tasks/done/TASK-203-public-pages-navigation-and-copy-polish.md`
## Expected Files to Modify
- `ai/tasks/in-progress/TASK-206-public-frontend-visual-consistency-and-annual-ranking-layout.md`
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/stats.html`
- `frontend/ranking.html`
- `frontend/assets/css/styles.css`
- `frontend/assets/css/hero-header-compact.css`
- `frontend/assets/css/historico.css`
- `frontend/assets/js/stats.js`
- `frontend/assets/js/main.js`
## Constraints
- Keep the change minimal.
- Preserve HLL Vietnam project identity.
- Do not introduce unnecessary frameworks or dependencies.
- Do not implement backend functionality unless the task explicitly requires it.
- Do not expand Elo/MMR, historical workers or RCON server #03 handling unless the task explicitly requires it.
- Do not overwrite repository-specific context with generic platform template text.
- No ejecutar `ai-platform run`.
- No modificar backend, endpoints, logica de datos, generacion de snapshots ni scripts backend.
- No tocar `ai/system-metrics.md`.
- No tocar `frontend/assets/img/weapons/`, assets/SVGs de armas ni copias relacionadas.
- No reemplazar ni editar fisicamente imagenes de clanes.
- No reactivar Elo/MMR ni reintroducir Comunidad Hispana #03.
## Validation
Before completing the task ensure:
- `index.html`, `historico.html`, `stats.html` y `ranking.html` mantienen navegacion superior comun con `Inicio`, `Historico`, `Estadisticas` y `Ranking`
- los cuatro hero muestran composicion coherente con logo a la izquierda y bloque textual a la derecha
- desaparecen del hero `Historico propio`, `Seccion Stats`, `Seccion Ranking` y cualquier chip equivalente de pagina
- el logo de comunidad no cambia de tamano de forma notable entre `index`, `historico`, `stats` y `ranking`
- el copy visible mantiene `Busca un jugador por nombre o ID y revisa sus resultados semanales, mensuales y ranking anual.`
- el copy visible mantiene `Consulta los lideres publicos de la comunidad, cambia de periodo y servidor sin salir de la pagina y revisa quien destaca en cada ventana.`
- el Top 20 anual se muestra en columnas o grid legible y el `player_id` no queda pegado al nombre como dato principal
- el logo `BxB` gana presencia visual sin romper responsive
- `git diff --name-only` matches the expected scope
- no unrelated files were modified
- integration tests are run when relevant and configured
## Outcome
Archivos modificados:
- `frontend/index.html`
- `frontend/historico.html`
- `frontend/stats.html`
- `frontend/ranking.html`
- `frontend/assets/css/hero-header-compact.css`
- `frontend/assets/css/historico.css`
- `frontend/assets/css/styles.css`
- `frontend/assets/js/main.js`
- `frontend/assets/js/stats.js`
- `ai/tasks/in-progress/TASK-206-public-frontend-visual-consistency-and-annual-ranking-layout.md`
Hero unificado:
- Se marco `index`, `historico`, `stats` y `ranking` como `hero--public-page` para dejar explicita la variante publica comun.
- `hero-header-compact.css` quedo restringido a ese patron comun y `historico.css` se alineo con la misma escala de padding, gap, logo y copy.
- `historico` dejo de usar una separacion mas amplia y un logo base mas pequeno que el resto; ahora mantiene una proporcion equivalente a `index`, `stats` y `ranking`.
- `stats` y `ranking` conservan logo a la izquierda y bloque textual a la derecha con la misma composicion visual base del hero publico.
Chips eliminados del hero:
- `Historico propio`
- `Seccion Stats`
- `Seccion Ranking`
Ranking anual:
- El Top 20 anual de `stats` paso de lista compacta apilada a una tabla legible con columnas `Posicion`, `Jugador`, `Valor / Kills`, `Partidas`, `Muertes`, `Teamkills` y `K/D`.
- El `player_id` se mantiene como dato secundario visual en una segunda linea bajo el nombre, en vez de aparecer pegado al jugador.
- El bloque superior del ranking anual se suavizo con copy menos tecnico y metadatos presentados en tarjetas (`Servidor`, `Año`, `Lectura`, `Partidas base`, `Actualizado`).
- No se tocaron endpoint ni contrato API; solo se cambio la presentacion generada por `stats.js` y su CSS asociado.
Clanes / BxB:
- `BxB` recibio una variante visual especifica de tarjeta y logo (`clan-card--bxb`, `clan-card__logo--bxb`) para ganar presencia sin reemplazar la imagen ni alterar el asset original.
- El ajuste se hizo en layout y CSS, manteniendo la coherencia general de la grilla de clanes y sin romper responsive.
Textos y navegacion:
- La navegacion superior comun queda en `Inicio`, `Historico`, `Estadisticas` y `Ranking`.
- Se mantuvo el copy principal pedido en `stats` y `ranking`.
- Se corrigieron textos visibles de apoyo en `main.js` como `Historico`, `Region`, `Informacion`, `Ultimo snapshot` y `Proximamente`.
Validaciones ejecutadas:
- `git status --short --untracked-files=all` ejecutado antes de modificar.
- Confirmacion de que `TASK-204-align-public-page-heroes-and-navigation-labels.md` existe en disco pero sigue `untracked`; no estaba commiteada.
- Confirmacion previa de cambios no relacionados en `ai/system-metrics.md` y en `frontend/assets/img/weapons/` / SVGs de armas; no se tocaron.
- Lectura previa de `AGENTS.md`, `ai/repo-context.md`, `ai/architecture-index.md`, task previa relevante, HTML/CSS/JS del frontend publico y seccion de clanes.
- Revision de `git diff --name-only` para comprobar alcance restringido a frontend publico y task.
- Inspeccion visual con capturas headless de Edge sobre `frontend/index.html`, `frontend/historico.html`, `frontend/stats.html` y `frontend/ranking.html` para verificar navegacion y coherencia del hero.
- Validacion estructural de la nueva tabla anual por inspeccion del HTML/JS/CSS renderizado. La vista con datos reales no pudo comprobarse visualmente porque el backend no estaba disponible en la validacion local.
Confirmaciones de alcance:
- No se tocaron `backend/`, endpoints, logica de datos, snapshots ni scripts backend.
- No se tocaron `ai/system-metrics.md`, `frontend/assets/img/weapons/` ni los SVGs de armas con cambios previos.
- No se reactivo Elo/MMR ni se reintrodujo Comunidad Hispana #03.
## Change Budget
- Prefer fewer than 5 modified files.
- Prefer changes under 200 lines when feasible.
- Split the work into follow-up tasks if limits are exceeded.

View File

@@ -1,10 +1,10 @@
@media (min-width: 1121px) { @media (min-width: 1121px) {
.hero:not(.historical-hero) .hero__content { .hero--public-page:not(.historical-hero) .hero__content {
padding-block: 54px 60px; padding-block: 54px 60px;
padding-inline: clamp(64px, 5.5vw, 104px); padding-inline: clamp(64px, 5.5vw, 104px);
} }
.hero:not(.historical-hero) .hero__brand { .hero--public-page:not(.historical-hero) .hero__brand {
width: 100%; width: 100%;
max-width: 1420px; max-width: 1420px;
margin: 0 auto; margin: 0 auto;
@@ -14,7 +14,7 @@
align-items: center; align-items: center;
} }
.hero:not(.historical-hero) .logo-frame { .hero--public-page:not(.historical-hero) .logo-frame {
grid-column: 1; grid-column: 1;
justify-self: start; justify-self: start;
width: min(470px, 100%); width: min(470px, 100%);
@@ -22,11 +22,11 @@
padding: 30px 34px; padding: 30px 34px;
} }
.hero:not(.historical-hero) .logo-frame__image { .hero--public-page:not(.historical-hero) .logo-frame__image {
max-height: 340px; max-height: 340px;
} }
.hero:not(.historical-hero) .hero__copy { .hero--public-page:not(.historical-hero) .hero__copy {
grid-column: 3; grid-column: 3;
justify-self: end; justify-self: end;
width: 100%; width: 100%;
@@ -37,41 +37,41 @@
transform: none; transform: none;
} }
.hero:not(.historical-hero) .hero__title { .hero--public-page:not(.historical-hero) .hero__title {
max-width: none; max-width: none;
font-size: clamp(4.2rem, 5.2vw, 6.35rem); font-size: clamp(4.2rem, 5.2vw, 6.35rem);
line-height: 1.02; line-height: 1.02;
} }
.hero:not(.historical-hero) .hero__title-accent { .hero--public-page:not(.historical-hero) .hero__title-accent {
display: inline; display: inline;
} }
.hero:not(.historical-hero) .hero__text { .hero--public-page:not(.historical-hero) .hero__text {
max-width: 68ch; max-width: 68ch;
} }
} }
@media (min-width: 1121px) and (max-width: 1380px) { @media (min-width: 1121px) and (max-width: 1380px) {
.hero:not(.historical-hero) .hero__content { .hero--public-page:not(.historical-hero) .hero__content {
padding-inline: clamp(44px, 4vw, 64px); padding-inline: clamp(44px, 4vw, 64px);
} }
.hero:not(.historical-hero) .hero__brand { .hero--public-page:not(.historical-hero) .hero__brand {
grid-template-columns: minmax(320px, 390px) minmax(28px, 1fr) minmax(560px, 720px); grid-template-columns: minmax(320px, 390px) minmax(28px, 1fr) minmax(560px, 720px);
column-gap: clamp(20px, 2.5vw, 40px); column-gap: clamp(20px, 2.5vw, 40px);
} }
.hero:not(.historical-hero) .logo-frame { .hero--public-page:not(.historical-hero) .logo-frame {
width: min(390px, 100%); width: min(390px, 100%);
min-height: 316px; min-height: 316px;
} }
.hero:not(.historical-hero) .logo-frame__image { .hero--public-page:not(.historical-hero) .logo-frame__image {
max-height: 298px; max-height: 298px;
} }
.hero:not(.historical-hero) .hero__copy { .hero--public-page:not(.historical-hero) .hero__copy {
grid-column: 3; grid-column: 3;
justify-self: end; justify-self: end;
width: 100%; width: 100%;
@@ -79,13 +79,13 @@
transform: none; transform: none;
} }
.hero:not(.historical-hero) .hero__title { .hero--public-page:not(.historical-hero) .hero__title {
font-size: clamp(3.7rem, 5.35vw, 5.15rem); font-size: clamp(3.7rem, 5.35vw, 5.15rem);
} }
} }
@media (max-width: 1120px) { @media (max-width: 1120px) {
.hero:not(.historical-hero) .hero__copy { .hero--public-page:not(.historical-hero) .hero__copy {
grid-column: auto; grid-column: auto;
transform: none; transform: none;
} }

View File

@@ -7,8 +7,8 @@
} }
.historical-hero__content { .historical-hero__content {
padding-top: 44px; padding-top: 58px;
padding-bottom: 46px; padding-bottom: 64px;
} }
.historical-hero__topline { .historical-hero__topline {
@@ -21,7 +21,7 @@
.historical-hero__copy { .historical-hero__copy {
display: grid; display: grid;
gap: 24px; gap: 18px;
min-width: 0; min-width: 0;
} }
@@ -34,7 +34,7 @@
.historical-hero__layout--registry { .historical-hero__layout--registry {
grid-template-columns: minmax(220px, 330px) minmax(0, 1fr); grid-template-columns: minmax(220px, 330px) minmax(0, 1fr);
column-gap: clamp(72px, 7vw, 140px); column-gap: 36px;
} }
.historical-hero__layout > * { .historical-hero__layout > * {
@@ -42,8 +42,8 @@
} }
.historical-logo-frame { .historical-logo-frame {
width: min(300px, 100%); width: min(340px, 100%);
min-height: 220px; min-height: 248px;
} }
.historical-hero__title { .historical-hero__title {
@@ -65,6 +65,11 @@
/* Registry/historico hero: logo left, real flexible spacer, copy anchored right. */ /* Registry/historico hero: logo left, real flexible spacer, copy anchored right. */
@media (min-width: 1121px) { @media (min-width: 1121px) {
.hero--public-page.historical-hero .historical-hero__content {
padding-block: 54px 60px;
padding-inline: clamp(64px, 5.5vw, 104px);
}
.historical-hero__layout--registry { .historical-hero__layout--registry {
width: 100%; width: 100%;
max-width: 1420px; max-width: 1420px;
@@ -113,6 +118,10 @@
} }
@media (min-width: 1121px) and (max-width: 1380px) { @media (min-width: 1121px) and (max-width: 1380px) {
.hero--public-page.historical-hero .historical-hero__content {
padding-inline: clamp(44px, 4vw, 64px);
}
.historical-hero__layout--registry { .historical-hero__layout--registry {
grid-template-columns: minmax(320px, 390px) minmax(28px, 1fr) minmax(560px, 720px); grid-template-columns: minmax(320px, 390px) minmax(28px, 1fr) minmax(560px, 720px);
column-gap: clamp(20px, 2.5vw, 40px); column-gap: clamp(20px, 2.5vw, 40px);

View File

@@ -112,7 +112,9 @@ a {
color: var(--text-soft); color: var(--text-soft);
font-size: 0.82rem; font-size: 0.82rem;
font-weight: 800; font-weight: 800;
letter-spacing: 0.12em; letter-spacing: 0.1em;
line-height: 1.2;
text-align: center;
text-transform: uppercase; text-transform: uppercase;
transition: transition:
transform 160ms ease, transform 160ms ease,
@@ -904,6 +906,15 @@ h2 {
padding-inline: 18px; padding-inline: 18px;
} }
.clan-card--bxb .clan-card__brand {
grid-template-columns: minmax(112px, 138px) minmax(0, 1fr);
}
.clan-card__logo--bxb {
min-height: 108px;
padding: 10px;
}
.clan-card__logo img { .clan-card__logo img {
width: auto; width: auto;
height: auto; height: auto;
@@ -912,6 +923,10 @@ h2 {
object-fit: contain; object-fit: contain;
} }
.clan-card__logo--bxb img {
max-height: 100px;
}
.clan-card__logo-placeholder { .clan-card__logo-placeholder {
width: 100%; width: 100%;
min-height: 84px; min-height: 84px;
@@ -972,11 +987,6 @@ h2 {
margin-top: 8px; margin-top: 8px;
} }
.stats-hero .hero__brand,
.ranking-hero .hero__brand {
align-items: center;
}
.stats-search-form { .stats-search-form {
margin: 0 0 14px; margin: 0 0 14px;
display: grid; display: grid;
@@ -1098,26 +1108,104 @@ h2 {
.stats-annual-table-wrap { .stats-annual-table-wrap {
display: grid; display: grid;
gap: 10px; gap: 16px;
} }
.stats-annual-meta { .stats-annual-meta {
display: grid; display: grid;
gap: 4px; gap: 12px;
margin: 0 0 8px; grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
margin: 0;
} }
.stats-annual-item { .stats-annual-card {
padding: 10px 12px; border: 1px solid rgba(159, 168, 141, 0.2);
border: 1px solid rgba(159, 168, 141, 0.22); border-radius: 18px;
border-radius: 14px; padding: 18px;
margin-bottom: 8px;
background: background:
linear-gradient(180deg, rgba(28, 34, 25, 0.82), rgba(12, 15, 11, 0.92)); linear-gradient(180deg, rgba(28, 34, 25, 0.88), rgba(12, 15, 11, 0.96));
} }
.stats-annual-item p { .stats-annual-meta-item {
margin: 0 0 6px; margin: 0;
padding: 14px 16px;
border: 1px solid rgba(159, 168, 141, 0.18);
border-radius: 16px;
background: rgba(15, 18, 13, 0.62);
}
.stats-annual-meta-item p,
.stats-annual-meta-item strong {
margin: 0;
}
.stats-annual-meta-item p {
margin-bottom: 8px;
color: var(--muted);
font-size: 0.74rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.stats-annual-table-shell {
overflow-x: auto;
border: 1px solid rgba(159, 168, 141, 0.18);
border-radius: 18px;
background: linear-gradient(180deg, rgba(19, 24, 16, 0.88), rgba(10, 13, 9, 0.96));
}
.stats-annual-table {
width: 100%;
min-width: 860px;
border-collapse: collapse;
}
.stats-annual-table th,
.stats-annual-table td {
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid rgba(159, 168, 141, 0.12);
}
.stats-annual-table thead th {
color: var(--accent-strong);
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
background: rgba(14, 18, 12, 0.96);
}
.stats-annual-table tbody tr:hover {
background: rgba(183, 201, 125, 0.05);
}
.stats-annual-table tbody tr:last-child td {
border-bottom: 0;
}
.stats-annual-rank {
color: var(--accent-warm);
font-weight: 700;
}
.stats-annual-player {
display: grid;
gap: 4px;
}
.stats-annual-player strong {
font-size: 1rem;
line-height: 1.35;
}
.stats-annual-player__id {
color: var(--muted);
font-size: 0.8rem;
}
.stats-annual-metric {
color: var(--accent-warm);
font-weight: 700;
} }
.stats-annual-sub { .stats-annual-sub {
@@ -1499,8 +1587,8 @@ h2 {
.public-nav__link { .public-nav__link {
min-height: 46px; min-height: 46px;
padding-inline: 12px; padding-inline: 12px;
font-size: 0.74rem; font-size: 0.72rem;
letter-spacing: 0.08em; letter-spacing: 0.06em;
} }
.stats-search-form__fields { .stats-search-form__fields {
@@ -1525,6 +1613,11 @@ h2 {
padding: 12px 10px; padding: 12px 10px;
} }
.stats-annual-table th,
.stats-annual-table td {
padding: 12px 10px;
}
.logo-frame { .logo-frame {
min-height: 180px; min-height: 180px;
padding: 16px 18px; padding: 16px 18px;

View File

@@ -33,7 +33,7 @@ const COMMUNITY_CLANS = Object.freeze([
logoAlt: "Logo de La 129", logoAlt: "Logo de La 129",
logoClassName: "clan-card__logo--wide", logoClassName: "clan-card__logo--wide",
discordUrl: "", discordUrl: "",
discordLabel: "Proximamente", discordLabel: "Pr\u00f3ximamente",
}, },
{ {
name: "250 Hispania", name: "250 Hispania",
@@ -65,9 +65,10 @@ const COMMUNITY_CLANS = Object.freeze([
"Clan activo de la comunidad.", "Clan activo de la comunidad.",
logoSrc: "./assets/img/clans/bxb.png", logoSrc: "./assets/img/clans/bxb.png",
logoAlt: "Logo de BxB", logoAlt: "Logo de BxB",
logoClassName: "", logoClassName: "clan-card__logo--bxb",
cardClassName: "clan-card--bxb",
discordUrl: "", discordUrl: "",
discordLabel: "Proximamente", discordLabel: "Pr\u00f3ximamente",
}, },
{ {
name: "7dv", name: "7dv",
@@ -209,7 +210,7 @@ async function hydrateServers(
if (serversData.items.length === 0) { if (serversData.items.length === 0) {
serversList.innerHTML = serversList.innerHTML =
'<p class="servers-empty">Informacion de servidores disponible mas adelante.</p>'; '<p class="servers-empty">Informaci\u00f3n de servidores disponible m\u00e1s adelante.</p>';
return; return;
} }
@@ -283,7 +284,7 @@ function renderServerStatsCard(server) {
{ label: "Mapa", value: currentMap, valueClassName: "server-card__quickfact-value--map" }, { label: "Mapa", value: currentMap, valueClassName: "server-card__quickfact-value--map" },
]; ];
if (region) { if (region) {
quickFactItems.push({ label: "Region", value: region }); quickFactItems.push({ label: "Regi\u00f3n", value: region });
} }
const quickFacts = renderQuickFacts(quickFactItems); const quickFacts = renderQuickFacts(quickFactItems);
@@ -345,7 +346,7 @@ function renderServerAction(server) {
return ` return `
<div class="server-card__actions"> <div class="server-card__actions">
<a class="server-action-link" href="${escapeHtml(actions.historicalUrl)}"> <a class="server-action-link" href="${escapeHtml(actions.historicalUrl)}">
Historico Hist\u00f3rico
</a> </a>
<a class="server-action-link" href="${escapeHtml(actions.currentMatchUrl)}"> <a class="server-action-link" href="${escapeHtml(actions.currentMatchUrl)}">
Partida actual Partida actual
@@ -367,9 +368,10 @@ function hydrateCommunityClans(listNode) {
function renderCommunityClanCard(clan) { function renderCommunityClanCard(clan) {
const logoMarkup = renderClanLogo(clan); const logoMarkup = renderClanLogo(clan);
const discordMarkup = renderClanDiscordLink(clan); const discordMarkup = renderClanDiscordLink(clan);
const cardClassName = clan.cardClassName ? ` ${escapeHtml(clan.cardClassName)}` : "";
return ` return `
<article class="clan-card"> <article class="clan-card${cardClassName}">
<div class="clan-card__brand"> <div class="clan-card__brand">
${logoMarkup} ${logoMarkup}
<div class="clan-card__copy"> <div class="clan-card__copy">
@@ -541,7 +543,7 @@ function deriveSnapshotState(serversData) {
return { return {
label: isFresh label: isFresh
? `Actualizado ${timestampLabel}` ? `Actualizado ${timestampLabel}`
: `Ultimo snapshot ${timestampLabel}`, : `\u00daltimo snapshot ${timestampLabel}`,
isFresh, isFresh,
}; };
} }

View File

@@ -7,7 +7,6 @@
const limitSelect = document.getElementById("ranking-limit"); const limitSelect = document.getElementById("ranking-limit");
const yearWrap = document.getElementById("ranking-year-wrap"); const yearWrap = document.getElementById("ranking-year-wrap");
const yearInput = document.getElementById("ranking-year"); const yearInput = document.getElementById("ranking-year");
const backendStateNode = document.getElementById("ranking-backend-state");
const stateNode = document.getElementById("ranking-state"); const stateNode = document.getElementById("ranking-state");
const titleNode = document.getElementById("ranking-title"); const titleNode = document.getElementById("ranking-title");
const metaNode = document.getElementById("ranking-meta"); const metaNode = document.getElementById("ranking-meta");
@@ -48,7 +47,6 @@
applyInitialUrlState(); applyInitialUrlState();
toggleYearField(); toggleYearField();
syncMetricState(); syncMetricState();
setBackendState("Comprobando disponibilidad del backend", false);
setRankingState("neutral", "Esperando filtros para cargar el ranking global."); setRankingState("neutral", "Esperando filtros para cargar el ranking global.");
clearRankingSurface(); clearRankingSurface();
refreshBackendHealth(); refreshBackendHealth();
@@ -90,16 +88,6 @@
}); });
} }
function setBackendState(label, online) {
isBackendOnline = online;
if (!backendStateNode) {
return;
}
backendStateNode.textContent = label;
backendStateNode.classList.toggle("status-chip--ok", online);
backendStateNode.classList.toggle("status-chip--fallback", !online);
}
function setRankingState(state, message) { function setRankingState(state, message) {
if (!stateNode) { if (!stateNode) {
return; return;
@@ -192,7 +180,7 @@
} }
setFilterNote( setFilterNote(
"Ranking compara top globales. Para buscar un jugador concreto usa Stats.", "Ranking compara top globales. Para buscar un jugador concreto usa Estadísticas.",
"neutral", "neutral",
); );
} }
@@ -245,7 +233,7 @@
if (!payload || payload.status !== "ok") { if (!payload || payload.status !== "ok") {
throw new Error("Unexpected health payload"); throw new Error("Unexpected health payload");
} }
setBackendState("Backend operativo", true); isBackendOnline = true;
if (!String(stateNode?.textContent || "").includes("limite del URL")) { if (!String(stateNode?.textContent || "").includes("limite del URL")) {
setRankingState( setRankingState(
"neutral", "neutral",
@@ -255,7 +243,7 @@
void loadRanking(); void loadRanking();
} catch (error) { } catch (error) {
console.warn("Ranking health check failed", error); console.warn("Ranking health check failed", error);
setBackendState("Backend no disponible", false); isBackendOnline = false;
setRankingState("error", "Backend no disponible. El ranking queda en estado offline."); setRankingState("error", "Backend no disponible. El ranking queda en estado offline.");
renderEmptyState( renderEmptyState(
"No fue posible contactar el backend. Cuando vuelva a estar disponible podras consultar semanal, mensual o anual.", "No fue posible contactar el backend. Cuando vuelva a estar disponible podras consultar semanal, mensual o anual.",
@@ -319,7 +307,7 @@
renderRanking(payload.data || {}); renderRanking(payload.data || {});
} catch (error) { } catch (error) {
console.warn("Ranking request failed", error); console.warn("Ranking request failed", error);
setBackendState("Backend no disponible", false); isBackendOnline = false;
setRankingState("error", "Error controlado al cargar el ranking."); setRankingState("error", "Error controlado al cargar el ranking.");
renderEmptyState( renderEmptyState(
"La lectura del ranking fall\u00f3 en este intento. Revisa el backend o actualiza la p\u00e1gina.", "La lectura del ranking fall\u00f3 en este intento. Revisa el backend o actualiza la p\u00e1gina.",

View File

@@ -5,7 +5,6 @@
const searchStateNode = document.getElementById("stats-search-state"); const searchStateNode = document.getElementById("stats-search-state");
const searchHelpNode = document.getElementById("stats-search-help"); const searchHelpNode = document.getElementById("stats-search-help");
const resultListNode = document.getElementById("stats-result-list"); const resultListNode = document.getElementById("stats-result-list");
const backendStateNode = document.getElementById("stats-backend-state");
const profilePanel = document.getElementById("stats-profile-panel"); const profilePanel = document.getElementById("stats-profile-panel");
const profileTitle = document.getElementById("stats-profile-title"); const profileTitle = document.getElementById("stats-profile-title");
const profileStateNode = document.getElementById("stats-profile-state"); const profileStateNode = document.getElementById("stats-profile-state");
@@ -25,9 +24,6 @@
const annualServerId = "all"; const annualServerId = "all";
const messages = { const messages = {
backendChecking: "Comprobando disponibilidad del backend",
backendOnline: "Backend operativo",
backendUnavailable: "Backend no disponible. Reintenta en unos segundos.",
backendUnavailableForSearch: backendUnavailableForSearch:
"Backend no disponible. No se pueden buscar jugadores ni cargar su perfil.", "Backend no disponible. No se pueden buscar jugadores ni cargar su perfil.",
backendUnavailableForAnnual: backendUnavailableForAnnual:
@@ -48,14 +44,14 @@
profileError: "No fue posible cargar las estad\u00edsticas del jugador.", profileError: "No fue posible cargar las estad\u00edsticas del jugador.",
annualLoading: "Cargando ranking anual...", annualLoading: "Cargando ranking anual...",
annualMissing: annualMissing:
"No hay snapshot generado para el a\u00f1o solicitado. Mostrar\u00e1 estado pending hasta que exista.", "Todavia no hay ranking anual publicado para el a\u00f1o solicitado.",
annualReadyEmpty: annualReadyEmpty:
"Ranking anual listo, pero sin datos para el a\u00f1o y servidor seleccionados.", "El ranking anual ya esta disponible, pero no muestra filas para ese filtro.",
annualUnsupportedMetric: annualUnsupportedMetric:
"La m\u00e9trica anual solicitada no est\u00e1 soportada en V1.", "La m\u00e9trica anual solicitada no est\u00e1 soportada en V1.",
annualMetricInvalid: annualMetricInvalid:
"Par\u00e1metro de ranking anual inv\u00e1lido. Usa metric=kills en V1.", "Par\u00e1metro de ranking anual inv\u00e1lido. Usa metric=kills en V1.",
annualReadyPrefix: "Ranking anual listo para", annualReadyPrefix: "Ranking anual disponible para",
weeklyPlaceholder: weeklyPlaceholder:
"Sin datos semanales. El ranking semanal se actualiza al cargar un jugador.", "Sin datos semanales. El ranking semanal se actualiza al cargar un jugador.",
monthlyPlaceholder: monthlyPlaceholder:
@@ -82,7 +78,6 @@
"El ranking anual se consulta por separado con el a\u00f1o indicado."; "El ranking anual se consulta por separado con el a\u00f1o indicado.";
} }
setBackendState(messages.backendChecking, false);
if (annualYearInput) { if (annualYearInput) {
annualYearInput.value = String(annualDefaultYear); annualYearInput.value = String(annualDefaultYear);
} }
@@ -108,18 +103,8 @@
}); });
} }
function setBackendState(label, isOnline) {
isBackendOnline = isOnline;
if (!backendStateNode) {
return;
}
backendStateNode.textContent = label;
backendStateNode.classList.toggle("status-chip--ok", isOnline);
backendStateNode.classList.toggle("status-chip--fallback", !isOnline);
}
function markAsBackendUnavailable() { function markAsBackendUnavailable() {
setBackendState(messages.backendUnavailable, false); isBackendOnline = false;
if (searchStateNode) { if (searchStateNode) {
searchStateNode.textContent = messages.backendUnavailableForSearch; searchStateNode.textContent = messages.backendUnavailableForSearch;
searchStateNode.className = "stats-state stats-state--error"; searchStateNode.className = "stats-state stats-state--error";
@@ -173,10 +158,10 @@
throw new Error("Unexpected health payload"); throw new Error("Unexpected health payload");
} }
setBackendState(messages.backendOnline, true); isBackendOnline = true;
setAnnualState( setAnnualState(
"neutral", "neutral",
"Backend disponible. Selecciona un a\u00f1o para cargar el ranking anual (kills).", "Backend disponible. Selecciona un a\u00f1o para cargar el ranking anual.",
); );
void loadAnnualRanking(); void loadAnnualRanking();
} catch (error) { } catch (error) {
@@ -345,17 +330,33 @@
setAnnualState( setAnnualState(
"neutral", "neutral",
`${messages.annualReadyPrefix} ${serverId}, a\u00f1o ${year}, m\u00e9trica ${metric}`, `${messages.annualReadyPrefix} ${serverId}, a\u00f1o ${year}.`,
); );
annualContentNode.innerHTML = ` annualContentNode.innerHTML = `
<article class="stats-summary-card"> <article class="stats-annual-card">
<p class="stats-summary-title">Top ${limit} anual</p> <p class="stats-summary-title">Top ${limit} anual</p>
<div class="stats-annual-meta"> <div class="stats-annual-meta">
<p><strong>Servidor:</strong> ${escapeHtml(serverId)}</p> <article class="stats-annual-meta-item">
<p><strong>M\u00e9trica:</strong> ${escapeHtml(metric)}</p> <p>Servidor</p>
<p><strong>Partidas fuente:</strong> ${safeInt(sourceMatches, 0)}</p> <strong>${escapeHtml(serverId)}</strong>
<p><strong>Actualizado:</strong> ${escapeHtml(generatedAt || "No disponible")}</p> </article>
<article class="stats-annual-meta-item">
<p>A\u00f1o</p>
<strong>${year}</strong>
</article>
<article class="stats-annual-meta-item">
<p>Lectura</p>
<strong>${escapeHtml(metric)}</strong>
</article>
<article class="stats-annual-meta-item">
<p>Partidas base</p>
<strong>${safeInt(sourceMatches, 0)}</strong>
</article>
<article class="stats-annual-meta-item">
<p>Actualizado</p>
<strong>${escapeHtml(generatedAt || "No disponible")}</strong>
</article>
</div> </div>
${renderAnnualRows(items)} ${renderAnnualRows(items)}
</article> </article>
@@ -363,27 +364,54 @@
} }
function renderAnnualRows(items) { function renderAnnualRows(items) {
return items const rowsMarkup = items
.map((item) => { .map((item) => {
const rank = safeInt(item.ranking_position, 0); const rank = safeInt(item.ranking_position, 0);
const playerId = escapeHtml(String(item.player_id || "")); const playerId = escapeHtml(String(item.player_id || ""));
const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre")); const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre"));
const metricValue = safeInt(item.metric_value, 0); const metricValue = safeInt(item.metric_value, 0);
const matches = safeInt(item.matches_considered, 0); const matches = safeInt(item.matches_considered, 0);
const kills = safeInt(item.kills, 0);
const deaths = safeInt(item.deaths, 0); const deaths = safeInt(item.deaths, 0);
const teamkills = safeInt(item.teamkills, 0); const teamkills = safeInt(item.teamkills, 0);
const kd = safeDecimal(item.kd_ratio, 2, "0.00"); const kd = safeDecimal(item.kd_ratio, 2, "0.00");
return ` return `
<article class="stats-annual-item"> <tr>
<p><strong>#${rank}</strong> ${playerName} <span class="stats-annual-sub">(ID: ${playerId})</span></p> <td class="stats-annual-rank">#${rank}</td>
<p><strong>Valor:</strong> ${metricValue} - <strong>Partidas:</strong> ${matches}</p> <td>
<p><strong>Kills:</strong> ${kills} - <strong>Deaths:</strong> ${deaths} - <strong>Teamkills:</strong> ${teamkills} - <strong>K/D:</strong> ${kd}</p> <div class="stats-annual-player">
</article> <strong>${playerName}</strong>
<span class="stats-annual-player__id">ID ${playerId}</span>
</div>
</td>
<td class="stats-annual-metric">${metricValue}</td>
<td>${matches}</td>
<td>${deaths}</td>
<td>${teamkills}</td>
<td>${kd}</td>
</tr>
`; `;
}) })
.join(""); .join("");
return `
<div class="stats-annual-table-shell">
<table class="stats-annual-table">
<thead>
<tr>
<th>Posici\u00f3n</th>
<th>Jugador</th>
<th>Valor / Kills</th>
<th>Partidas</th>
<th>Muertes</th>
<th>Teamkills</th>
<th>K/D</th>
</tr>
</thead>
<tbody>${rowsMarkup}</tbody>
</table>
</div>
`;
} }
function renderResultItem(item) { function renderResultItem(item) {
@@ -508,7 +536,6 @@
); );
} }
setBackendState(messages.backendOnline, true);
isBackendOnline = true; isBackendOnline = true;
} catch (error) { } catch (error) {
console.warn("Player profile failed", error); console.warn("Player profile failed", error);

View File

@@ -21,13 +21,13 @@
Hist&oacute;rico Hist&oacute;rico
</a> </a>
<a class="public-nav__link" href="./stats.html"> <a class="public-nav__link" href="./stats.html">
Stats Estad&iacute;sticas
</a> </a>
<a class="public-nav__link" href="./ranking.html"> <a class="public-nav__link" href="./ranking.html">
Ranking Ranking
</a> </a>
</nav> </nav>
<header class="hero historical-hero"> <header class="hero hero--public-page historical-hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content historical-hero__content"> <div class="hero__content historical-hero__content">
<div class="historical-hero__layout historical-hero__layout--registry"> <div class="historical-hero__layout historical-hero__layout--registry">
@@ -43,7 +43,6 @@
</div> </div>
<div class="historical-hero__copy"> <div class="historical-hero__copy">
<div> <div>
<p class="eyebrow eyebrow--section">Hist&oacute;rico propio</p>
<h1 class="historical-hero__title"> <h1 class="historical-hero__title">
Registro de la Registro de la
<span class="hero__title-accent">Comunidad Hispana</span> <span class="hero__title-accent">Comunidad Hispana</span>

View File

@@ -24,13 +24,13 @@
Histórico Histórico
</a> </a>
<a class="public-nav__link" href="./stats.html"> <a class="public-nav__link" href="./stats.html">
Stats Estadísticas
</a> </a>
<a class="public-nav__link" href="./ranking.html"> <a class="public-nav__link" href="./ranking.html">
Ranking Ranking
</a> </a>
</nav> </nav>
<header class="hero"> <header class="hero hero--public-page">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content"> <div class="hero__content">
<div class="hero__brand"> <div class="hero__brand">

View File

@@ -9,6 +9,7 @@
/> />
<title>Ranking - HLL Vietnam</title> <title>Ranking - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
<link rel="stylesheet" href="./assets/css/hero-header-compact.css" />
</head> </head>
<body data-backend-base-url="http://127.0.0.1:8000"> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell stats-page-shell"> <div class="page-shell stats-page-shell">
@@ -20,13 +21,13 @@
Hist&oacute;rico Hist&oacute;rico
</a> </a>
<a class="public-nav__link" href="./stats.html"> <a class="public-nav__link" href="./stats.html">
Stats Estad&iacute;sticas
</a> </a>
<a class="public-nav__link is-active" href="./ranking.html" aria-current="page"> <a class="public-nav__link is-active" href="./ranking.html" aria-current="page">
Ranking Ranking
</a> </a>
</nav> </nav>
<header class="hero stats-hero ranking-hero"> <header class="hero hero--public-page stats-hero ranking-hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content"> <div class="hero__content">
<div class="hero__brand"> <div class="hero__brand">
@@ -41,7 +42,6 @@
/> />
</div> </div>
<div class="hero__copy"> <div class="hero__copy">
<p class="eyebrow eyebrow--section">Secci&oacute;n Ranking</p>
<h1 class="hero__title"> <h1 class="hero__title">
Ranking Ranking
<span class="hero__title-accent">Global</span> <span class="hero__title-accent">Global</span>
@@ -62,7 +62,7 @@
<h2>Top p&uacute;blico por ventana</h2> <h2>Top p&uacute;blico por ventana</h2>
</div> </div>
<p class="panel__intro panel__intro--tight"> <p class="panel__intro panel__intro--tight">
El ranking expone la lectura p&uacute;blica de l&iacute;deres. Para b&uacute;squeda individual usa la secci&oacute;n Stats. El ranking expone la lectura p&uacute;blica de l&iacute;deres. Para b&uacute;squeda individual usa Estad&iacute;sticas.
</p> </p>
<form class="ranking-form" id="ranking-form"> <form class="ranking-form" id="ranking-form">
<div class="ranking-form__grid"> <div class="ranking-form__grid">
@@ -123,7 +123,7 @@
<div class="hero__actions ranking-form__actions"> <div class="hero__actions ranking-form__actions">
<button class="discord-button" type="submit">Actualizar ranking</button> <button class="discord-button" type="submit">Actualizar ranking</button>
<a class="secondary-button secondary-button--compact" href="./stats.html"> <a class="secondary-button secondary-button--compact" href="./stats.html">
Buscar jugador en Stats Buscar jugador en Estad&iacute;sticas
</a> </a>
</div> </div>
</form> </form>

View File

@@ -5,10 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta <meta
name="description" name="description"
content="Consulta estad&iacute;sticas personales de jugadores en el panel Stats de HLL Vietnam." content="Consulta estad&iacute;sticas personales de jugadores en la zona p&uacute;blica de HLL Vietnam."
/> />
<title>Stats - HLL Vietnam</title> <title>Estad&iacute;sticas - HLL Vietnam</title>
<link rel="stylesheet" href="./assets/css/styles.css" /> <link rel="stylesheet" href="./assets/css/styles.css" />
<link rel="stylesheet" href="./assets/css/hero-header-compact.css" />
</head> </head>
<body data-backend-base-url="http://127.0.0.1:8000"> <body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell stats-page-shell"> <div class="page-shell stats-page-shell">
@@ -20,13 +21,13 @@
Hist&oacute;rico Hist&oacute;rico
</a> </a>
<a class="public-nav__link is-active" href="./stats.html" aria-current="page"> <a class="public-nav__link is-active" href="./stats.html" aria-current="page">
Stats Estad&iacute;sticas
</a> </a>
<a class="public-nav__link" href="./ranking.html"> <a class="public-nav__link" href="./ranking.html">
Ranking Ranking
</a> </a>
</nav> </nav>
<header class="hero stats-hero"> <header class="hero hero--public-page stats-hero">
<div class="hero__overlay"></div> <div class="hero__overlay"></div>
<div class="hero__content"> <div class="hero__content">
<div class="hero__brand"> <div class="hero__brand">
@@ -41,7 +42,6 @@
/> />
</div> </div>
<div class="hero__copy"> <div class="hero__copy">
<p class="eyebrow eyebrow--section">Secci&oacute;n Stats</p>
<h1 class="hero__title"> <h1 class="hero__title">
Estad&iacute;sticas Estad&iacute;sticas
<span class="hero__title-accent">Personales</span> <span class="hero__title-accent">Personales</span>
@@ -126,7 +126,7 @@
<h2>Top 20 anual</h2> <h2>Top 20 anual</h2>
</div> </div>
<p class="panel__intro panel__intro--tight"> <p class="panel__intro panel__intro--tight">
El ranking anual usa snapshots pre-computados para evitar recalcular toda la temporada en cada consulta. El ranking anual usa un corte ya preparado de la temporada para cargar el Top 20 sin recalcular toda la campa&ntilde;a en cada consulta.
</p> </p>
<form class="stats-annual-form" id="stats-annual-form"> <form class="stats-annual-form" id="stats-annual-form">
<label class="stats-search-form__label" for="stats-annual-year">A&ntilde;o</label> <label class="stats-search-form__label" for="stats-annual-year">A&ntilde;o</label>