Polish player profile layout name fallback and user copy
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# TASK-256 - Polish player profile layout, name fallback and user copy
|
||||
|
||||
## Summary
|
||||
|
||||
This task polishes the public player profile in `stats.html` after TASK-253 and TASK-255.
|
||||
|
||||
Changes included:
|
||||
|
||||
- moved external profile links out of the lower summary grid
|
||||
- kept selected search-result names as UI fallback when the profile payload has no player name
|
||||
- replaced technical user-facing copy with player-friendly messages
|
||||
- kept KPM visibility restricted to `kpm_status == ready`
|
||||
|
||||
## External Profile Layout
|
||||
|
||||
`Perfiles externos` now renders directly under the `Perfil personal` state text and before the weekly/monthly comparison cards.
|
||||
|
||||
The lower summary grid now only contains:
|
||||
|
||||
- `Identidad`
|
||||
- `Ventana semanal`
|
||||
- `Ventana mensual`
|
||||
|
||||
The links remain responsive and wrap on narrow screens.
|
||||
|
||||
## Player Name Fallback
|
||||
|
||||
Root cause:
|
||||
|
||||
- degraded weekly/monthly profile payloads can return no `player_name`
|
||||
- the frontend then fell back to `Sin nombre`
|
||||
- this ignored the player name already selected from search results
|
||||
|
||||
Fix:
|
||||
|
||||
- the selected result name is passed into `loadPlayerProfile(...)`
|
||||
- visible name priority is:
|
||||
- payload player name if it is usable
|
||||
- selected search-result name
|
||||
- current search text if it looks like a name
|
||||
- `Jugador seleccionado`
|
||||
|
||||
Raw Steam/Epic identifiers are not used as visible profile names.
|
||||
|
||||
## User Copy
|
||||
|
||||
Replaced visible technical wording:
|
||||
|
||||
- `Backend no disponible...` -> `Servicio no disponible...`
|
||||
- `Verifica backend...` -> `Intentalo de nuevo en unos segundos.`
|
||||
- `Jugador sin estadisticas suficientes...` -> `Todavia no hay estadisticas suficientes para este jugador.`
|
||||
- `Ranking ausente` -> `Sin posicion`
|
||||
- backend/recalculation note -> `Comparativa basada en los datos disponibles actualmente.`
|
||||
|
||||
Internal variable names and fetch URLs still use backend terminology where required by code.
|
||||
|
||||
## Validation
|
||||
|
||||
Executed:
|
||||
|
||||
- `node --check frontend/assets/js/stats.js`
|
||||
|
||||
Static checks:
|
||||
|
||||
- no visible `Sin nombre` fallback remains in stats UI
|
||||
- no visible profile copy mentions backend, endpoint, payload, read model, ranking block, or recalculation
|
||||
- KPM is still rendered only for `kpm_status == ready`
|
||||
- Steam profile links still resolve to Steam + Hellor + HLL Records + Helo
|
||||
- Epic profile links still resolve to Hellor + HLL Records
|
||||
|
||||
## Notes
|
||||
|
||||
- No backend changes.
|
||||
- No TeamKills changes.
|
||||
- No asset changes.
|
||||
- No scheduler/RCON/server configuration changes.
|
||||
@@ -1455,6 +1455,28 @@ h2 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.stats-profile-links-bar {
|
||||
margin: 0 0 18px;
|
||||
}
|
||||
|
||||
.stats-profile-links-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 14px;
|
||||
}
|
||||
|
||||
.stats-profile-links-strip__label {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.stats-profile-links-bar .stats-profile-links {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.stats-profile-links a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
const profilePanel = document.getElementById("stats-profile-panel");
|
||||
const profileTitle = document.getElementById("stats-profile-title");
|
||||
const profileStateNode = document.getElementById("stats-profile-state");
|
||||
const profileLinksBarNode = document.getElementById("stats-profile-links-bar");
|
||||
const comparisonGrid = document.getElementById("stats-comparison-grid");
|
||||
const summaryGrid = document.getElementById("stats-summary-grid");
|
||||
const weeklySummaryNode = document.getElementById("stats-weekly-summary");
|
||||
@@ -43,14 +44,14 @@
|
||||
|
||||
const messages = {
|
||||
backendUnavailableForSearch:
|
||||
"Backend no disponible. No se pueden buscar jugadores ni cargar su perfil.",
|
||||
"Servicio no disponible. No se pueden buscar jugadores ni cargar su perfil.",
|
||||
backendUnavailableForAnnual:
|
||||
"Backend no disponible. No se puede consultar el ranking anual.",
|
||||
"Servicio no disponible. No se puede consultar el ranking anual.",
|
||||
searchNoInput: "Introduce al menos 4 caracteres para buscar un jugador.",
|
||||
searchLoading: "Buscando jugadores...",
|
||||
searchEmpty: "Sin resultados. Prueba con otro texto o ID.",
|
||||
searchError:
|
||||
"Error al buscar jugadores. Verifica backend y reintenta.",
|
||||
"Error al buscar jugadores. Intentalo de nuevo en unos segundos.",
|
||||
searchReadyPrefix: "Se encontraron ",
|
||||
searchReadySuffix:
|
||||
" jugador(es). Selecciona uno para ver sus estad\u00edsticas.",
|
||||
@@ -58,7 +59,7 @@
|
||||
"Introduce al menos 4 caracteres para buscar un jugador.",
|
||||
profileLoading: "Cargando estad\u00edsticas personales...",
|
||||
profileNoStats:
|
||||
"Jugador sin estad\u00edsticas suficientes para mostrar datos personales.",
|
||||
"Todavia no hay estadisticas suficientes para este jugador.",
|
||||
profileError: "No fue posible cargar las estad\u00edsticas del jugador.",
|
||||
annualLoading: "Cargando ranking anual...",
|
||||
annualMissing:
|
||||
@@ -75,9 +76,9 @@
|
||||
monthlyPlaceholder:
|
||||
"Sin datos mensuales. El ranking mensual se actualiza al cargar un jugador.",
|
||||
weeklyWindowUnavailable:
|
||||
"No se pudo cargar el bloque semanal; se muestran los datos mensuales.",
|
||||
"No se pudieron cargar los datos semanales; se muestran los datos mensuales.",
|
||||
monthlyWindowUnavailable:
|
||||
"No se pudo cargar el bloque mensual; se muestran los datos semanales.",
|
||||
"No se pudieron cargar los datos mensuales; se muestran los datos semanales.",
|
||||
weeklyWindowUnavailableSummary:
|
||||
"Resumen semanal no disponible temporalmente.",
|
||||
monthlyWindowUnavailableSummary:
|
||||
@@ -85,7 +86,7 @@
|
||||
profileReadyTitle: "Perfil personal",
|
||||
profileEmptyTitle: "Selecciona un jugador para ver sus estad\u00edsticas.",
|
||||
partialProfileLoadWarning:
|
||||
"Algunos bloques de ranking no se cargaron; se mantienen los disponibles.",
|
||||
"Algunas lecturas no se cargaron; se mantienen las disponibles.",
|
||||
};
|
||||
|
||||
let isBackendOnline = false;
|
||||
@@ -158,6 +159,9 @@
|
||||
if (comparisonGrid) {
|
||||
comparisonGrid.innerHTML = "";
|
||||
}
|
||||
if (profileLinksBarNode) {
|
||||
profileLinksBarNode.innerHTML = "";
|
||||
}
|
||||
summaryGrid.innerHTML = "";
|
||||
if (weeklySummaryNode) {
|
||||
weeklySummaryNode.textContent = "Cargando ...";
|
||||
@@ -181,7 +185,7 @@
|
||||
isBackendOnline = true;
|
||||
setAnnualState(
|
||||
"neutral",
|
||||
"Backend disponible. Cargando ranking anual 2026.",
|
||||
"Servicio disponible. Cargando ranking anual 2026.",
|
||||
);
|
||||
void loadAnnualRanking();
|
||||
} catch (error) {
|
||||
@@ -234,8 +238,9 @@
|
||||
resultListNode.querySelectorAll("[data-player-id]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const playerId = button.getAttribute("data-player-id");
|
||||
const playerName = button.getAttribute("data-player-name");
|
||||
if (playerId) {
|
||||
void loadPlayerProfile(playerId);
|
||||
void loadPlayerProfile(playerId, playerName);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -376,7 +381,7 @@
|
||||
const rowsMarkup = items
|
||||
.map((item) => {
|
||||
const rank = safeInt(item.ranking_position, 0);
|
||||
const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre"));
|
||||
const playerName = escapeHtml(resolveVisiblePlayerName(item.player_name, null, null));
|
||||
const matches = safeInt(item.matches_considered, 0);
|
||||
const kills = safeInt(firstFiniteValue(item.kills, item.metric_value), 0);
|
||||
const killsPerMatch = formatKillsPerMatch(
|
||||
@@ -430,7 +435,7 @@
|
||||
|
||||
function renderResultItem(item) {
|
||||
const playerId = escapeHtml(String(item.player_id || ""));
|
||||
const playerName = escapeHtml(String(item.player_name || "Jugador sin nombre"));
|
||||
const playerName = escapeHtml(resolveVisiblePlayerName(item.player_name, null, null));
|
||||
const matches = Number.isFinite(Number(item.matches_considered))
|
||||
? Number(item.matches_considered)
|
||||
: 0;
|
||||
@@ -448,6 +453,7 @@
|
||||
class="stats-result-item__button"
|
||||
type="button"
|
||||
data-player-id="${playerId}"
|
||||
data-player-name="${playerName}"
|
||||
aria-label="Cargar perfil de ${playerName}"
|
||||
>
|
||||
<div class="stats-result-item__main">
|
||||
@@ -463,7 +469,7 @@
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadPlayerProfile(playerId) {
|
||||
async function loadPlayerProfile(playerId, selectedPlayerName = "") {
|
||||
if (!isBackendOnline) {
|
||||
markAsBackendUnavailable();
|
||||
return;
|
||||
@@ -493,8 +499,10 @@
|
||||
|
||||
profilePanel.hidden = false;
|
||||
profileTitle.textContent = messages.profileReadyTitle;
|
||||
const playerName = String(
|
||||
(weeklyData?.player_name || monthlyData?.player_name || "Sin nombre"),
|
||||
const playerName = resolveVisiblePlayerName(
|
||||
weeklyData?.player_name || monthlyData?.player_name,
|
||||
selectedPlayerName,
|
||||
searchInput?.value,
|
||||
);
|
||||
const profileIdentityData =
|
||||
weeklyData?.player_id || weeklyData?.external_profile_links
|
||||
@@ -519,8 +527,13 @@
|
||||
monthlyData,
|
||||
weeklyFailed,
|
||||
monthlyFailed,
|
||||
externalProfileBrands,
|
||||
});
|
||||
if (profileLinksBarNode) {
|
||||
profileLinksBarNode.innerHTML = renderStatsExternalProfilesBar(
|
||||
profileIdentityData,
|
||||
externalProfileBrands,
|
||||
);
|
||||
}
|
||||
|
||||
if (partialLoadWarning) {
|
||||
profileStateNode.textContent += ` ${messages.partialProfileLoadWarning}`;
|
||||
@@ -567,6 +580,9 @@
|
||||
if (comparisonGrid) {
|
||||
comparisonGrid.innerHTML = "";
|
||||
}
|
||||
if (profileLinksBarNode) {
|
||||
profileLinksBarNode.innerHTML = "";
|
||||
}
|
||||
summaryGrid.innerHTML = "";
|
||||
if (weeklySummaryNode) {
|
||||
weeklySummaryNode.textContent = messages.weeklyPlaceholder;
|
||||
@@ -579,12 +595,10 @@
|
||||
|
||||
function renderProfileSummaryCards({
|
||||
playerName,
|
||||
profileIdentityData,
|
||||
weeklyData,
|
||||
monthlyData,
|
||||
weeklyFailed,
|
||||
monthlyFailed,
|
||||
externalProfileBrands,
|
||||
}) {
|
||||
const weeklySummary = weeklyFailed
|
||||
? `
|
||||
@@ -625,7 +639,6 @@
|
||||
<p><strong>Partidas semanales:</strong> ${safeInt(weeklyData?.matches_considered, 0)}</p>
|
||||
<p><strong>Partidas mensuales:</strong> ${safeInt(monthlyData?.matches_considered, 0)}</p>
|
||||
</article>
|
||||
${renderStatsExternalProfilesCard(profileIdentityData, externalProfileBrands)}
|
||||
${weeklySummary}
|
||||
${monthlySummary}
|
||||
`;
|
||||
@@ -800,7 +813,7 @@
|
||||
<strong>Delta de partidas:</strong> ${formatSignedNumber(matchesDelta)}
|
||||
</p>
|
||||
<p class="stats-comparison-card__note">
|
||||
La lectura compara solo datos existentes del backend actual, sin recalcular rankings ni pedir nuevos endpoints.
|
||||
Comparativa basada en los datos disponibles actualmente.
|
||||
</p>
|
||||
</article>
|
||||
`;
|
||||
@@ -836,7 +849,7 @@
|
||||
if (requestFailed) {
|
||||
return {
|
||||
tone: "warning",
|
||||
title: "Bloque no disponible",
|
||||
title: "Datos no disponibles",
|
||||
detail: `No fue posible cargar los datos de la ventana ${label} en este intento.`,
|
||||
};
|
||||
}
|
||||
@@ -844,8 +857,8 @@
|
||||
if (!ranking) {
|
||||
return {
|
||||
tone: "error",
|
||||
title: "Ranking ausente",
|
||||
detail: `No se recibio bloque de ranking ${label} desde el backend.`,
|
||||
title: "Sin posicion",
|
||||
detail: `Aun no hay datos de la ventana ${label} suficientes para calcular su posicion.`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -921,6 +934,30 @@
|
||||
return labels[String(metric || "").trim()] || "Kills";
|
||||
}
|
||||
|
||||
function resolveVisiblePlayerName(primaryName, selectedName, queryText) {
|
||||
const candidates = [primaryName, selectedName, queryText];
|
||||
for (const candidate of candidates) {
|
||||
const normalized = String(candidate || "").trim();
|
||||
if (isDisplayablePlayerName(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
return "Jugador seleccionado";
|
||||
}
|
||||
|
||||
function isDisplayablePlayerName(value) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
if (/^\d{17}$/.test(value)) {
|
||||
return false;
|
||||
}
|
||||
if (/^[0-9a-f]{32}$/i.test(value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function labelForStatsWindowKind(windowKind) {
|
||||
const labels = {
|
||||
"current-week": "Semana actual",
|
||||
@@ -950,15 +987,15 @@
|
||||
`;
|
||||
}
|
||||
|
||||
function renderStatsExternalProfilesCard(profileData, brands) {
|
||||
function renderStatsExternalProfilesBar(profileData, brands) {
|
||||
const links = resolveStatsExternalProfileLinks(profileData, brands);
|
||||
if (!links.length) {
|
||||
return "";
|
||||
}
|
||||
return `
|
||||
<article class="stats-summary-card">
|
||||
<p class="stats-summary-title">Perfiles externos</p>
|
||||
${
|
||||
links.length
|
||||
? `
|
||||
<div class="stats-profile-links">
|
||||
<div class="stats-profile-links-strip">
|
||||
<span class="stats-profile-links-strip__label">Perfiles externos</span>
|
||||
<div class="stats-profile-links" aria-label="Perfiles externos">
|
||||
${links
|
||||
.map(
|
||||
({ href, label, logoSrc }) => `
|
||||
@@ -974,10 +1011,7 @@
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`
|
||||
: "<p>Perfiles externos no disponibles.</p>"
|
||||
}
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@
|
||||
<p class="panel__intro panel__intro--tight" id="stats-profile-state">
|
||||
Selecciona un jugador para ver su panel personal.
|
||||
</p>
|
||||
<div class="stats-profile-links-bar" id="stats-profile-links-bar"></div>
|
||||
<div class="stats-comparison-grid" id="stats-comparison-grid"></div>
|
||||
<div class="stats-summary-grid" id="stats-summary-grid"></div>
|
||||
<div class="stats-ranking-grid">
|
||||
|
||||
Reference in New Issue
Block a user