Add historical UI and backend API slice
This commit is contained in:
242
frontend/assets/css/historico.css
Normal file
242
frontend/assets/css/historico.css
Normal file
@@ -0,0 +1,242 @@
|
||||
.historical-shell {
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
.historical-hero {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.historical-hero__content {
|
||||
padding-top: 44px;
|
||||
padding-bottom: 46px;
|
||||
}
|
||||
|
||||
.historical-hero__topline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.historical-hero__copy {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.historical-hero__title {
|
||||
max-width: 12ch;
|
||||
}
|
||||
|
||||
.historical-hero__text {
|
||||
max-width: 60ch;
|
||||
}
|
||||
|
||||
.historical-content {
|
||||
gap: 22px;
|
||||
}
|
||||
|
||||
.historical-panel {
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.historical-panel__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.historical-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.historical-selector__button {
|
||||
min-height: 48px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid rgba(183, 201, 125, 0.28);
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(180deg, rgba(24, 30, 22, 0.94), rgba(11, 14, 10, 0.98));
|
||||
color: var(--text-soft);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
border-color 160ms ease,
|
||||
background 160ms ease,
|
||||
color 160ms ease;
|
||||
}
|
||||
|
||||
.historical-selector__button:hover,
|
||||
.historical-selector__button:focus-visible,
|
||||
.historical-selector__button.is-active {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(210, 182, 118, 0.5);
|
||||
background: linear-gradient(180deg, rgba(183, 201, 125, 0.18), rgba(89, 101, 58, 0.24));
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.historical-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.historical-stat-card,
|
||||
.historical-match-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 18px;
|
||||
border: 1px solid rgba(159, 168, 141, 0.16);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(28, 34, 25, 0.94), rgba(12, 15, 11, 0.98));
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.historical-stat-card p,
|
||||
.historical-stat-card strong,
|
||||
.historical-match-card p,
|
||||
.historical-match-card strong {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.historical-stat-card p,
|
||||
.historical-match-meta__label {
|
||||
margin-bottom: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-stat-card strong {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.historical-state {
|
||||
margin: 0 0 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px dashed rgba(159, 168, 141, 0.28);
|
||||
border-radius: 14px;
|
||||
color: var(--text-soft);
|
||||
background: rgba(13, 17, 12, 0.52);
|
||||
}
|
||||
|
||||
.historical-state.is-error {
|
||||
border-style: solid;
|
||||
border-color: rgba(210, 182, 118, 0.28);
|
||||
color: var(--accent-warm);
|
||||
}
|
||||
|
||||
.historical-state[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.historical-table-shell {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.historical-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 620px;
|
||||
}
|
||||
|
||||
.historical-table th,
|
||||
.historical-table td {
|
||||
padding: 14px 12px;
|
||||
border-bottom: 1px solid rgba(159, 168, 141, 0.12);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.historical-table th {
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-table td {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.historical-table tbody tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.historical-table__position {
|
||||
color: var(--accent-warm);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.historical-match-list {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.historical-match-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.historical-match-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.historical-match-card__title {
|
||||
margin: 0;
|
||||
font-size: 1.08rem;
|
||||
}
|
||||
|
||||
.historical-match-card__result {
|
||||
padding: 0.45rem 0.75rem;
|
||||
border: 1px solid rgba(183, 201, 125, 0.24);
|
||||
border-radius: 999px;
|
||||
color: var(--accent-strong);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.historical-match-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.historical-match-meta strong {
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.historical-hero__topline,
|
||||
.historical-panel__header,
|
||||
.historical-match-card__top {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.historical-selector {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.historical-selector__button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.historical-table {
|
||||
min-width: 540px;
|
||||
}
|
||||
}
|
||||
@@ -332,6 +332,47 @@ h2 {
|
||||
box-shadow: 0 12px 30px rgba(110, 127, 72, 0.35);
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 52px;
|
||||
min-width: 220px;
|
||||
padding: 0 24px;
|
||||
border: 1px solid rgba(210, 182, 118, 0.32);
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(180deg, rgba(28, 34, 25, 0.9), rgba(12, 15, 11, 0.96));
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
border-color 160ms ease,
|
||||
background 160ms ease;
|
||||
}
|
||||
|
||||
.secondary-button:hover,
|
||||
.secondary-button:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(210, 182, 118, 0.5);
|
||||
background: linear-gradient(180deg, rgba(43, 50, 36, 0.94), rgba(16, 20, 14, 0.98));
|
||||
}
|
||||
|
||||
.secondary-button--ghost {
|
||||
min-width: 0;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.secondary-button--compact {
|
||||
min-height: 44px;
|
||||
min-width: 0;
|
||||
padding: 0 18px;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.discord-button:hover,
|
||||
.discord-button:focus-visible {
|
||||
transform: translateY(-1px);
|
||||
@@ -410,6 +451,17 @@ h2 {
|
||||
max-width: 58ch;
|
||||
}
|
||||
|
||||
.panel__actions {
|
||||
margin-top: 18px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.panel__actions--compact {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel--video {
|
||||
position: relative;
|
||||
backdrop-filter: blur(4px);
|
||||
@@ -764,6 +816,10 @@ h2 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.secondary-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 18px;
|
||||
gap: 18px;
|
||||
|
||||
329
frontend/assets/js/historico.js
Normal file
329
frontend/assets/js/historico.js
Normal file
@@ -0,0 +1,329 @@
|
||||
const HISTORICAL_SERVER_SLUGS = Object.freeze([
|
||||
"comunidad-hispana-01",
|
||||
"comunidad-hispana-02",
|
||||
]);
|
||||
const DEFAULT_HISTORICAL_SERVER = HISTORICAL_SERVER_SLUGS[0];
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const backendBaseUrl =
|
||||
document.body.dataset.backendBaseUrl || "http://127.0.0.1:8000";
|
||||
const selectorButtons = Array.from(
|
||||
document.querySelectorAll("[data-server-slug]"),
|
||||
);
|
||||
const summaryNode = document.getElementById("historical-summary");
|
||||
const rangeNode = document.getElementById("historical-range");
|
||||
const weeklyStateNode = document.getElementById("weekly-top-kills-state");
|
||||
const weeklyTableNode = document.getElementById("weekly-top-kills-table");
|
||||
const weeklyBodyNode = document.getElementById("weekly-top-kills-body");
|
||||
const recentStateNode = document.getElementById("recent-matches-state");
|
||||
const recentListNode = document.getElementById("recent-matches-list");
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
let activeServerSlug = normalizeServerSlug(params.get("server"));
|
||||
|
||||
const refreshHistoricalView = async () => {
|
||||
syncActiveButtons(selectorButtons, activeServerSlug);
|
||||
setRangeBadge(rangeNode, "Cargando rango temporal", false);
|
||||
renderSummaryLoading(summaryNode);
|
||||
setState(weeklyStateNode, "Cargando ranking semanal...");
|
||||
setState(recentStateNode, "Cargando partidas recientes...");
|
||||
weeklyTableNode.hidden = true;
|
||||
recentListNode.innerHTML = "";
|
||||
|
||||
const [summaryResult, topKillsResult, recentMatchesResult] =
|
||||
await Promise.allSettled([
|
||||
fetchJson(
|
||||
`${backendBaseUrl}/api/historical/server-summary?server=${encodeURIComponent(activeServerSlug)}`,
|
||||
),
|
||||
fetchJson(
|
||||
`${backendBaseUrl}/api/historical/weekly-top-kills?server=${encodeURIComponent(activeServerSlug)}&limit=10`,
|
||||
),
|
||||
fetchJson(
|
||||
`${backendBaseUrl}/api/historical/recent-matches?server=${encodeURIComponent(activeServerSlug)}&limit=6`,
|
||||
),
|
||||
]);
|
||||
|
||||
hydrateSummary(summaryResult, summaryNode, rangeNode);
|
||||
hydrateWeeklyTopKills(
|
||||
topKillsResult,
|
||||
weeklyStateNode,
|
||||
weeklyTableNode,
|
||||
weeklyBodyNode,
|
||||
);
|
||||
hydrateRecentMatches(recentMatchesResult, recentStateNode, recentListNode);
|
||||
};
|
||||
|
||||
selectorButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const nextServerSlug = normalizeServerSlug(button.dataset.serverSlug);
|
||||
if (nextServerSlug === activeServerSlug) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeServerSlug = nextServerSlug;
|
||||
params.set("server", activeServerSlug);
|
||||
window.history.replaceState({}, "", `?${params.toString()}`);
|
||||
void refreshHistoricalView();
|
||||
});
|
||||
});
|
||||
|
||||
void refreshHistoricalView();
|
||||
});
|
||||
|
||||
function hydrateSummary(result, summaryNode, rangeNode) {
|
||||
if (result.status !== "fulfilled") {
|
||||
renderSummaryError(summaryNode);
|
||||
setRangeBadge(rangeNode, "Resumen historico no disponible", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = result.value?.data?.items;
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
renderSummaryEmpty(summaryNode);
|
||||
setRangeBadge(rangeNode, "Sin cobertura historica", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const summary = items[0];
|
||||
const timeRange = summary.time_range || {};
|
||||
const rangeLabel = buildRangeLabel(timeRange.start, timeRange.end);
|
||||
setRangeBadge(
|
||||
rangeNode,
|
||||
rangeLabel || "Cobertura parcial disponible",
|
||||
Boolean(rangeLabel),
|
||||
);
|
||||
summaryNode.innerHTML = [
|
||||
renderSummaryCard("Servidor", summary.server?.name || "Servidor no disponible"),
|
||||
renderSummaryCard("Partidas", formatNumber(summary.matches_count)),
|
||||
renderSummaryCard("Jugadores unicos", formatNumber(summary.unique_players)),
|
||||
renderSummaryCard("Kills agregadas", formatNumber(summary.total_kills)),
|
||||
renderSummaryCard(
|
||||
"Mapas dominantes",
|
||||
formatTopMaps(summary.top_maps),
|
||||
),
|
||||
].join("");
|
||||
}
|
||||
|
||||
function hydrateWeeklyTopKills(
|
||||
result,
|
||||
stateNode,
|
||||
tableNode,
|
||||
bodyNode,
|
||||
) {
|
||||
if (result.status !== "fulfilled") {
|
||||
setState(stateNode, "No se pudo cargar el ranking semanal.", true);
|
||||
tableNode.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const items = result.value?.data?.items;
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
setState(
|
||||
stateNode,
|
||||
"Sin datos historicos suficientes para construir el ranking semanal.",
|
||||
);
|
||||
tableNode.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
bodyNode.innerHTML = items
|
||||
.map(
|
||||
(item) => `
|
||||
<tr>
|
||||
<td class="historical-table__position">#${escapeHtml(item.ranking_position)}</td>
|
||||
<td>${escapeHtml(item.player?.name || "Jugador no identificado")}</td>
|
||||
<td>${escapeHtml(formatNumber(item.weekly_kills))}</td>
|
||||
<td>${escapeHtml(formatNumber(item.matches_considered))}</td>
|
||||
<td>${escapeHtml(item.server?.name || "Servidor no disponible")}</td>
|
||||
</tr>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
stateNode.hidden = true;
|
||||
tableNode.hidden = false;
|
||||
}
|
||||
|
||||
function hydrateRecentMatches(result, stateNode, listNode) {
|
||||
if (result.status !== "fulfilled") {
|
||||
setState(stateNode, "No se pudieron cargar las partidas recientes.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = result.value?.data?.items;
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
setState(stateNode, "Todavia no hay partidas recientes disponibles.");
|
||||
return;
|
||||
}
|
||||
|
||||
listNode.innerHTML = items.map((item) => renderRecentMatchCard(item)).join("");
|
||||
stateNode.hidden = true;
|
||||
}
|
||||
|
||||
function renderRecentMatchCard(item) {
|
||||
const mapName = item.map?.pretty_name || item.map?.name || "Mapa no disponible";
|
||||
return `
|
||||
<article class="historical-match-card">
|
||||
<div class="historical-match-card__top">
|
||||
<div>
|
||||
<p class="historical-match-meta__label">Partida ${escapeHtml(item.match_id || "sin id")}</p>
|
||||
<h3 class="historical-match-card__title">${escapeHtml(mapName)}</h3>
|
||||
</div>
|
||||
<span class="historical-match-card__result">${escapeHtml(formatMatchResult(item.result))}</span>
|
||||
</div>
|
||||
<div class="historical-match-meta">
|
||||
<article>
|
||||
<p class="historical-match-meta__label">Servidor</p>
|
||||
<strong>${escapeHtml(item.server?.name || "Servidor no disponible")}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<p class="historical-match-meta__label">Cierre</p>
|
||||
<strong>${escapeHtml(formatTimestamp(item.closed_at))}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<p class="historical-match-meta__label">Jugadores</p>
|
||||
<strong>${escapeHtml(formatNumber(item.player_count))}</strong>
|
||||
</article>
|
||||
<article>
|
||||
<p class="historical-match-meta__label">Marcador</p>
|
||||
<strong>${escapeHtml(formatScore(item.result))}</strong>
|
||||
</article>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSummaryLoading(summaryNode) {
|
||||
summaryNode.innerHTML = renderSummaryCard("Estado", "Cargando datos historicos");
|
||||
}
|
||||
|
||||
function renderSummaryError(summaryNode) {
|
||||
summaryNode.innerHTML = renderSummaryCard("Estado", "Error al cargar el resumen");
|
||||
}
|
||||
|
||||
function renderSummaryEmpty(summaryNode) {
|
||||
summaryNode.innerHTML = renderSummaryCard("Estado", "Sin datos historicos suficientes");
|
||||
}
|
||||
|
||||
function renderSummaryCard(label, value) {
|
||||
return `
|
||||
<article class="historical-stat-card">
|
||||
<p>${escapeHtml(label)}</p>
|
||||
<strong>${escapeHtml(value)}</strong>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function setState(node, message, isError = false) {
|
||||
node.textContent = message;
|
||||
node.hidden = false;
|
||||
node.classList.toggle("is-error", isError);
|
||||
}
|
||||
|
||||
function setRangeBadge(node, label, isFresh) {
|
||||
node.textContent = label;
|
||||
node.classList.toggle("status-chip--ok", isFresh);
|
||||
node.classList.toggle("status-chip--fallback", !isFresh);
|
||||
}
|
||||
|
||||
function syncActiveButtons(buttons, activeServerSlug) {
|
||||
buttons.forEach((button) => {
|
||||
button.classList.toggle(
|
||||
"is-active",
|
||||
button.dataset.serverSlug === activeServerSlug,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeServerSlug(rawValue) {
|
||||
const normalized = typeof rawValue === "string" ? rawValue.trim() : "";
|
||||
if (HISTORICAL_SERVER_SLUGS.includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return DEFAULT_HISTORICAL_SERVER;
|
||||
}
|
||||
|
||||
function buildRangeLabel(start, end) {
|
||||
if (!start && !end) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${formatTimestamp(start)} a ${formatTimestamp(end)}`;
|
||||
}
|
||||
|
||||
function formatTopMaps(topMaps) {
|
||||
if (!Array.isArray(topMaps) || topMaps.length === 0) {
|
||||
return "Sin mapas dominantes";
|
||||
}
|
||||
|
||||
return topMaps
|
||||
.map((item) => `${item.map_name} (${formatNumber(item.matches_count)})`)
|
||||
.join(" / ");
|
||||
}
|
||||
|
||||
function formatMatchResult(result) {
|
||||
const winner = result?.winner;
|
||||
if (winner === "allies") {
|
||||
return "Victoria Aliada";
|
||||
}
|
||||
if (winner === "axis") {
|
||||
return "Victoria Axis";
|
||||
}
|
||||
if (winner === "draw") {
|
||||
return "Empate";
|
||||
}
|
||||
return "Resultado parcial";
|
||||
}
|
||||
|
||||
function formatScore(result) {
|
||||
const alliedScore = Number.isFinite(result?.allied_score)
|
||||
? result.allied_score
|
||||
: "-";
|
||||
const axisScore = Number.isFinite(result?.axis_score)
|
||||
? result.axis_score
|
||||
: "-";
|
||||
return `${alliedScore} - ${axisScore}`;
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const parsedValue = Number(value);
|
||||
if (!Number.isFinite(parsedValue)) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat("es-ES").format(parsedValue);
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) {
|
||||
return "Fecha no disponible";
|
||||
}
|
||||
|
||||
const value = new Date(timestamp);
|
||||
if (Number.isNaN(value.getTime())) {
|
||||
return "Fecha no disponible";
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat("es-ES", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
124
frontend/historico.html
Normal file
124
frontend/historico.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Historico propio de HLL Vietnam con ranking semanal y partidas recientes por servidor."
|
||||
/>
|
||||
<title>Historico - HLL Vietnam</title>
|
||||
<link rel="stylesheet" href="./assets/css/styles.css" />
|
||||
<link rel="stylesheet" href="./assets/css/historico.css" />
|
||||
</head>
|
||||
<body data-backend-base-url="http://127.0.0.1:8000">
|
||||
<div class="page-shell historical-shell">
|
||||
<header class="hero historical-hero">
|
||||
<div class="hero__overlay"></div>
|
||||
<div class="hero__content historical-hero__content">
|
||||
<div class="historical-hero__topline">
|
||||
<a class="secondary-button secondary-button--ghost" href="./index.html">
|
||||
Volver a la landing
|
||||
</a>
|
||||
<p class="eyebrow">Historico propio</p>
|
||||
</div>
|
||||
<div class="historical-hero__copy">
|
||||
<div>
|
||||
<h1 class="historical-hero__title">
|
||||
Registro tactico
|
||||
<span class="hero__title-accent">Comunidad Hispana</span>
|
||||
</h1>
|
||||
<p class="hero__text historical-hero__text">
|
||||
Consulta el ranking semanal y las partidas recientes usando solo
|
||||
la capa historica interna del proyecto.
|
||||
</p>
|
||||
</div>
|
||||
<div class="historical-selector" aria-label="Seleccion de servidor">
|
||||
<button
|
||||
class="historical-selector__button is-active"
|
||||
type="button"
|
||||
data-server-slug="comunidad-hispana-01"
|
||||
>
|
||||
Comunidad Hispana #01
|
||||
</button>
|
||||
<button
|
||||
class="historical-selector__button"
|
||||
type="button"
|
||||
data-server-slug="comunidad-hispana-02"
|
||||
>
|
||||
Comunidad Hispana #02
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="content historical-content">
|
||||
<section class="panel historical-panel" aria-labelledby="historical-summary-title">
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header historical-panel__header">
|
||||
<div>
|
||||
<p class="eyebrow eyebrow--section">Cobertura</p>
|
||||
<h2 id="historical-summary-title">Resumen del servidor</h2>
|
||||
</div>
|
||||
<p class="status-chip status-chip--fallback" id="historical-range">
|
||||
Cargando rango temporal
|
||||
</p>
|
||||
</div>
|
||||
<div class="historical-summary-grid" id="historical-summary">
|
||||
<article class="historical-stat-card">
|
||||
<p>Estado</p>
|
||||
<strong>Cargando datos historicos</strong>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel historical-panel" aria-labelledby="weekly-ranking-title">
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header historical-panel__header">
|
||||
<div>
|
||||
<p class="eyebrow eyebrow--section">Ranking semanal</p>
|
||||
<h2 id="weekly-ranking-title">Top kills de los ultimos 7 dias</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="historical-state" id="weekly-top-kills-state" aria-live="polite">
|
||||
Cargando ranking semanal...
|
||||
</p>
|
||||
<div class="historical-table-shell">
|
||||
<table class="historical-table" id="weekly-top-kills-table" hidden>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Pos.</th>
|
||||
<th>Jugador</th>
|
||||
<th>Kills</th>
|
||||
<th>Partidas</th>
|
||||
<th>Servidor</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="weekly-top-kills-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel historical-panel" aria-labelledby="recent-matches-title">
|
||||
<div class="panel__shell">
|
||||
<div class="panel__header historical-panel__header">
|
||||
<div>
|
||||
<p class="eyebrow eyebrow--section">Partidas recientes</p>
|
||||
<h2 id="recent-matches-title">Ultimos cierres registrados</h2>
|
||||
</div>
|
||||
</div>
|
||||
<p class="historical-state" id="recent-matches-state" aria-live="polite">
|
||||
Cargando partidas recientes...
|
||||
</p>
|
||||
<div class="historical-match-list" id="recent-matches-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="./assets/js/historico.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -47,6 +47,9 @@
|
||||
>
|
||||
Unirse al Discord
|
||||
</a>
|
||||
<a class="secondary-button" href="./historico.html">
|
||||
Ver historico propio
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,6 +91,11 @@
|
||||
<p class="panel__intro panel__intro--tight">
|
||||
El panel muestra el ultimo snapshot disponible consultado desde backend.
|
||||
</p>
|
||||
<div class="panel__actions panel__actions--compact">
|
||||
<a class="secondary-button secondary-button--compact" href="./historico.html">
|
||||
Abrir zona historica del proyecto
|
||||
</a>
|
||||
</div>
|
||||
<div class="servers-grid" id="servers-list">
|
||||
<article class="server-card server-card--stats server-card--real">
|
||||
<div class="server-card__top server-card__top--stats">
|
||||
|
||||
Reference in New Issue
Block a user