diff --git a/backend/app/player_external_profiles.py b/backend/app/player_external_profiles.py index 27a47f7..032f57e 100644 --- a/backend/app/player_external_profiles.py +++ b/backend/app/player_external_profiles.py @@ -14,7 +14,8 @@ def build_external_player_profile_fields( player_id: object = None, steam_id: object = None, ) -> 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) if steam_id_64: return { @@ -24,9 +25,25 @@ def build_external_player_profile_fields( "steam": f"https://steamcommunity.com/profiles/{steam_id_64}", "hellor": f"https://hellor.pro/player/{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: @@ -34,10 +51,15 @@ def normalize_steam_id_64(value: object) -> str | 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: normalized_player_id = str(player_id or "").strip() if normalize_steam_id_64(steam_id) or normalize_steam_id_64(normalized_player_id): return "steam" - if _EPIC_ID_RE.fullmatch(normalized_player_id): + if normalize_epic_id(normalized_player_id): return "epic" return "unknown" diff --git a/frontend/assets/css/historico-scoreboard-detail.css b/frontend/assets/css/historico-scoreboard-detail.css index 8b9c179..180074f 100644 --- a/frontend/assets/css/historico-scoreboard-detail.css +++ b/frontend/assets/css/historico-scoreboard-detail.css @@ -590,3 +590,163 @@ 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; +} diff --git a/frontend/assets/js/historico-partida.js b/frontend/assets/js/historico-partida.js index 95c6739..25ad79e 100644 --- a/frontend/assets/js/historico-partida.js +++ b/frontend/assets/js/historico-partida.js @@ -16,7 +16,7 @@ document.addEventListener("DOMContentLoaded", () => { 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"), + playerTeamFilters: [...document.querySelectorAll('input[name="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"), @@ -227,7 +227,9 @@ function renderPlayerSection(item, nodes) { const renderRows = () => renderPlayerTable(item, players, state, nodes); nodes.playerSearch.value = ""; - nodes.playerTeamFilter.value = state.team; + nodes.playerTeamFilters.forEach((control) => { + control.checked = control.value === state.team; + }); nodes.playerSort.value = state.sort; nodes.playerSortDirection.value = state.direction; bindPlayerTableControls(nodes, state, renderRows); @@ -245,11 +247,13 @@ function bindPlayerTableControls(nodes, state, renderRows) { state.search = nodes.playerSearch.value; renderRows(); }; - nodes.playerTeamFilter.onchange = () => { - closePlayerDetailRows(nodes.playersBody); - state.team = nodes.playerTeamFilter.value; - renderRows(); - }; + nodes.playerTeamFilters.forEach((control) => { + control.onchange = () => { + closePlayerDetailRows(nodes.playersBody); + state.team = control.value; + renderRows(); + }; + }); nodes.playerSort.onchange = () => { state.sort = nodes.playerSort.value; state.isDefaultSort = false; @@ -483,10 +487,10 @@ function renderPlayerStatsPanel(player, item, context) {

${escapeHtml(context.playerName)}

- ${renderPlayerStatChip("K", formatOptionalNumber(player.kills))} - ${renderPlayerStatChip("D", formatOptionalNumber(player.deaths))} + ${renderPlayerStatChip("Kills", formatOptionalNumber(player.kills))} + ${renderPlayerStatChip("Muertes", formatOptionalNumber(player.deaths))} ${renderPlayerStatChip("TK", formatOptionalNumber(player.teamkills))} - ${renderPlayerStatChip("K/D", formatKdRatio(player))} + ${renderPlayerStatChip("KD", formatKdRatio(player))} ${renderPlayerStatChip("KPM", context.kpm)}
@@ -512,6 +516,7 @@ function renderExternalProfilesSection(player) { ["steam", "Steam"], ["hellor", "Hellor"], ["hll_records", "HLL Records"], + ["helo", "Helo"], ] .map(([key, label]) => [label, player.external_profile_links?.[key]]) .filter(([, href]) => typeof href === "string" && href.trim()); @@ -534,12 +539,26 @@ function renderExternalProfilesSection(player) { .join("")} ` - : "

Perfiles externos no disponibles.

" + : renderExternalProfilesUnavailable(player) } `; } + +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 + ? `

Jugador detectado como Epic. ID capturado: ${escapeHtml(epicId)}. Sin enlaces externos compatibles confirmados para este proveedor.

` + : "

Jugador detectado como Epic. Sin enlaces externos compatibles confirmados para este proveedor.

"; + } + + return "

Perfiles externos no disponibles.

"; +} + function renderPlayerStatChip(label, value) { return `
diff --git a/frontend/historico-partida.html b/frontend/historico-partida.html index 55ee7f9..ab4833f 100644 --- a/frontend/historico-partida.html +++ b/frontend/historico-partida.html @@ -94,6 +94,23 @@ aria-label="Controles de jugadores" hidden > +
+ Equipo +
+ + + +
+
-