feat: refine current match live feed
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,65 +9,6 @@
|
||||
/>
|
||||
<title>Comunidad Hispana - HLL Vietnam</title>
|
||||
<link rel="stylesheet" href="./assets/css/styles.css" />
|
||||
<style>
|
||||
.server-card--stats {
|
||||
min-height: 385px;
|
||||
}
|
||||
|
||||
.server-card__top--stats {
|
||||
min-height: 255px;
|
||||
}
|
||||
|
||||
.server-card__status-column {
|
||||
min-width: min(190px, 44%);
|
||||
align-self: stretch;
|
||||
align-content: start;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.server-card__actions {
|
||||
align-self: end;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.server-card__actions .server-action-link:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.server-card__actions .server-action-link:nth-child(2) {
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.server-card__actions .server-action-link:nth-child(2)::before {
|
||||
content: "Historico";
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.server-card__quickfacts {
|
||||
max-width: min(280px, 48%);
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.server-card--stats,
|
||||
.server-card__top--stats {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.server-card__status-column {
|
||||
min-width: 150px;
|
||||
align-self: start;
|
||||
grid-template-rows: none;
|
||||
}
|
||||
|
||||
.server-card__actions {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.server-card__quickfacts {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
data-backend-base-url="http://127.0.0.1:8000"
|
||||
@@ -181,4 +122,4 @@
|
||||
|
||||
<script src="./assets/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user