feat: refine team filters and epic profile links
This commit is contained in:
@@ -14,7 +14,8 @@ def build_external_player_profile_fields(
|
|||||||
player_id: object = None,
|
player_id: object = None,
|
||||||
steam_id: object = None,
|
steam_id: object = None,
|
||||||
) -> dict[str, object]:
|
) -> dict[str, object]:
|
||||||
"""Expose player profile links only when a captured SteamID64 is valid."""
|
"""Expose external profile links only when a captured identifier is safe."""
|
||||||
|
|
||||||
steam_id_64 = normalize_steam_id_64(steam_id) or normalize_steam_id_64(player_id)
|
steam_id_64 = normalize_steam_id_64(steam_id) or normalize_steam_id_64(player_id)
|
||||||
if steam_id_64:
|
if steam_id_64:
|
||||||
return {
|
return {
|
||||||
@@ -24,9 +25,25 @@ def build_external_player_profile_fields(
|
|||||||
"steam": f"https://steamcommunity.com/profiles/{steam_id_64}",
|
"steam": f"https://steamcommunity.com/profiles/{steam_id_64}",
|
||||||
"hellor": f"https://hellor.pro/player/{steam_id_64}",
|
"hellor": f"https://hellor.pro/player/{steam_id_64}",
|
||||||
"hll_records": f"https://hllrecords.com/profiles/{steam_id_64}",
|
"hll_records": f"https://hllrecords.com/profiles/{steam_id_64}",
|
||||||
|
"helo": f"https://helo-system.de/statistics/players/{steam_id_64}?series=2024",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return {"platform": infer_player_platform(player_id=player_id, steam_id=steam_id)}
|
|
||||||
|
epic_id = normalize_epic_id(player_id)
|
||||||
|
if epic_id:
|
||||||
|
return {
|
||||||
|
"epic_id": epic_id,
|
||||||
|
"platform": "epic",
|
||||||
|
"external_profile_links": {
|
||||||
|
"hellor": f"https://hellor.pro/player/{epic_id}",
|
||||||
|
"hll_records": f"https://hllrecords.com/profiles/{epic_id}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"platform": infer_player_platform(player_id=player_id, steam_id=steam_id),
|
||||||
|
"external_profile_links": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def normalize_steam_id_64(value: object) -> str | None:
|
def normalize_steam_id_64(value: object) -> str | None:
|
||||||
@@ -34,10 +51,15 @@ def normalize_steam_id_64(value: object) -> str | None:
|
|||||||
return normalized if _STEAM_ID64_RE.fullmatch(normalized) else None
|
return normalized if _STEAM_ID64_RE.fullmatch(normalized) else None
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_epic_id(value: object) -> str | None:
|
||||||
|
normalized = str(value or "").strip()
|
||||||
|
return normalized.lower() if _EPIC_ID_RE.fullmatch(normalized) else None
|
||||||
|
|
||||||
|
|
||||||
def infer_player_platform(*, player_id: object = None, steam_id: object = None) -> str:
|
def infer_player_platform(*, player_id: object = None, steam_id: object = None) -> str:
|
||||||
normalized_player_id = str(player_id or "").strip()
|
normalized_player_id = str(player_id or "").strip()
|
||||||
if normalize_steam_id_64(steam_id) or normalize_steam_id_64(normalized_player_id):
|
if normalize_steam_id_64(steam_id) or normalize_steam_id_64(normalized_player_id):
|
||||||
return "steam"
|
return "steam"
|
||||||
if _EPIC_ID_RE.fullmatch(normalized_player_id):
|
if normalize_epic_id(normalized_player_id):
|
||||||
return "epic"
|
return "epic"
|
||||||
return "unknown"
|
return "unknown"
|
||||||
|
|||||||
@@ -590,3 +590,163 @@
|
|||||||
height: 96px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.historical-player-control--team-toggle {
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control--team-toggle legend {
|
||||||
|
color: var(--muted);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle span {
|
||||||
|
border: 1px solid rgba(210, 196, 130, 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--muted);
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 0.55rem 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle input:checked + span {
|
||||||
|
background: rgba(210, 196, 130, 0.12);
|
||||||
|
border-color: rgba(210, 196, 130, 0.72);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle input:focus-visible + span {
|
||||||
|
outline: 2px solid rgba(210, 196, 130, 0.9);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-stats-panel__profiles code {
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Player controls refinement: team selector above search/sort. */
|
||||||
|
.historical-player-control--team-toggle {
|
||||||
|
border: 0;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
order: -1;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-control--team-toggle legend {
|
||||||
|
color: var(--muted);
|
||||||
|
display: block;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle label {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle span {
|
||||||
|
border: 1px solid rgba(210, 196, 130, 0.34);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--muted);
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 0.58rem 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
transition:
|
||||||
|
background 160ms ease,
|
||||||
|
border-color 160ms ease,
|
||||||
|
color 160ms ease,
|
||||||
|
box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle__item--all input:checked + span {
|
||||||
|
background: rgba(210, 196, 130, 0.13);
|
||||||
|
border-color: rgba(210, 196, 130, 0.78);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: 0 0 0 1px rgba(210, 196, 130, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle__item--allies span {
|
||||||
|
border-color: rgba(109, 171, 255, 0.42);
|
||||||
|
color: #b8d7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle__item--allies input:checked + span {
|
||||||
|
background: rgba(72, 135, 220, 0.24);
|
||||||
|
border-color: rgba(133, 190, 255, 0.9);
|
||||||
|
color: #e4f1ff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(109, 171, 255, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle__item--axis span {
|
||||||
|
border-color: rgba(225, 113, 86, 0.48);
|
||||||
|
color: #ffb6a6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle__item--axis input:checked + span {
|
||||||
|
background: rgba(170, 72, 54, 0.28);
|
||||||
|
border-color: rgba(255, 143, 113, 0.92);
|
||||||
|
color: #ffe1d9;
|
||||||
|
box-shadow: 0 0 0 1px rgba(225, 113, 86, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-team-toggle input:focus-visible + span {
|
||||||
|
outline: 2px solid rgba(210, 196, 130, 0.9);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historical-player-stats-panel__profiles code {
|
||||||
|
color: var(--text);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
playersState: document.getElementById("match-detail-players-state"),
|
playersState: document.getElementById("match-detail-players-state"),
|
||||||
playerControls: document.getElementById("match-detail-player-controls"),
|
playerControls: document.getElementById("match-detail-player-controls"),
|
||||||
playerSearch: document.getElementById("match-detail-player-search"),
|
playerSearch: document.getElementById("match-detail-player-search"),
|
||||||
playerTeamFilter: document.getElementById("match-detail-player-team-filter"),
|
playerTeamFilters: [...document.querySelectorAll('input[name="match-detail-player-team-filter"]')],
|
||||||
playerSort: document.getElementById("match-detail-player-sort"),
|
playerSort: document.getElementById("match-detail-player-sort"),
|
||||||
playerSortDirection: document.getElementById("match-detail-player-sort-direction"),
|
playerSortDirection: document.getElementById("match-detail-player-sort-direction"),
|
||||||
playersTableShell: document.getElementById("match-detail-players-table-shell"),
|
playersTableShell: document.getElementById("match-detail-players-table-shell"),
|
||||||
@@ -227,7 +227,9 @@ function renderPlayerSection(item, nodes) {
|
|||||||
const renderRows = () => renderPlayerTable(item, players, state, nodes);
|
const renderRows = () => renderPlayerTable(item, players, state, nodes);
|
||||||
|
|
||||||
nodes.playerSearch.value = "";
|
nodes.playerSearch.value = "";
|
||||||
nodes.playerTeamFilter.value = state.team;
|
nodes.playerTeamFilters.forEach((control) => {
|
||||||
|
control.checked = control.value === state.team;
|
||||||
|
});
|
||||||
nodes.playerSort.value = state.sort;
|
nodes.playerSort.value = state.sort;
|
||||||
nodes.playerSortDirection.value = state.direction;
|
nodes.playerSortDirection.value = state.direction;
|
||||||
bindPlayerTableControls(nodes, state, renderRows);
|
bindPlayerTableControls(nodes, state, renderRows);
|
||||||
@@ -245,11 +247,13 @@ function bindPlayerTableControls(nodes, state, renderRows) {
|
|||||||
state.search = nodes.playerSearch.value;
|
state.search = nodes.playerSearch.value;
|
||||||
renderRows();
|
renderRows();
|
||||||
};
|
};
|
||||||
nodes.playerTeamFilter.onchange = () => {
|
nodes.playerTeamFilters.forEach((control) => {
|
||||||
|
control.onchange = () => {
|
||||||
closePlayerDetailRows(nodes.playersBody);
|
closePlayerDetailRows(nodes.playersBody);
|
||||||
state.team = nodes.playerTeamFilter.value;
|
state.team = control.value;
|
||||||
renderRows();
|
renderRows();
|
||||||
};
|
};
|
||||||
|
});
|
||||||
nodes.playerSort.onchange = () => {
|
nodes.playerSort.onchange = () => {
|
||||||
state.sort = nodes.playerSort.value;
|
state.sort = nodes.playerSort.value;
|
||||||
state.isDefaultSort = false;
|
state.isDefaultSort = false;
|
||||||
@@ -483,10 +487,10 @@ function renderPlayerStatsPanel(player, item, context) {
|
|||||||
<h4>${escapeHtml(context.playerName)}</h4>
|
<h4>${escapeHtml(context.playerName)}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="historical-player-stats-panel__summary">
|
<div class="historical-player-stats-panel__summary">
|
||||||
${renderPlayerStatChip("K", formatOptionalNumber(player.kills))}
|
${renderPlayerStatChip("Kills", formatOptionalNumber(player.kills))}
|
||||||
${renderPlayerStatChip("D", formatOptionalNumber(player.deaths))}
|
${renderPlayerStatChip("Muertes", formatOptionalNumber(player.deaths))}
|
||||||
${renderPlayerStatChip("TK", formatOptionalNumber(player.teamkills))}
|
${renderPlayerStatChip("TK", formatOptionalNumber(player.teamkills))}
|
||||||
${renderPlayerStatChip("K/D", formatKdRatio(player))}
|
${renderPlayerStatChip("KD", formatKdRatio(player))}
|
||||||
${renderPlayerStatChip("KPM", context.kpm)}
|
${renderPlayerStatChip("KPM", context.kpm)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -512,6 +516,7 @@ function renderExternalProfilesSection(player) {
|
|||||||
["steam", "Steam"],
|
["steam", "Steam"],
|
||||||
["hellor", "Hellor"],
|
["hellor", "Hellor"],
|
||||||
["hll_records", "HLL Records"],
|
["hll_records", "HLL Records"],
|
||||||
|
["helo", "Helo"],
|
||||||
]
|
]
|
||||||
.map(([key, label]) => [label, player.external_profile_links?.[key]])
|
.map(([key, label]) => [label, player.external_profile_links?.[key]])
|
||||||
.filter(([, href]) => typeof href === "string" && href.trim());
|
.filter(([, href]) => typeof href === "string" && href.trim());
|
||||||
@@ -534,12 +539,26 @@ function renderExternalProfilesSection(player) {
|
|||||||
.join("")}
|
.join("")}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: "<p>Perfiles externos no disponibles.</p>"
|
: renderExternalProfilesUnavailable(player)
|
||||||
}
|
}
|
||||||
</article>
|
</article>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderExternalProfilesUnavailable(player) {
|
||||||
|
const platform = String(player.platform || "").toLowerCase();
|
||||||
|
const epicId = typeof player.epic_id === "string" ? player.epic_id.trim() : "";
|
||||||
|
|
||||||
|
if (platform === "epic") {
|
||||||
|
return epicId
|
||||||
|
? `<p>Jugador detectado como Epic. ID capturado: <code>${escapeHtml(epicId)}</code>. Sin enlaces externos compatibles confirmados para este proveedor.</p>`
|
||||||
|
: "<p>Jugador detectado como Epic. Sin enlaces externos compatibles confirmados para este proveedor.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<p>Perfiles externos no disponibles.</p>";
|
||||||
|
}
|
||||||
|
|
||||||
function renderPlayerStatChip(label, value) {
|
function renderPlayerStatChip(label, value) {
|
||||||
return `
|
return `
|
||||||
<article>
|
<article>
|
||||||
|
|||||||
@@ -94,6 +94,23 @@
|
|||||||
aria-label="Controles de jugadores"
|
aria-label="Controles de jugadores"
|
||||||
hidden
|
hidden
|
||||||
>
|
>
|
||||||
|
<fieldset class="historical-player-control historical-player-control--team-toggle">
|
||||||
|
<legend>Equipo</legend>
|
||||||
|
<div class="historical-player-team-toggle" role="radiogroup" aria-label="Filtro por equipo">
|
||||||
|
<label class="historical-player-team-toggle__item historical-player-team-toggle__item--all">
|
||||||
|
<input type="radio" name="match-detail-player-team-filter" value="all" checked />
|
||||||
|
<span>Todos</span>
|
||||||
|
</label>
|
||||||
|
<label class="historical-player-team-toggle__item historical-player-team-toggle__item--allies">
|
||||||
|
<input type="radio" name="match-detail-player-team-filter" value="allies" />
|
||||||
|
<span>Aliados</span>
|
||||||
|
</label>
|
||||||
|
<label class="historical-player-team-toggle__item historical-player-team-toggle__item--axis">
|
||||||
|
<input type="radio" name="match-detail-player-team-filter" value="axis" />
|
||||||
|
<span>Eje</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<label class="historical-player-control historical-player-control--search">
|
<label class="historical-player-control historical-player-control--search">
|
||||||
<span>Buscar jugador</span>
|
<span>Buscar jugador</span>
|
||||||
<input
|
<input
|
||||||
@@ -103,25 +120,15 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
</label>
|
</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">
|
<label class="historical-player-control">
|
||||||
<span>Ordenar por</span>
|
<span>Ordenar por</span>
|
||||||
<select id="match-detail-player-sort">
|
<select id="match-detail-player-sort">
|
||||||
<option value="name">Jugador</option>
|
<option value="kills" selected>Kills</option>
|
||||||
<option value="team">Equipo</option>
|
<option value="deaths">Muertes</option>
|
||||||
<option value="kills" selected>K</option>
|
|
||||||
<option value="deaths">D</option>
|
|
||||||
<option value="teamkills">TK</option>
|
<option value="teamkills">TK</option>
|
||||||
<option value="kd">K/D</option>
|
<option value="kd">KD</option>
|
||||||
<option value="kpm">KPM</option>
|
<option value="kpm">KPM</option>
|
||||||
|
<option value="name">Jugador</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="historical-player-control">
|
<label class="historical-player-control">
|
||||||
@@ -138,10 +145,10 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Jugador</th>
|
<th>Jugador</th>
|
||||||
<th>Equipo</th>
|
<th>Equipo</th>
|
||||||
<th>K</th>
|
<th>Kills</th>
|
||||||
<th>D</th>
|
<th>Muertes</th>
|
||||||
<th>TK</th>
|
<th>TK</th>
|
||||||
<th>K/D</th>
|
<th>KD</th>
|
||||||
<th>KPM</th>
|
<th>KPM</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user