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

@@ -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);