fix: actually render scoreboard match detail layout
This commit is contained in:
@@ -25,7 +25,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
if (!serverSlug || !matchId) {
|
if (!serverSlug || !matchId) {
|
||||||
nodes.title.textContent = "Partida no seleccionada";
|
nodes.title.textContent = "Partida no seleccionada";
|
||||||
nodes.summary.textContent = "Vuelve al historico y abre una partida registrada.";
|
nodes.summary.textContent = "Vuelve al historico y abre una partida registrada.";
|
||||||
nodes.note.textContent = "Faltan parametros internos para cargar este detalle.";
|
nodes.note.textContent = "";
|
||||||
setState(nodes.state, "No hay una partida seleccionada.", true);
|
setState(nodes.state, "No hay una partida seleccionada.", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -46,8 +46,7 @@ async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) {
|
|||||||
nodes.title.textContent = "Detalle no disponible";
|
nodes.title.textContent = "Detalle no disponible";
|
||||||
nodes.summary.textContent =
|
nodes.summary.textContent =
|
||||||
"La partida existe como enlace interno, pero todavia no hay detalle suficiente para mostrar.";
|
"La partida existe como enlace interno, pero todavia no hay detalle suficiente para mostrar.";
|
||||||
nodes.note.textContent =
|
nodes.note.textContent = "";
|
||||||
"El historico local puede tener solo una ventana RCON parcial o ningun registro ampliado.";
|
|
||||||
setState(nodes.state, "Detalle no disponible para esta partida.");
|
setState(nodes.state, "Detalle no disponible para esta partida.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,7 +55,7 @@ async function loadMatchDetail({ backendBaseUrl, serverSlug, matchId, nodes }) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
nodes.title.textContent = "Detalle no disponible";
|
nodes.title.textContent = "Detalle no disponible";
|
||||||
nodes.summary.textContent = "No se pudo conectar con el backend local.";
|
nodes.summary.textContent = "No se pudo conectar con el backend local.";
|
||||||
nodes.note.textContent = "Comprueba que el backend este levantado y vuelve a intentarlo.";
|
nodes.note.textContent = "";
|
||||||
setState(nodes.state, "Error al cargar el detalle de la partida.", true);
|
setState(nodes.state, "Error al cargar el detalle de la partida.", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,31 +65,81 @@ function renderMatchDetail(item, nodes) {
|
|||||||
const serverName = item.server?.name || item.server?.slug || "Servidor no disponible";
|
const serverName = item.server?.name || item.server?.slug || "Servidor no disponible";
|
||||||
nodes.title.textContent = mapName;
|
nodes.title.textContent = mapName;
|
||||||
nodes.summary.textContent = `${serverName} - ${formatDetailSubtitle(item)}`;
|
nodes.summary.textContent = `${serverName} - ${formatDetailSubtitle(item)}`;
|
||||||
nodes.note.textContent = buildDetailNote(item);
|
nodes.note.textContent = "";
|
||||||
nodes.grid.innerHTML = [
|
nodes.grid.innerHTML = renderScoreboardDetail(item, { mapName, serverName });
|
||||||
renderDetailCard("Servidor", serverName),
|
|
||||||
renderDetailCard("Mapa", mapName),
|
|
||||||
renderDetailCard("Modo", formatGameMode(item.game_mode || item.gamestate?.game_mode)),
|
|
||||||
renderDetailCard("Marcador", formatScore(item.result)),
|
|
||||||
renderDetailCard("Ganador", formatWinner(item.winner || item.result?.winner)),
|
|
||||||
renderDetailCard("Resultado", formatMatchResult(item.result)),
|
|
||||||
renderDetailCard("Inicio", formatMatchTimestamp(item, "start")),
|
|
||||||
renderDetailCard("Fin", formatMatchTimestamp(item, "end")),
|
|
||||||
renderDetailCard("Duracion", formatDuration(item.duration_seconds)),
|
|
||||||
renderDetailCard("Confianza", formatConfidence(item.confidence)),
|
|
||||||
renderDetailCard(
|
|
||||||
"Fuente",
|
|
||||||
formatSourceBasis(item.source_basis || item.result_source) || "No disponible",
|
|
||||||
),
|
|
||||||
renderDetailCard("Base", formatCaptureBasis(item.capture_basis)),
|
|
||||||
].join("");
|
|
||||||
renderPlayerSection(item, nodes);
|
renderPlayerSection(item, nodes);
|
||||||
renderTimelineSection(item, nodes);
|
hideTimelineSection(nodes);
|
||||||
renderActions(item, nodes.actions);
|
renderActions(item, nodes.actions);
|
||||||
nodes.state.hidden = true;
|
nodes.state.hidden = true;
|
||||||
nodes.grid.hidden = false;
|
nodes.grid.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderScoreboardDetail(item, { mapName, serverName }) {
|
||||||
|
const result = item.result || {};
|
||||||
|
const alliedScore = Number.isFinite(Number(result.allied_score))
|
||||||
|
? formatNumber(result.allied_score)
|
||||||
|
: "-";
|
||||||
|
const axisScore = Number.isFinite(Number(result.axis_score))
|
||||||
|
? formatNumber(result.axis_score)
|
||||||
|
: "-";
|
||||||
|
const winner = String(item.winner || result.winner || "").toLowerCase();
|
||||||
|
const isAlliedWinner = winner === "allies" || winner === "allied";
|
||||||
|
const isAxisWinner = winner === "axis";
|
||||||
|
const metadata = [
|
||||||
|
["Servidor", serverName],
|
||||||
|
["Mapa", mapName],
|
||||||
|
["Modo", formatGameMode(item.game_mode || item.gamestate?.game_mode)],
|
||||||
|
["Duracion", formatDuration(item.duration_seconds)],
|
||||||
|
["Inicio", formatMatchTimestamp(item, "start")],
|
||||||
|
];
|
||||||
|
if (item.ended_at) {
|
||||||
|
metadata.push(["Fin", formatMatchTimestamp(item, "end")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<section class="historical-scoreboard-detail" aria-label="Marcador de la partida">
|
||||||
|
<div class="historical-scoreboard-detail__side ${isAlliedWinner ? "is-winner" : ""}">
|
||||||
|
<span>Bando aliado</span>
|
||||||
|
<strong>Aliados</strong>
|
||||||
|
<em>${isAlliedWinner ? "Ganador" : ""}</em>
|
||||||
|
</div>
|
||||||
|
<div class="historical-scoreboard-detail__score">
|
||||||
|
<span>Marcador</span>
|
||||||
|
<strong>${escapeHtml(alliedScore)} - ${escapeHtml(axisScore)}</strong>
|
||||||
|
<em>${escapeHtml(formatWinner(winner))}</em>
|
||||||
|
</div>
|
||||||
|
<div class="historical-scoreboard-detail__side historical-scoreboard-detail__side--axis ${isAxisWinner ? "is-winner" : ""}">
|
||||||
|
<span>Bando eje</span>
|
||||||
|
<strong>Eje</strong>
|
||||||
|
<em>${isAxisWinner ? "Ganador" : ""}</em>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="historical-match-compact-meta" aria-label="Datos principales de la partida">
|
||||||
|
${metadata.map(([label, value]) => renderCompactMeta(label, value)).join("")}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCompactMeta(label, value) {
|
||||||
|
return `
|
||||||
|
<article>
|
||||||
|
<span>${escapeHtml(label)}</span>
|
||||||
|
<strong>${escapeHtml(value || "No disponible")}</strong>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideTimelineSection(nodes) {
|
||||||
|
if (!nodes.timelineSection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nodes.timelineSection.hidden = true;
|
||||||
|
nodes.timelineNote.textContent = "";
|
||||||
|
nodes.timelineState.hidden = true;
|
||||||
|
nodes.timelineGrid.hidden = true;
|
||||||
|
nodes.timelineGrid.innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
function renderPlayerSection(item, nodes) {
|
function renderPlayerSection(item, nodes) {
|
||||||
const players = Array.isArray(item.players) ? item.players : [];
|
const players = Array.isArray(item.players) ? item.players : [];
|
||||||
nodes.playersSection.hidden = false;
|
nodes.playersSection.hidden = false;
|
||||||
@@ -128,37 +177,6 @@ function renderPlayerRow(player) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTimelineSection(item, nodes) {
|
|
||||||
const eventCounts = Array.isArray(item.timeline?.event_counts)
|
|
||||||
? item.timeline.event_counts
|
|
||||||
: [];
|
|
||||||
nodes.timelineSection.hidden = false;
|
|
||||||
if (eventCounts.length === 0) {
|
|
||||||
nodes.timelineNote.textContent =
|
|
||||||
"No hay resumen de eventos disponible para esta partida.";
|
|
||||||
setState(nodes.timelineState, "Sin eventos agregados para mostrar.");
|
|
||||||
nodes.timelineGrid.hidden = true;
|
|
||||||
nodes.timelineGrid.innerHTML = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.timelineNote.textContent = `${formatNumber(eventCounts.length)} tipos de evento registrados.`;
|
|
||||||
nodes.timelineState.hidden = true;
|
|
||||||
nodes.timelineGrid.innerHTML = eventCounts.map((event) => renderTimelineCard(event)).join("");
|
|
||||||
nodes.timelineGrid.hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTimelineCard(event) {
|
|
||||||
const label =
|
|
||||||
event.event_type ||
|
|
||||||
event.type ||
|
|
||||||
event.name ||
|
|
||||||
event.label ||
|
|
||||||
"Evento registrado";
|
|
||||||
const count = event.count ?? event.total ?? event.event_count ?? 0;
|
|
||||||
return renderDetailCard(formatEventType(label), formatNumber(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderActions(item, actionsNode) {
|
function renderActions(item, actionsNode) {
|
||||||
const matchUrl = normalizeExternalMatchUrl(item.match_url);
|
const matchUrl = normalizeExternalMatchUrl(item.match_url);
|
||||||
if (!matchUrl) {
|
if (!matchUrl) {
|
||||||
@@ -179,23 +197,6 @@ function renderActions(item, actionsNode) {
|
|||||||
actionsNode.hidden = false;
|
actionsNode.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDetailCard(label, value) {
|
|
||||||
return `
|
|
||||||
<article class="historical-stat-card">
|
|
||||||
<p>${escapeHtml(label)}</p>
|
|
||||||
<strong>${escapeHtml(value)}</strong>
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildDetailNote(item) {
|
|
||||||
const source = formatSourceBasis(item.source_basis || item.result_source);
|
|
||||||
if (source) {
|
|
||||||
return `Detalle servido desde ${source}. Los campos sin cobertura fiable se muestran como no disponibles.`;
|
|
||||||
}
|
|
||||||
return "Detalle servido desde el historico local disponible para esta partida.";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDetailSubtitle(item) {
|
function formatDetailSubtitle(item) {
|
||||||
if (item.capture_basis === "rcon-materialized-admin-log") {
|
if (item.capture_basis === "rcon-materialized-admin-log") {
|
||||||
return "Partida RCON materializada";
|
return "Partida RCON materializada";
|
||||||
@@ -209,52 +210,13 @@ function formatDetailSubtitle(item) {
|
|||||||
return "Partida historica";
|
return "Partida historica";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCaptureBasis(value) {
|
|
||||||
if (value === "rcon-materialized-admin-log") {
|
|
||||||
return "Registro RCON materializado";
|
|
||||||
}
|
|
||||||
if (value === "rcon-competitive-window") {
|
|
||||||
return "Ventana competitiva RCON";
|
|
||||||
}
|
|
||||||
if (value === "public-scoreboard-match") {
|
|
||||||
return "Scoreboard persistido";
|
|
||||||
}
|
|
||||||
return value ? String(value).replaceAll("-", " ") : "Historico local";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatSourceBasis(value) {
|
|
||||||
if (value === "admin-log-match-ended") {
|
|
||||||
return "cierre RCON confirmado";
|
|
||||||
}
|
|
||||||
if (value === "rcon-session") {
|
|
||||||
return "sesion RCON registrada";
|
|
||||||
}
|
|
||||||
if (value === "public-scoreboard-match") {
|
|
||||||
return "scoreboard externo";
|
|
||||||
}
|
|
||||||
return value ? String(value).replaceAll("-", " ") : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatConfidence(value) {
|
|
||||||
if (value === "exact") {
|
|
||||||
return "Exacta";
|
|
||||||
}
|
|
||||||
if (value === "approximate") {
|
|
||||||
return "Aproximada";
|
|
||||||
}
|
|
||||||
if (value === "partial") {
|
|
||||||
return "Parcial";
|
|
||||||
}
|
|
||||||
return value ? String(value).replaceAll("-", " ") : "No disponible";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTeamSide(value) {
|
function formatTeamSide(value) {
|
||||||
const normalized = String(value || "").toLowerCase();
|
const normalized = String(value || "").toLowerCase();
|
||||||
if (normalized === "allies" || normalized === "allied") {
|
if (normalized === "allies" || normalized === "allied") {
|
||||||
return "Aliados";
|
return "Aliados";
|
||||||
}
|
}
|
||||||
if (normalized === "axis") {
|
if (normalized === "axis") {
|
||||||
return "Axis";
|
return "Eje";
|
||||||
}
|
}
|
||||||
return value || "No disponible";
|
return value || "No disponible";
|
||||||
}
|
}
|
||||||
@@ -281,39 +243,18 @@ function formatDuration(value) {
|
|||||||
return `${formatNumber(hours)} h ${formatNumber(remainingMinutes)} min`;
|
return `${formatNumber(hours)} h ${formatNumber(remainingMinutes)} min`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMatchResult(result) {
|
|
||||||
if (hasMatchScore(result)) {
|
|
||||||
return `${formatWinner(result.winner)} (${formatScore(result)})`;
|
|
||||||
}
|
|
||||||
return result?.winner ? formatWinner(result.winner) : "Resultado no disponible";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatWinner(value) {
|
function formatWinner(value) {
|
||||||
const normalized = String(value || "").toLowerCase();
|
const normalized = String(value || "").toLowerCase();
|
||||||
if (normalized === "allies" || normalized === "allied") {
|
if (normalized === "allies" || normalized === "allied") {
|
||||||
return "Aliados";
|
return "Ganador: Aliados";
|
||||||
}
|
}
|
||||||
if (normalized === "axis") {
|
if (normalized === "axis") {
|
||||||
return "Axis";
|
return "Ganador: Eje";
|
||||||
}
|
}
|
||||||
if (normalized === "draw") {
|
if (normalized === "draw") {
|
||||||
return "Empate";
|
return "Empate";
|
||||||
}
|
}
|
||||||
return "No disponible";
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatScore(result) {
|
|
||||||
if (!hasMatchScore(result)) {
|
|
||||||
return "Resultado no disponible";
|
return "Resultado no disponible";
|
||||||
}
|
|
||||||
return `${Number(result.allied_score)} - ${Number(result.axis_score)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasMatchScore(result) {
|
|
||||||
return (
|
|
||||||
Number.isFinite(Number(result?.allied_score)) &&
|
|
||||||
Number.isFinite(Number(result?.axis_score))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOptionalNumber(value) {
|
function formatOptionalNumber(value) {
|
||||||
@@ -346,14 +287,6 @@ function formatNamedCounts(items) {
|
|||||||
.join(" / ");
|
.join(" / ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatEventType(value) {
|
|
||||||
const normalized = String(value || "").replaceAll("_", " ").replaceAll("-", " ");
|
|
||||||
if (!normalized) {
|
|
||||||
return "Evento";
|
|
||||||
}
|
|
||||||
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNumber(value) {
|
function formatNumber(value) {
|
||||||
const parsedValue = Number(value);
|
const parsedValue = Number(value);
|
||||||
if (!Number.isFinite(parsedValue)) {
|
if (!Number.isFinite(parsedValue)) {
|
||||||
@@ -394,9 +327,6 @@ function formatMatchTimestamp(item, kind) {
|
|||||||
if (timestamp) {
|
if (timestamp) {
|
||||||
return formatTimestamp(timestamp);
|
return formatTimestamp(timestamp);
|
||||||
}
|
}
|
||||||
if (item.timestamp_confidence === "server-time-only") {
|
|
||||||
return "No disponible";
|
|
||||||
}
|
|
||||||
return "No disponible";
|
return "No disponible";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user