Fix ranking layout and loading state
This commit is contained in:
@@ -1479,6 +1479,11 @@ h2 {
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
|
||||
}
|
||||
|
||||
.ranking-results-title {
|
||||
margin: 0 0 16px;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.ranking-meta-card {
|
||||
margin: 0;
|
||||
padding: 14px 16px;
|
||||
@@ -1569,6 +1574,11 @@ h2 {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.ranking-form__actions .discord-button[disabled] {
|
||||
opacity: 0.72;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.server-action-link--disabled {
|
||||
border-color: rgba(159, 168, 141, 0.18);
|
||||
background: linear-gradient(180deg, rgba(42, 46, 39, 0.5), rgba(19, 22, 17, 0.7));
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
const serverSelect = document.getElementById("ranking-server");
|
||||
const metricSelect = document.getElementById("ranking-metric");
|
||||
const limitSelect = document.getElementById("ranking-limit");
|
||||
const submitButton = document.getElementById("ranking-submit");
|
||||
const yearWrap = document.getElementById("ranking-year-wrap");
|
||||
const yearInput = document.getElementById("ranking-year");
|
||||
const stateNode = document.getElementById("ranking-state");
|
||||
@@ -37,9 +38,10 @@
|
||||
"comunidad-hispana-01",
|
||||
"comunidad-hispana-02",
|
||||
];
|
||||
const supportedLimits = ["5", "10", "20", "50", "100"];
|
||||
const supportedLimits = ["5", "10", "20", "30", "50", "100"];
|
||||
|
||||
let isBackendOnline = false;
|
||||
let currentRequestId = 0;
|
||||
let activeController = null;
|
||||
|
||||
if (yearInput) {
|
||||
yearInput.value = String(currentYear);
|
||||
@@ -48,9 +50,9 @@
|
||||
applyInitialUrlState();
|
||||
toggleYearField();
|
||||
syncMetricState();
|
||||
setRankingState("neutral", "Esperando filtros para cargar el ranking global.");
|
||||
setRankingState("neutral", "Preparando ranking p\u00fablico...");
|
||||
clearRankingSurface();
|
||||
refreshBackendHealth();
|
||||
void loadRanking();
|
||||
|
||||
if (timeframeSelect) {
|
||||
timeframeSelect.addEventListener("change", () => {
|
||||
@@ -105,6 +107,14 @@
|
||||
filterNoteNode.className = `ranking-form__note ranking-form__note--${tone}`;
|
||||
}
|
||||
|
||||
function setFormBusy(isBusy) {
|
||||
if (!submitButton) {
|
||||
return;
|
||||
}
|
||||
submitButton.disabled = isBusy;
|
||||
submitButton.setAttribute("aria-busy", String(isBusy));
|
||||
}
|
||||
|
||||
function applyInitialUrlState() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const timeframe = params.get("timeframe");
|
||||
@@ -181,7 +191,7 @@
|
||||
}
|
||||
|
||||
setFilterNote(
|
||||
"Ranking compara top globales. Para buscar un jugador concreto usa Estadísticas.",
|
||||
"El ranking expone los resultados de los l\u00edderes. Para b\u00fasqueda individual usa Estad\u00edsticas.",
|
||||
"neutral",
|
||||
);
|
||||
}
|
||||
@@ -224,48 +234,14 @@
|
||||
emptyNode.textContent = message;
|
||||
}
|
||||
|
||||
async function refreshBackendHealth() {
|
||||
try {
|
||||
const response = await fetch(`${backendBaseUrl}/health`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Health request failed with ${response.status}`);
|
||||
}
|
||||
const payload = await response.json();
|
||||
if (!payload || payload.status !== "ok") {
|
||||
throw new Error("Unexpected health payload");
|
||||
}
|
||||
isBackendOnline = true;
|
||||
if (!String(stateNode?.textContent || "").includes("limite del URL")) {
|
||||
setRankingState(
|
||||
"neutral",
|
||||
"Backend disponible. Ajusta filtros o usa la lectura inicial.",
|
||||
);
|
||||
}
|
||||
void loadRanking();
|
||||
} catch (error) {
|
||||
console.warn("Ranking health check failed", error);
|
||||
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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRanking() {
|
||||
const requestId = currentRequestId + 1;
|
||||
currentRequestId = requestId;
|
||||
const timeframe = String(timeframeSelect?.value || defaultTimeframe);
|
||||
const serverId = String(serverSelect?.value || defaultServerId);
|
||||
const metric = String(metricSelect?.value || defaultMetric);
|
||||
const limit = String(limitSelect?.value || defaultLimit);
|
||||
|
||||
if (!isBackendOnline) {
|
||||
setRankingState("error", "Backend no disponible. El ranking queda en estado offline.");
|
||||
renderEmptyState(
|
||||
"No fue posible contactar el backend. Reintenta cuando el servicio vuelva a estar disponible.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let year = null;
|
||||
if (timeframe === "annual") {
|
||||
year = Number.parseInt(String(yearInput?.value || "").trim(), 10);
|
||||
@@ -277,7 +253,12 @@
|
||||
}
|
||||
|
||||
setRankingState("loading", "Cargando ranking global...");
|
||||
clearRankingSurface();
|
||||
setFormBusy(true);
|
||||
|
||||
if (activeController) {
|
||||
activeController.abort();
|
||||
}
|
||||
activeController = new AbortController();
|
||||
|
||||
try {
|
||||
const searchParams = new URLSearchParams({
|
||||
@@ -290,7 +271,12 @@
|
||||
searchParams.set("year", String(year));
|
||||
}
|
||||
|
||||
const response = await fetch(`${backendBaseUrl}/api/ranking?${searchParams.toString()}`);
|
||||
const response = await fetch(`${backendBaseUrl}/api/ranking?${searchParams.toString()}`, {
|
||||
signal: activeController.signal,
|
||||
});
|
||||
if (requestId !== currentRequestId) {
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const errorPayload = await safeParseJson(response);
|
||||
const errorMessage = String(
|
||||
@@ -307,12 +293,22 @@
|
||||
|
||||
renderRanking(payload.data || {});
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
if (requestId !== currentRequestId) {
|
||||
return;
|
||||
}
|
||||
console.warn("Ranking request failed", error);
|
||||
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.",
|
||||
"La lectura del ranking fall\u00f3 en este intento. Revisa el backend o vuelve a intentarlo.",
|
||||
);
|
||||
} finally {
|
||||
if (requestId === currentRequestId) {
|
||||
setFormBusy(false);
|
||||
activeController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +317,7 @@
|
||||
if (statusCode === 400 && normalizedMessage.includes("limit")) {
|
||||
setRankingState("warning", "El limite solicitado no es valido.");
|
||||
renderEmptyState(
|
||||
"Usa un limite permitido por la interfaz o por el backend. Esta vista admite Top 5, 10, 20, 50 y 100.",
|
||||
"Usa un limite permitido por la interfaz o por el backend. Esta vista admite Top 5, 10, 20, 30, 50 y 100.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -350,7 +346,7 @@
|
||||
}
|
||||
if (statusCode === 400 && normalizedMessage.includes("timeframe")) {
|
||||
setRankingState("warning", "El periodo solicitado no esta soportado.");
|
||||
renderEmptyState("Usa una ventana semanal, mensual o anual.");
|
||||
renderEmptyState("Usa un periodo semanal, mensual o anual.");
|
||||
return;
|
||||
}
|
||||
setRankingState(
|
||||
@@ -402,7 +398,7 @@
|
||||
renderEmptyState(
|
||||
timeframe === "annual"
|
||||
? "La lectura anual existe pero no devolvio filas para este filtro."
|
||||
: "No se encontraron jugadores con actividad suficiente en esta ventana.",
|
||||
: "No se encontraron jugadores con actividad suficiente en este periodo.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -433,7 +429,7 @@
|
||||
{ label: "Servidor activo", value: labelForServer(data.server_id), active: true },
|
||||
{ label: "M\u00e9trica activa", value: labelForMetric(metric), active: true },
|
||||
{ label: "L\u00edmite", value: `Top ${safeInt(data.limit, safeInt(defaultLimit, 20))}` },
|
||||
{ label: "Ventana", value: labelForWindow(data) },
|
||||
{ label: "Periodo", value: labelForWindow(data) },
|
||||
{ label: "Actualizado", value: formatDateTime(source.generated_at) },
|
||||
];
|
||||
|
||||
@@ -501,7 +497,7 @@
|
||||
if (String(data.timeframe || "") === "annual") {
|
||||
return `A\u00f1o ${safeInt(data.year, currentYear)}`;
|
||||
}
|
||||
return String(data.window_label || data.window_kind || "Ventana activa");
|
||||
return String(data.window_label || data.window_kind || "Periodo activo");
|
||||
}
|
||||
|
||||
function labelForServer(serverId) {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<span class="hero__title-accent">Global</span>
|
||||
</h1>
|
||||
<p class="hero__text">
|
||||
Consulta los líderes públicos de la comunidad, cambia de periodo y servidor sin salir de la página y revisa quién destaca en cada ventana.
|
||||
Consulta los líderes públicos de la comunidad, cambia de periodo y servidor sin salir de la página y revisa quién destaca en cada periodo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -59,10 +59,10 @@
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header">
|
||||
<p class="eyebrow eyebrow--section">Filtros de lectura</p>
|
||||
<h2>Top público por ventana</h2>
|
||||
<h2>Top público por periodo</h2>
|
||||
</div>
|
||||
<p class="panel__intro panel__intro--tight">
|
||||
El ranking expone la lectura pública de líderes. Para búsqueda individual usa Estadísticas.
|
||||
El ranking expone los resultados de los líderes. Para búsqueda individual usa Estadísticas.
|
||||
</p>
|
||||
<form class="ranking-form" id="ranking-form">
|
||||
<div class="ranking-form__grid">
|
||||
@@ -99,6 +99,7 @@
|
||||
<option value="5">Top 5</option>
|
||||
<option value="10">Top 10</option>
|
||||
<option value="20" selected>Top 20</option>
|
||||
<option value="30">Top 30</option>
|
||||
<option value="50">Top 50</option>
|
||||
<option value="100">Top 100</option>
|
||||
</select>
|
||||
@@ -121,24 +122,16 @@
|
||||
Consulta líderes públicos y cambia de periodo o servidor sin salir de la página.
|
||||
</p>
|
||||
<div class="hero__actions ranking-form__actions">
|
||||
<button class="discord-button" type="submit">Actualizar ranking</button>
|
||||
<button class="discord-button" id="ranking-submit" type="submit">Actualizar ranking</button>
|
||||
<a class="secondary-button secondary-button--compact" href="./stats.html">
|
||||
Buscar jugador en Estadísticas
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
<p class="stats-state stats-state--neutral" id="ranking-state" role="status" aria-live="polite">
|
||||
Esperando filtros para cargar el ranking global.
|
||||
Preparando ranking público...
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header">
|
||||
<p class="eyebrow eyebrow--section">Resultados</p>
|
||||
<h2 id="ranking-title">Tabla activa</h2>
|
||||
</div>
|
||||
<h3 class="ranking-results-title" id="ranking-title">Tabla activa</h3>
|
||||
<div class="ranking-meta" id="ranking-meta"></div>
|
||||
<div class="ranking-table-shell">
|
||||
<table class="historical-table ranking-table" id="ranking-table" hidden>
|
||||
|
||||
Reference in New Issue
Block a user