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) {
.hero:not(.historical-hero) .hero__content {
.hero--public-page:not(.historical-hero) .hero__content {
padding-block: 54px 60px;
padding-inline: clamp(64px, 5.5vw, 104px);
}
.hero:not(.historical-hero) .hero__brand {
.hero--public-page:not(.historical-hero) .hero__brand {
width: 100%;
max-width: 1420px;
margin: 0 auto;
@@ -14,7 +14,7 @@
align-items: center;
}
.hero:not(.historical-hero) .logo-frame {
.hero--public-page:not(.historical-hero) .logo-frame {
grid-column: 1;
justify-self: start;
width: min(470px, 100%);
@@ -22,11 +22,11 @@
padding: 30px 34px;
}
.hero:not(.historical-hero) .logo-frame__image {
.hero--public-page:not(.historical-hero) .logo-frame__image {
max-height: 340px;
}
.hero:not(.historical-hero) .hero__copy {
.hero--public-page:not(.historical-hero) .hero__copy {
grid-column: 3;
justify-self: end;
width: 100%;
@@ -37,41 +37,41 @@
transform: none;
}
.hero:not(.historical-hero) .hero__title {
.hero--public-page:not(.historical-hero) .hero__title {
max-width: none;
font-size: clamp(4.2rem, 5.2vw, 6.35rem);
line-height: 1.02;
}
.hero:not(.historical-hero) .hero__title-accent {
.hero--public-page:not(.historical-hero) .hero__title-accent {
display: inline;
}
.hero:not(.historical-hero) .hero__text {
.hero--public-page:not(.historical-hero) .hero__text {
max-width: 68ch;
}
}
@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);
}
.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);
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%);
min-height: 316px;
}
.hero:not(.historical-hero) .logo-frame__image {
.hero--public-page:not(.historical-hero) .logo-frame__image {
max-height: 298px;
}
.hero:not(.historical-hero) .hero__copy {
.hero--public-page:not(.historical-hero) .hero__copy {
grid-column: 3;
justify-self: end;
width: 100%;
@@ -79,14 +79,14 @@
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);
}
}
@media (max-width: 1120px) {
.hero:not(.historical-hero) .hero__copy {
.hero--public-page:not(.historical-hero) .hero__copy {
grid-column: auto;
transform: none;
}
}
}

View File

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

View File

@@ -112,7 +112,9 @@ a {
color: var(--text-soft);
font-size: 0.82rem;
font-weight: 800;
letter-spacing: 0.12em;
letter-spacing: 0.1em;
line-height: 1.2;
text-align: center;
text-transform: uppercase;
transition:
transform 160ms ease,
@@ -904,6 +906,15 @@ h2 {
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 {
width: auto;
height: auto;
@@ -912,6 +923,10 @@ h2 {
object-fit: contain;
}
.clan-card__logo--bxb img {
max-height: 100px;
}
.clan-card__logo-placeholder {
width: 100%;
min-height: 84px;
@@ -972,11 +987,6 @@ h2 {
margin-top: 8px;
}
.stats-hero .hero__brand,
.ranking-hero .hero__brand {
align-items: center;
}
.stats-search-form {
margin: 0 0 14px;
display: grid;
@@ -1098,26 +1108,104 @@ h2 {
.stats-annual-table-wrap {
display: grid;
gap: 10px;
gap: 16px;
}
.stats-annual-meta {
display: grid;
gap: 4px;
margin: 0 0 8px;
gap: 12px;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
margin: 0;
}
.stats-annual-item {
padding: 10px 12px;
border: 1px solid rgba(159, 168, 141, 0.22);
border-radius: 14px;
margin-bottom: 8px;
.stats-annual-card {
border: 1px solid rgba(159, 168, 141, 0.2);
border-radius: 18px;
padding: 18px;
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 {
margin: 0 0 6px;
.stats-annual-meta-item {
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 {
@@ -1499,8 +1587,8 @@ h2 {
.public-nav__link {
min-height: 46px;
padding-inline: 12px;
font-size: 0.74rem;
letter-spacing: 0.08em;
font-size: 0.72rem;
letter-spacing: 0.06em;
}
.stats-search-form__fields {
@@ -1525,6 +1613,11 @@ h2 {
padding: 12px 10px;
}
.stats-annual-table th,
.stats-annual-table td {
padding: 12px 10px;
}
.logo-frame {
min-height: 180px;
padding: 16px 18px;

View File

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

View File

@@ -7,7 +7,6 @@
const limitSelect = document.getElementById("ranking-limit");
const yearWrap = document.getElementById("ranking-year-wrap");
const yearInput = document.getElementById("ranking-year");
const backendStateNode = document.getElementById("ranking-backend-state");
const stateNode = document.getElementById("ranking-state");
const titleNode = document.getElementById("ranking-title");
const metaNode = document.getElementById("ranking-meta");
@@ -48,7 +47,6 @@
applyInitialUrlState();
toggleYearField();
syncMetricState();
setBackendState("Comprobando disponibilidad del backend", false);
setRankingState("neutral", "Esperando filtros para cargar el ranking global.");
clearRankingSurface();
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) {
if (!stateNode) {
return;
@@ -192,7 +180,7 @@
}
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",
);
}
@@ -245,7 +233,7 @@
if (!payload || payload.status !== "ok") {
throw new Error("Unexpected health payload");
}
setBackendState("Backend operativo", true);
isBackendOnline = true;
if (!String(stateNode?.textContent || "").includes("limite del URL")) {
setRankingState(
"neutral",
@@ -255,7 +243,7 @@
void loadRanking();
} catch (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.");
renderEmptyState(
"No fue posible contactar el backend. Cuando vuelva a estar disponible podras consultar semanal, mensual o anual.",
@@ -319,7 +307,7 @@
renderRanking(payload.data || {});
} catch (error) {
console.warn("Ranking request failed", error);
setBackendState("Backend no disponible", false);
isBackendOnline = false;
setRankingState("error", "Error controlado al cargar el ranking.");
renderEmptyState(
"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 searchHelpNode = document.getElementById("stats-search-help");
const resultListNode = document.getElementById("stats-result-list");
const backendStateNode = document.getElementById("stats-backend-state");
const profilePanel = document.getElementById("stats-profile-panel");
const profileTitle = document.getElementById("stats-profile-title");
const profileStateNode = document.getElementById("stats-profile-state");
@@ -25,9 +24,6 @@
const annualServerId = "all";
const messages = {
backendChecking: "Comprobando disponibilidad del backend",
backendOnline: "Backend operativo",
backendUnavailable: "Backend no disponible. Reintenta en unos segundos.",
backendUnavailableForSearch:
"Backend no disponible. No se pueden buscar jugadores ni cargar su perfil.",
backendUnavailableForAnnual:
@@ -48,14 +44,14 @@
profileError: "No fue posible cargar las estad\u00edsticas del jugador.",
annualLoading: "Cargando ranking anual...",
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:
"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:
"La m\u00e9trica anual solicitada no est\u00e1 soportada en V1.",
annualMetricInvalid:
"Par\u00e1metro de ranking anual inv\u00e1lido. Usa metric=kills en V1.",
annualReadyPrefix: "Ranking anual listo para",
annualReadyPrefix: "Ranking anual disponible para",
weeklyPlaceholder:
"Sin datos semanales. El ranking semanal se actualiza al cargar un jugador.",
monthlyPlaceholder:
@@ -82,7 +78,6 @@
"El ranking anual se consulta por separado con el a\u00f1o indicado.";
}
setBackendState(messages.backendChecking, false);
if (annualYearInput) {
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() {
setBackendState(messages.backendUnavailable, false);
isBackendOnline = false;
if (searchStateNode) {
searchStateNode.textContent = messages.backendUnavailableForSearch;
searchStateNode.className = "stats-state stats-state--error";
@@ -173,10 +158,10 @@
throw new Error("Unexpected health payload");
}
setBackendState(messages.backendOnline, true);
isBackendOnline = true;
setAnnualState(
"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();
} catch (error) {
@@ -345,17 +330,33 @@
setAnnualState(
"neutral",
`${messages.annualReadyPrefix} ${serverId}, a\u00f1o ${year}, m\u00e9trica ${metric}`,
`${messages.annualReadyPrefix} ${serverId}, a\u00f1o ${year}.`,
);
annualContentNode.innerHTML = `
<article class="stats-summary-card">
<article class="stats-annual-card">
<p class="stats-summary-title">Top ${limit} anual</p>
<div class="stats-annual-meta">
<p><strong>Servidor:</strong> ${escapeHtml(serverId)}</p>
<p><strong>M\u00e9trica:</strong> ${escapeHtml(metric)}</p>
<p><strong>Partidas fuente:</strong> ${safeInt(sourceMatches, 0)}</p>
<p><strong>Actualizado:</strong> ${escapeHtml(generatedAt || "No disponible")}</p>
<article class="stats-annual-meta-item">
<p>Servidor</p>
<strong>${escapeHtml(serverId)}</strong>
</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>
${renderAnnualRows(items)}
</article>
@@ -363,27 +364,54 @@
}
function renderAnnualRows(items) {
return items
const rowsMarkup = items
.map((item) => {
const rank = safeInt(item.ranking_position, 0);
const playerId = escapeHtml(String(item.player_id || ""));
const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre"));
const metricValue = safeInt(item.metric_value, 0);
const matches = safeInt(item.matches_considered, 0);
const kills = safeInt(item.kills, 0);
const deaths = safeInt(item.deaths, 0);
const teamkills = safeInt(item.teamkills, 0);
const kd = safeDecimal(item.kd_ratio, 2, "0.00");
return `
<article class="stats-annual-item">
<p><strong>#${rank}</strong> ${playerName} <span class="stats-annual-sub">(ID: ${playerId})</span></p>
<p><strong>Valor:</strong> ${metricValue} - <strong>Partidas:</strong> ${matches}</p>
<p><strong>Kills:</strong> ${kills} - <strong>Deaths:</strong> ${deaths} - <strong>Teamkills:</strong> ${teamkills} - <strong>K/D:</strong> ${kd}</p>
</article>
<tr>
<td class="stats-annual-rank">#${rank}</td>
<td>
<div class="stats-annual-player">
<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("");
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) {
@@ -508,7 +536,6 @@
);
}
setBackendState(messages.backendOnline, true);
isBackendOnline = true;
} catch (error) {
console.warn("Player profile failed", error);

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
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/hero-header-compact.css" />
</head>
<body data-backend-base-url="http://127.0.0.1:8000">
<div class="page-shell stats-page-shell">
@@ -20,13 +21,13 @@
Hist&oacute;rico
</a>
<a class="public-nav__link is-active" href="./stats.html" aria-current="page">
Stats
Estad&iacute;sticas
</a>
<a class="public-nav__link" href="./ranking.html">
Ranking
</a>
</nav>
<header class="hero stats-hero">
<header class="hero hero--public-page stats-hero">
<div class="hero__overlay"></div>
<div class="hero__content">
<div class="hero__brand">
@@ -41,7 +42,6 @@
/>
</div>
<div class="hero__copy">
<p class="eyebrow eyebrow--section">Secci&oacute;n Stats</p>
<h1 class="hero__title">
Estad&iacute;sticas
<span class="hero__title-accent">Personales</span>
@@ -126,7 +126,7 @@
<h2>Top 20 anual</h2>
</div>
<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>
<form class="stats-annual-form" id="stats-annual-form">
<label class="stats-search-form__label" for="stats-annual-year">A&ntilde;o</label>