Merge remote-tracking branch 'origin/codex/match-detail-player-table-controls'
This commit is contained in:
111
ai/tasks/done/TASK-145-match-detail-player-table-controls.md
Normal file
111
ai/tasks/done/TASK-145-match-detail-player-table-controls.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
id: TASK-145
|
||||||
|
title: Add match detail player table controls
|
||||||
|
status: done
|
||||||
|
type: frontend
|
||||||
|
team: Frontend Senior
|
||||||
|
supporting_teams: []
|
||||||
|
roadmap_item: foundation
|
||||||
|
priority: medium
|
||||||
|
---
|
||||||
|
|
||||||
|
# TASK-145 - Add match detail player table controls
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Keep inactive match-detail player rows low priority and add compact search, team
|
||||||
|
filtering and sorting controls to the player table.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The internal match detail page already exposes a compact player stat table and
|
||||||
|
expandable detail panels. Profile and lobby snapshot rows with no team and no
|
||||||
|
match activity can currently compete with real participants in that table, and
|
||||||
|
larger matches need client-side controls to find and compare players without
|
||||||
|
changing backend contracts.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. Inspect the listed files first.
|
||||||
|
2. Apply only the scoped match-detail player table change.
|
||||||
|
3. Validate the result and document relevant findings.
|
||||||
|
|
||||||
|
## Files to Read First
|
||||||
|
|
||||||
|
- `ai/architecture-index.md`
|
||||||
|
- `ai/repo-context.md`
|
||||||
|
- `ai/orchestrator/frontend-senior.md`
|
||||||
|
- `frontend/historico-partida.html`
|
||||||
|
- `frontend/assets/js/historico-partida.js`
|
||||||
|
- `frontend/assets/css/historico-scoreboard-detail.css`
|
||||||
|
|
||||||
|
## Expected Files to Modify
|
||||||
|
|
||||||
|
- `frontend/historico-partida.html`
|
||||||
|
- `frontend/assets/js/historico-partida.js`
|
||||||
|
- `frontend/assets/css/historico-scoreboard-detail.css`
|
||||||
|
- `ai/tasks/done/TASK-145-match-detail-player-table-controls.md`
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
- Preserve the compact main columns and click-to-open player detail panels.
|
||||||
|
- Keep Aliados and Eje row treatments and badges.
|
||||||
|
- Filter team labels through the existing localized team mapping.
|
||||||
|
- Do not reintroduce expanded stats, event data or backend changes as table columns.
|
||||||
|
- Keep frontend behavior responsive and browser-loaded without new dependencies.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `node --check frontend/assets/js/historico-partida.js`
|
||||||
|
- `node --check frontend/assets/js/historico.js`
|
||||||
|
- `node --check frontend/assets/js/historico-recent-live.js`
|
||||||
|
- `docker compose build --no-cache frontend`
|
||||||
|
- `docker compose up -d frontend`
|
||||||
|
- Manual match-detail verification for default inactive ordering, stat sorting,
|
||||||
|
name search, team filters and panel close-on-filter behavior.
|
||||||
|
- Review `git diff --name-only` against task scope.
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Implemented. The match-detail player table now has client-side player-name
|
||||||
|
search, localized team filtering, sort-column and direction controls above the
|
||||||
|
existing compact table. Default row ordering keeps active participants first,
|
||||||
|
orders them by kills descending, deaths ascending and name, and sends rows with
|
||||||
|
unknown team, zero combat stats and no expanded match stats to the bottom.
|
||||||
|
Numeric explicit sorts retain that inactive demotion, while name/team sorts can
|
||||||
|
compare all visible rows and inactive rows stay visually muted.
|
||||||
|
|
||||||
|
The table continues to render row/detail pairs together after control changes.
|
||||||
|
Player-name clicks still open the existing expanded detail panel, and search or
|
||||||
|
team-filter changes close any open panel before the visible rows refresh.
|
||||||
|
|
||||||
|
Validation passed:
|
||||||
|
|
||||||
|
- `node --check frontend/assets/js/historico-partida.js`
|
||||||
|
- `node --check frontend/assets/js/historico.js`
|
||||||
|
- `node --check frontend/assets/js/historico-recent-live.js`
|
||||||
|
- `docker compose build --no-cache frontend`
|
||||||
|
- `docker compose up -d frontend`
|
||||||
|
- Rendered fallback verification on
|
||||||
|
`historico-partida.html?server=comunidad-hispana-02&match=comunidad-hispana-02%3A1779315955%3A1779319098%3Akharkovwarfare`
|
||||||
|
for control visibility, row detail opening, close-on-search, player-name
|
||||||
|
search, Aliados/Eje/Sin equipo filters and Jugador/Equipo/K/D/TK/K-D/KPM
|
||||||
|
sort selectors.
|
||||||
|
- Rendered default ordering on the provided match showed 18 active rows before
|
||||||
|
44 inactive/no-team rows; visible zero-kill rows with deaths stayed in the
|
||||||
|
active block before the inactive rows.
|
||||||
|
- Desktop and mobile controls screenshots were inspected outside the repo.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- Browser plugin runtime tools were not exposed by tool discovery in this
|
||||||
|
session, so rendered validation used a temporary local headless
|
||||||
|
Chrome/Selenium fallback outside the repository.
|
||||||
|
- Browser console output on the tested route only reported the existing missing
|
||||||
|
`favicon.ico` request.
|
||||||
|
|
||||||
|
## Change Budget
|
||||||
|
|
||||||
|
- Prefer fewer than 5 modified files.
|
||||||
|
- Prefer changes under 200 lines when feasible.
|
||||||
|
- Split the work into follow-up tasks if limits are exceeded.
|
||||||
@@ -38,10 +38,66 @@
|
|||||||
min-width: 760px;
|
min-width: 760px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historical-player-controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 1fr) repeat(3, minmax(150px, 190px));
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-controls[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.66rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control input,
|
||||||
|
.historical-player-control select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid rgba(159, 168, 141, 0.28);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(10, 13, 10, 0.82);
|
||||||
|
color: var(--text);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control input::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control input:focus-visible,
|
||||||
|
.historical-player-control select:focus-visible {
|
||||||
|
outline: 2px solid rgba(210, 182, 118, 0.72);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
.historical-player-row {
|
.historical-player-row {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.historical-player-row.is-inactive {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-row.is-inactive .historical-player-row__details-button span:first-child {
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.historical-player-row:hover td,
|
.historical-player-row:hover td,
|
||||||
.historical-player-row:focus-within td,
|
.historical-player-row:focus-within td,
|
||||||
.historical-player-row.is-expanded td {
|
.historical-player-row.is-expanded td {
|
||||||
@@ -302,6 +358,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
|
.historical-player-controls {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control--search {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.historical-player-stats-panel__header,
|
.historical-player-stats-panel__header,
|
||||||
.historical-player-stats-panel__grid {
|
.historical-player-stats-panel__grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
playersSection: document.getElementById("match-detail-players-section"),
|
playersSection: document.getElementById("match-detail-players-section"),
|
||||||
playersNote: document.getElementById("match-detail-players-note"),
|
playersNote: document.getElementById("match-detail-players-note"),
|
||||||
playersState: document.getElementById("match-detail-players-state"),
|
playersState: document.getElementById("match-detail-players-state"),
|
||||||
|
playerControls: document.getElementById("match-detail-player-controls"),
|
||||||
|
playerSearch: document.getElementById("match-detail-player-search"),
|
||||||
|
playerTeamFilter: document.getElementById("match-detail-player-team-filter"),
|
||||||
|
playerSort: document.getElementById("match-detail-player-sort"),
|
||||||
|
playerSortDirection: document.getElementById("match-detail-player-sort-direction"),
|
||||||
playersTableShell: document.getElementById("match-detail-players-table-shell"),
|
playersTableShell: document.getElementById("match-detail-players-table-shell"),
|
||||||
playersBody: document.getElementById("match-detail-players-body"),
|
playersBody: document.getElementById("match-detail-players-body"),
|
||||||
timelineSection: document.getElementById("match-detail-timeline-section"),
|
timelineSection: document.getElementById("match-detail-timeline-section"),
|
||||||
@@ -206,29 +211,159 @@ function renderPlayerSection(item, nodes) {
|
|||||||
nodes.playersState,
|
nodes.playersState,
|
||||||
"No hay filas de jugador registradas para esta partida.",
|
"No hay filas de jugador registradas para esta partida.",
|
||||||
);
|
);
|
||||||
|
nodes.playerControls.hidden = true;
|
||||||
nodes.playersTableShell.hidden = true;
|
nodes.playersTableShell.hidden = true;
|
||||||
nodes.playersBody.innerHTML = "";
|
nodes.playersBody.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.playersNote.textContent = `${formatNumber(players.length)} jugadores con estadisticas locales.`;
|
const state = {
|
||||||
nodes.playersState.hidden = true;
|
search: "",
|
||||||
nodes.playersBody.innerHTML = players
|
team: "all",
|
||||||
.map((player, index) => renderPlayerRows(player, item, index))
|
sort: "kills",
|
||||||
.join("");
|
direction: "desc",
|
||||||
bindPlayerDetailRows(nodes.playersBody);
|
isDefaultSort: true,
|
||||||
|
};
|
||||||
|
const renderRows = () => renderPlayerTable(item, players, state, nodes);
|
||||||
|
|
||||||
|
nodes.playerSearch.value = "";
|
||||||
|
nodes.playerTeamFilter.value = state.team;
|
||||||
|
nodes.playerSort.value = state.sort;
|
||||||
|
nodes.playerSortDirection.value = state.direction;
|
||||||
|
bindPlayerTableControls(nodes, state, renderRows);
|
||||||
|
renderRows();
|
||||||
|
nodes.playerControls.hidden = false;
|
||||||
nodes.playersTableShell.hidden = false;
|
nodes.playersTableShell.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPlayerRows(player, item, index) {
|
function bindPlayerTableControls(nodes, state, renderRows) {
|
||||||
|
nodes.playerControls.onsubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
nodes.playerSearch.oninput = () => {
|
||||||
|
closePlayerDetailRows(nodes.playersBody);
|
||||||
|
state.search = nodes.playerSearch.value;
|
||||||
|
renderRows();
|
||||||
|
};
|
||||||
|
nodes.playerTeamFilter.onchange = () => {
|
||||||
|
closePlayerDetailRows(nodes.playersBody);
|
||||||
|
state.team = nodes.playerTeamFilter.value;
|
||||||
|
renderRows();
|
||||||
|
};
|
||||||
|
nodes.playerSort.onchange = () => {
|
||||||
|
state.sort = nodes.playerSort.value;
|
||||||
|
state.isDefaultSort = false;
|
||||||
|
renderRows();
|
||||||
|
};
|
||||||
|
nodes.playerSortDirection.onchange = () => {
|
||||||
|
state.direction = nodes.playerSortDirection.value;
|
||||||
|
state.isDefaultSort = false;
|
||||||
|
renderRows();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPlayerTable(item, players, state, nodes) {
|
||||||
|
const visiblePlayers = getVisiblePlayers(players, item, state);
|
||||||
|
nodes.playersNote.textContent =
|
||||||
|
visiblePlayers.length === players.length
|
||||||
|
? `${formatNumber(players.length)} jugadores con estadisticas locales.`
|
||||||
|
: `${formatNumber(visiblePlayers.length)} de ${formatNumber(players.length)} jugadores visibles.`;
|
||||||
|
nodes.playersState.hidden = visiblePlayers.length > 0;
|
||||||
|
if (!visiblePlayers.length) {
|
||||||
|
nodes.playersState.textContent = "No hay jugadores que coincidan con los controles activos.";
|
||||||
|
}
|
||||||
|
nodes.playersBody.innerHTML = visiblePlayers
|
||||||
|
.map((entry, index) => renderPlayerRows(entry.player, item, index, entry.inactive))
|
||||||
|
.join("");
|
||||||
|
bindPlayerDetailRows(nodes.playersBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisiblePlayers(players, item, state) {
|
||||||
|
const normalizedSearch = normalizeLookupText(state.search);
|
||||||
|
return players
|
||||||
|
.map((player) => ({
|
||||||
|
player,
|
||||||
|
inactive: isInactiveMatchPlayer(player),
|
||||||
|
team: getTeamSideDisplay(player.team || player.team_side),
|
||||||
|
}))
|
||||||
|
.filter((entry) => {
|
||||||
|
const matchesTeam = state.team === "all" || entry.team.key === state.team;
|
||||||
|
const matchesName =
|
||||||
|
!normalizedSearch ||
|
||||||
|
normalizeLookupText(entry.player.player_name).includes(normalizedSearch);
|
||||||
|
return matchesTeam && matchesName;
|
||||||
|
})
|
||||||
|
.sort((a, b) => comparePlayerEntries(a, b, item, state));
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparePlayerEntries(a, b, item, state) {
|
||||||
|
if (state.isDefaultSort) {
|
||||||
|
return (
|
||||||
|
compareInactivePriority(a, b) ||
|
||||||
|
compareNumericStat(b.player.kills, a.player.kills) ||
|
||||||
|
compareNumericStat(a.player.deaths, b.player.deaths) ||
|
||||||
|
comparePlayerNames(a.player, b.player)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["name", "team"].includes(state.sort)) {
|
||||||
|
const inactivePriority = compareInactivePriority(a, b);
|
||||||
|
if (inactivePriority) {
|
||||||
|
return inactivePriority;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = state.direction === "asc" ? 1 : -1;
|
||||||
|
const compared = comparePlayerSortValue(a, b, item, state.sort);
|
||||||
|
return compared * direction || comparePlayerNames(a.player, b.player);
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparePlayerSortValue(a, b, item, sort) {
|
||||||
|
if (sort === "name") {
|
||||||
|
return comparePlayerNames(a.player, b.player);
|
||||||
|
}
|
||||||
|
if (sort === "team") {
|
||||||
|
return compareText(a.team.label, b.team.label);
|
||||||
|
}
|
||||||
|
if (sort === "deaths" || sort === "teamkills" || sort === "kills") {
|
||||||
|
return compareNumericStat(a.player[sort], b.player[sort]);
|
||||||
|
}
|
||||||
|
if (sort === "kd") {
|
||||||
|
return compareNumericStat(getKdRatioValue(a.player), getKdRatioValue(b.player));
|
||||||
|
}
|
||||||
|
return compareNumericStat(
|
||||||
|
getKpmValue(a.player.kills, item.duration_seconds),
|
||||||
|
getKpmValue(b.player.kills, item.duration_seconds),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareInactivePriority(a, b) {
|
||||||
|
return Number(a.inactive) - Number(b.inactive);
|
||||||
|
}
|
||||||
|
|
||||||
|
function comparePlayerNames(a, b) {
|
||||||
|
return compareText(getPlayerName(a), getPlayerName(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareText(a, b) {
|
||||||
|
return String(a || "").localeCompare(String(b || ""), "es", {
|
||||||
|
sensitivity: "base",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareNumericStat(a, b) {
|
||||||
|
return toSortableNumber(a) - toSortableNumber(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPlayerRows(player, item, index, inactive = false) {
|
||||||
const team = getTeamSideDisplay(player.team || player.team_side);
|
const team = getTeamSideDisplay(player.team || player.team_side);
|
||||||
const rowId = `match-player-row-${index}`;
|
const rowId = `match-player-row-${index}`;
|
||||||
const panelId = `match-player-panel-${index}`;
|
const panelId = `match-player-panel-${index}`;
|
||||||
const playerName = player.player_name || player.name || "Jugador no identificado";
|
const playerName = getPlayerName(player);
|
||||||
const kpm = formatKpm(player.kills, item.duration_seconds);
|
const kpm = formatKpm(player.kills, item.duration_seconds);
|
||||||
return `
|
return `
|
||||||
<tr
|
<tr
|
||||||
class="historical-player-row historical-player-row--${team.key}"
|
class="historical-player-row historical-player-row--${team.key} ${inactive ? "is-inactive" : ""}"
|
||||||
id="${escapeHtml(rowId)}"
|
id="${escapeHtml(rowId)}"
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
@@ -301,6 +436,37 @@ function bindPlayerDetailRows(playersBody) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closePlayerDetailRows(playersBody) {
|
||||||
|
[...playersBody.querySelectorAll(".historical-player-row")].forEach((row) => {
|
||||||
|
const button = row.querySelector(".historical-player-row__details-button");
|
||||||
|
const detailRow = row.nextElementSibling;
|
||||||
|
if (!button || !detailRow?.classList.contains("historical-player-detail-row")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
row.classList.remove("is-expanded");
|
||||||
|
detailRow.classList.remove("is-open");
|
||||||
|
button.setAttribute("aria-expanded", "false");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlayerName(player) {
|
||||||
|
return player.player_name || player.name || "Jugador no identificado";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInactiveMatchPlayer(player) {
|
||||||
|
const team = getTeamSideDisplay(player.team || player.team_side);
|
||||||
|
return (
|
||||||
|
team.key === "unknown" &&
|
||||||
|
toSortableNumber(player.kills) === 0 &&
|
||||||
|
toSortableNumber(player.deaths) === 0 &&
|
||||||
|
toSortableNumber(player.teamkills) === 0 &&
|
||||||
|
getKdRatioValue(player) === 0 &&
|
||||||
|
!hasNamedCounts(player.top_weapons) &&
|
||||||
|
!hasNamedCounts(player.most_killed) &&
|
||||||
|
!hasNamedCounts(player.death_by)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPlayerStatsPanel(player, item, context) {
|
function renderPlayerStatsPanel(player, item, context) {
|
||||||
const matchups = buildPlayerDirectMatchups(player);
|
const matchups = buildPlayerDirectMatchups(player);
|
||||||
const hasExpandedStats =
|
const hasExpandedStats =
|
||||||
@@ -606,18 +772,32 @@ function formatOptionalNumber(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatKdRatio(player) {
|
function formatKdRatio(player) {
|
||||||
|
if (
|
||||||
|
!Number.isFinite(Number(player.kd_ratio)) &&
|
||||||
|
(!Number.isFinite(Number(player.kills)) || !Number.isFinite(Number(player.deaths)))
|
||||||
|
) {
|
||||||
|
return "No disponible";
|
||||||
|
}
|
||||||
|
return formatDecimal(getKdRatioValue(player), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKdRatioValue(player) {
|
||||||
if (Number.isFinite(Number(player.kd_ratio))) {
|
if (Number.isFinite(Number(player.kd_ratio))) {
|
||||||
return formatDecimal(player.kd_ratio, 2);
|
return Number(player.kd_ratio);
|
||||||
}
|
}
|
||||||
const kills = Number(player.kills);
|
const kills = Number(player.kills);
|
||||||
const deaths = Number(player.deaths);
|
const deaths = Number(player.deaths);
|
||||||
if (!Number.isFinite(kills) || !Number.isFinite(deaths)) {
|
if (!Number.isFinite(kills) || !Number.isFinite(deaths)) {
|
||||||
return "No disponible";
|
return 0;
|
||||||
}
|
}
|
||||||
return deaths > 0 ? formatDecimal(kills / deaths, 2) : formatDecimal(kills, 2);
|
return deaths > 0 ? kills / deaths : kills;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatKpm(kills, durationSeconds) {
|
function formatKpm(kills, durationSeconds) {
|
||||||
|
return formatDecimal(getKpmValue(kills, durationSeconds), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKpmValue(kills, durationSeconds) {
|
||||||
const parsedKills = Number(kills);
|
const parsedKills = Number(kills);
|
||||||
const parsedDurationSeconds = Number(durationSeconds);
|
const parsedDurationSeconds = Number(durationSeconds);
|
||||||
if (
|
if (
|
||||||
@@ -625,9 +805,9 @@ function formatKpm(kills, durationSeconds) {
|
|||||||
!Number.isFinite(parsedDurationSeconds) ||
|
!Number.isFinite(parsedDurationSeconds) ||
|
||||||
parsedDurationSeconds <= 0
|
parsedDurationSeconds <= 0
|
||||||
) {
|
) {
|
||||||
return formatDecimal(0, 2);
|
return 0;
|
||||||
}
|
}
|
||||||
return formatDecimal(parsedKills / (parsedDurationSeconds / 60), 2);
|
return parsedKills / (parsedDurationSeconds / 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNamedCounts(items) {
|
function formatNamedCounts(items) {
|
||||||
@@ -656,6 +836,11 @@ function formatNumber(value) {
|
|||||||
return new Intl.NumberFormat("es-ES").format(parsedValue);
|
return new Intl.NumberFormat("es-ES").format(parsedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toSortableNumber(value) {
|
||||||
|
const parsedValue = Number(value);
|
||||||
|
return Number.isFinite(parsedValue) ? parsedValue : 0;
|
||||||
|
}
|
||||||
|
|
||||||
function formatSignedNumber(value) {
|
function formatSignedNumber(value) {
|
||||||
const parsedValue = Number(value);
|
const parsedValue = Number(value);
|
||||||
if (!Number.isFinite(parsedValue) || parsedValue === 0) {
|
if (!Number.isFinite(parsedValue) || parsedValue === 0) {
|
||||||
|
|||||||
@@ -87,6 +87,50 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="historical-state" id="match-detail-players-state" hidden></p>
|
<p class="historical-state" id="match-detail-players-state" hidden></p>
|
||||||
|
<form
|
||||||
|
class="historical-player-controls"
|
||||||
|
id="match-detail-player-controls"
|
||||||
|
aria-label="Controles de jugadores"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<label class="historical-player-control historical-player-control--search">
|
||||||
|
<span>Buscar jugador</span>
|
||||||
|
<input
|
||||||
|
id="match-detail-player-search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Nombre del jugador"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="historical-player-control">
|
||||||
|
<span>Equipo</span>
|
||||||
|
<select id="match-detail-player-team-filter">
|
||||||
|
<option value="all">Todos</option>
|
||||||
|
<option value="allies">Aliados</option>
|
||||||
|
<option value="axis">Eje</option>
|
||||||
|
<option value="unknown">Sin equipo / No disponible</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="historical-player-control">
|
||||||
|
<span>Ordenar por</span>
|
||||||
|
<select id="match-detail-player-sort">
|
||||||
|
<option value="name">Jugador</option>
|
||||||
|
<option value="team">Equipo</option>
|
||||||
|
<option value="kills" selected>K</option>
|
||||||
|
<option value="deaths">D</option>
|
||||||
|
<option value="teamkills">TK</option>
|
||||||
|
<option value="kd">K/D</option>
|
||||||
|
<option value="kpm">KPM</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="historical-player-control">
|
||||||
|
<span>Direccion</span>
|
||||||
|
<select id="match-detail-player-sort-direction">
|
||||||
|
<option value="desc" selected>Descendente</option>
|
||||||
|
<option value="asc">Ascendente</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
<div class="historical-table-shell" id="match-detail-players-table-shell" hidden>
|
<div class="historical-table-shell" id="match-detail-players-table-shell" hidden>
|
||||||
<table class="historical-table historical-table--players">
|
<table class="historical-table historical-table--players">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user