feat: refine current match live feed

This commit is contained in:
devRaGonSa
2026-05-21 19:47:12 +02:00
parent 370e014b59
commit d558ac81ed
14 changed files with 330 additions and 152 deletions

View File

@@ -591,50 +591,41 @@
.current-match-killfeed {
display: grid;
grid-auto-flow: column;
grid-template-rows: repeat(5, minmax(0, 1fr));
grid-auto-columns: minmax(0, 1fr);
gap: 10px 12px;
min-height: 250px;
max-height: 320px;
padding: 16px;
gap: 10px;
max-height: 760px;
padding: 4px 0 0;
overflow: hidden;
border: 1px solid rgba(210, 182, 118, 0.26);
border-radius: 8px;
background:
linear-gradient(90deg, rgba(210, 182, 118, 0.08) 1px, transparent 1px),
linear-gradient(180deg, rgba(183, 201, 125, 0.06) 1px, transparent 1px),
linear-gradient(180deg, rgba(12, 18, 11, 0.96), rgba(7, 10, 7, 0.98));
background-size: 33.333% 100%, 100% 62px, auto;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.04),
0 18px 38px rgba(0, 0, 0, 0.22);
}
.current-match-killfeed__row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
grid-template-columns: minmax(0, 1fr) minmax(102px, 150px) minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
min-height: 38px;
padding: 7px 9px;
gap: 12px;
min-height: 56px;
padding: 12px 14px;
overflow: hidden;
border: 1px solid rgba(159, 168, 141, 0.16);
border: 1px solid rgba(159, 168, 141, 0.18);
border-left: 3px solid rgba(183, 201, 125, 0.34);
border-radius: 6px;
color: var(--text);
background: rgba(24, 31, 21, 0.86);
background:
linear-gradient(135deg, rgba(183, 201, 125, 0.08), transparent 44%),
rgba(14, 19, 12, 0.84);
animation: current-match-killfeed-enter 180ms ease-out;
}
.current-match-killfeed__row.is-teamkill {
border-color: rgba(210, 182, 118, 0.42);
background: rgba(73, 49, 27, 0.72);
border-left-color: rgba(210, 182, 118, 0.64);
background:
linear-gradient(135deg, rgba(210, 182, 118, 0.12), transparent 48%),
rgba(73, 49, 27, 0.42);
}
.current-match-killfeed__player {
min-width: 0;
overflow: hidden;
font-size: 0.84rem;
font-size: 0.94rem;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
@@ -649,20 +640,41 @@
}
.current-match-killfeed__weapon {
display: inline-flex;
display: grid;
grid-template-rows: 34px;
align-items: center;
justify-content: center;
gap: 5px;
min-width: 34px;
min-height: 24px;
padding: 0 6px;
min-width: 102px;
min-height: 40px;
padding: 2px 8px;
border: 1px solid rgba(183, 201, 125, 0.24);
border-radius: 4px;
color: var(--text);
background: rgba(7, 10, 7, 0.8);
font-size: 0.68rem;
}
.current-match-killfeed__weapon-icon {
display: block;
width: min(100%, 116px);
height: 32px;
object-fit: contain;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.72));
}
.current-match-killfeed__weapon-fallback {
display: grid;
width: 32px;
height: 32px;
place-items: center;
border: 1px solid rgba(210, 182, 118, 0.3);
border-radius: 4px;
color: var(--accent-warm);
font-size: 1rem;
font-weight: 800;
line-height: 1;
}
.current-match-killfeed__weapon-fallback[hidden] {
display: none;
}
.current-match-killfeed__weapon em {
@@ -920,14 +932,11 @@
}
.current-match-killfeed {
grid-template-rows: repeat(8, minmax(0, 1fr));
min-height: 312px;
max-height: 380px;
background-size: 50% 100%, 100% 48px, auto;
max-height: none;
}
.current-match-killfeed__row {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
grid-template-columns: minmax(0, 1fr) minmax(96px, 128px) minmax(0, 1fr);
}
.current-match-killfeed__teamkill {
@@ -937,12 +946,13 @@
}
@media (max-width: 480px) {
.current-match-killfeed {
grid-auto-flow: row;
grid-template-rows: none;
min-height: 0;
max-height: none;
background-size: 100% 48px, 100% 48px, auto;
.current-match-killfeed__row {
grid-template-columns: 1fr;
gap: 8px;
}
.current-match-killfeed__weapon {
justify-self: start;
}
}

View File

@@ -617,7 +617,7 @@ h2 {
}
.server-card__top--stats {
align-items: stretch;
align-items: flex-start;
gap: 18px;
}
@@ -669,9 +669,10 @@ h2 {
.server-card__actions {
margin: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
align-self: end;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
@@ -680,7 +681,7 @@ h2 {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
width: auto;
max-width: 100%;
min-height: 42px;
min-width: 136px;
@@ -742,6 +743,14 @@ h2 {
gap: 12px;
}
.server-card__bottom {
display: grid;
grid-template-columns: minmax(0, 280px) minmax(272px, 1fr);
align-items: end;
justify-content: space-between;
gap: 16px;
}
.server-card__quickfact {
min-width: 0;
padding: 12px 14px;
@@ -1019,13 +1028,17 @@ h2 {
}
.server-card__actions {
justify-content: start;
justify-content: flex-start;
}
.server-card__quickfacts {
grid-template-columns: 1fr;
}
.server-card__bottom {
grid-template-columns: 1fr;
}
.clan-card__brand {
grid-template-columns: 1fr;
justify-items: start;

View File

@@ -297,10 +297,12 @@ function renderServerStatsCard(server) {
<div class="server-card__status-column">
<span class="server-state ${stateClass}">${escapeHtml(statusLabel)}</span>
<p class="server-card__population">${escapeHtml(`${players} / ${maxPlayers}`)}</p>
${actionMarkup}
</div>
</div>
${quickFacts}
<div class="server-card__bottom">
${quickFacts}
${actionMarkup}
</div>
</article>
`;
}
@@ -342,16 +344,8 @@ function renderServerAction(server) {
return `
<div class="server-card__actions">
<a
class="server-action-link"
href="${escapeHtml(actions.publicScoreboardUrl)}"
target="_blank"
rel="noreferrer"
>
Scoreboard publico
</a>
<a class="server-action-link" href="${escapeHtml(actions.historicalUrl)}">
Nuestro historico
Historico
</a>
<a class="server-action-link" href="${escapeHtml(actions.currentMatchUrl)}">
Partida actual

View File

@@ -1,4 +1,5 @@
const CURRENT_MATCH_POLL_INTERVAL_MS = 30 * 1000;
const CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS = 1500;
const CURRENT_MATCH_SERVERS = Object.freeze({
"comunidad-hispana-01": "Comunidad Hispana #01",
"comunidad-hispana-02": "Comunidad Hispana #02",
@@ -9,10 +10,14 @@ const CURRENT_MATCH_SCOREBOARDS = Object.freeze({
});
const CURRENT_MATCH_KILL_FEED_LIMIT = 15;
const CURRENT_MATCH_WEAPONS = Object.freeze({
"m1 garand": { label: "M1 Garand", glyph: "M1" },
mp40: { label: "MP40", glyph: "MP" },
"m1a1 thompson": { label: "M1A1 Thompson", glyph: "TH" },
unknown: { label: "Arma desconocida", glyph: "?" },
"m1 garand": { label: "M1 Garand", icon: "m1_garand.PNG" },
mp40: { label: "MP40", icon: "mp40.PNG" },
"m1a1 thompson": { label: "M1A1 Thompson", icon: "thompson.PNG" },
thompson: { label: "M1A1 Thompson", icon: "thompson.PNG" },
"gewehr 43": { label: "Gewehr 43", icon: "gewehr.PNG" },
g43: { label: "Gewehr 43", icon: "gewehr.PNG" },
mg42: { label: "MG42", icon: "mg42.PNG" },
unknown: { label: "Arma desconocida", icon: "" },
});
document.addEventListener("DOMContentLoaded", () => {
@@ -43,27 +48,43 @@ document.addEventListener("DOMContentLoaded", () => {
nodes.history.href = `./historico.html?server=${encodeURIComponent(serverSlug)}`;
const killFeedState = initializeKillFeed(nodes);
const playerStatsState = initializePlayerStats(nodes);
let refreshInFlight = false;
const refresh = async () => {
if (refreshInFlight) {
let matchRefreshInFlight = false;
const refreshMatchPanels = async () => {
if (matchRefreshInFlight) {
return;
}
refreshInFlight = true;
matchRefreshInFlight = true;
try {
await Promise.allSettled([
loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }),
loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }),
loadPlayerStats({ backendBaseUrl, serverSlug, nodes, playerStatsState }),
]);
} finally {
refreshInFlight = false;
matchRefreshInFlight = false;
}
};
void refresh();
let killFeedRefreshInFlight = false;
const refreshKillFeed = async () => {
if (killFeedRefreshInFlight) {
return;
}
killFeedRefreshInFlight = true;
try {
await loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState });
} finally {
killFeedRefreshInFlight = false;
}
};
void refreshMatchPanels();
void refreshKillFeed();
window.setInterval(() => {
void refresh();
void refreshMatchPanels();
}, CURRENT_MATCH_POLL_INTERVAL_MS);
window.setInterval(() => {
void refreshKillFeed();
}, CURRENT_MATCH_KILL_FEED_POLL_INTERVAL_MS);
});
async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) {
@@ -80,8 +101,11 @@ async function loadCurrentMatch({ backendBaseUrl, serverSlug, nodes }) {
async function loadKillFeed({ backendBaseUrl, serverSlug, nodes, killFeedState }) {
try {
const cursor = killFeedState.latestEventId
? `&since_event_id=${encodeURIComponent(killFeedState.latestEventId)}`
: "";
const payload = await fetchJson(
`${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}`,
`${backendBaseUrl}/api/current-match/kills?server=${encodeURIComponent(serverSlug)}&limit=${CURRENT_MATCH_KILL_FEED_LIMIT}${cursor}`,
);
renderKillFeed(payload?.data || {}, nodes, killFeedState);
} catch (error) {
@@ -108,7 +132,7 @@ function renderCurrentMatch(data, nodes) {
nodes.title.textContent = mapName;
nodes.summary.textContent = serverName;
nodes.note.textContent = data.found
? "Lectura en vivo recibida. La pagina se actualiza cada 30 segundos."
? "Lectura en vivo recibida. El feed de bajas se actualiza en tiempo casi real."
: "Todavia no hay snapshot live disponible para este servidor.";
nodes.scoreboard.href = scoreboardUrl || "./index.html";
nodes.scoreboard.hidden = !scoreboardUrl;
@@ -150,6 +174,7 @@ function initializeKillFeed(nodes) {
nodes.feedList = document.getElementById("current-match-feed-list");
return {
byId: new Map(),
latestEventId: "",
visibleSignature: "",
};
}
@@ -178,6 +203,7 @@ function renderKillFeed(data, nodes, state) {
const incoming = Array.isArray(data.items) ? data.items : [];
if (data.scope === "no-current-match-events") {
state.byId.clear();
state.latestEventId = "";
}
incoming.forEach((event) => {
if (event?.event_id) {
@@ -187,6 +213,8 @@ function renderKillFeed(data, nodes, state) {
const events = [...state.byId.values()]
.sort(compareKillFeedEvents)
.slice(0, CURRENT_MATCH_KILL_FEED_LIMIT);
state.byId = new Map(events.map((event) => [event.event_id, event]));
state.latestEventId = events[0]?.event_id || state.latestEventId;
if (events.length === 0) {
nodes.feedList.innerHTML = "";
state.visibleSignature = "";
@@ -227,7 +255,7 @@ function renderKillFeedRow(event) {
title="${escapeHtml(weapon.label)}"
aria-label="${escapeHtml(weapon.label)}"
>
<span aria-hidden="true">${escapeHtml(weapon.glyph)}</span>
${renderKillFeedWeaponIcon(weapon)}
<em>${escapeHtml(weapon.label)}</em>
</span>
<span class="current-match-killfeed__player current-match-killfeed__player--victim">
@@ -242,10 +270,29 @@ function resolveKillFeedWeapon(value) {
const key = normalizeLookupText(value);
return CURRENT_MATCH_WEAPONS[key] || {
label: String(value || CURRENT_MATCH_WEAPONS.unknown.label),
glyph: CURRENT_MATCH_WEAPONS.unknown.glyph,
icon: CURRENT_MATCH_WEAPONS.unknown.icon,
};
}
function renderKillFeedWeaponIcon(weapon) {
if (!weapon.icon) {
return '<span class="current-match-killfeed__weapon-fallback" aria-hidden="true">?</span>';
}
return `
<img
class="current-match-killfeed__weapon-icon"
src="./assets/img/weapons/${escapeHtml(weapon.icon)}"
alt=""
width="88"
height="32"
loading="lazy"
decoding="async"
onerror="this.hidden = true; this.nextElementSibling.hidden = false;"
/>
<span class="current-match-killfeed__weapon-fallback" aria-hidden="true" hidden>?</span>
`;
}
function renderPlayerStats(data, nodes, state) {
const items = Array.isArray(data.items) ? sortPlayerStats(data.items) : [];
if (items.length === 0) {
@@ -262,6 +309,7 @@ function renderPlayerStats(data, nodes, state) {
.map((item) =>
[
item.player_name,
item.team,
item.kills,
item.deaths,
item.teamkills,
@@ -312,10 +360,15 @@ function renderPlayerStatsTable(items) {
}
function renderPlayerStatsRow(item) {
const team = getPlayerTeamDisplay(item.team);
return `
<tr>
<tr class="historical-player-row historical-player-row--${team.key}">
<td>${escapeHtml(item.player_name || "Jugador no disponible")}</td>
<td>${escapeHtml(item.team || "No disponible")}</td>
<td class="historical-player-team-cell">
<span class="historical-player-team-badge historical-player-team-badge--${team.key}">
${escapeHtml(team.label)}
</span>
</td>
<td>${escapeHtml(formatStatNumber(item.kills))}</td>
<td>${escapeHtml(formatStatNumber(item.deaths))}</td>
<td>${escapeHtml(formatStatNumber(item.teamkills))}</td>
@@ -325,6 +378,17 @@ function renderPlayerStatsRow(item) {
`;
}
function getPlayerTeamDisplay(value) {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "allies" || normalized === "allied" || normalized === "aliados") {
return { key: "allies", label: "Aliados" };
}
if (normalized === "axis" || normalized === "eje") {
return { key: "axis", label: "Eje" };
}
return { key: "unknown", label: "No disponible" };
}
function toStatNumber(value) {
return Number.isFinite(Number(value)) ? Number(value) : 0;
}