Add match detail map hero image
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: TASK-136
|
||||
title: Add RCON match detail map hero image
|
||||
status: pending
|
||||
status: done
|
||||
type: frontend
|
||||
team: Frontend Senior
|
||||
supporting_teams:
|
||||
@@ -138,11 +138,28 @@ Then hard refresh with `Ctrl+F5` and verify:
|
||||
|
||||
## Outcome
|
||||
|
||||
To be completed by AI Platform Run / Codex CLI.
|
||||
- Added a hidden map-media slot to `frontend/historico-partida.html` and reveal it only when the current match resolves to a known local map asset.
|
||||
- Added `resolveMapImagePath` in `frontend/assets/js/historico-partida.js`, mapping the available local map names including the known Carentan match to `./assets/img/maps/carentan-day.webp`.
|
||||
- Added the map assets under `frontend/assets/img/maps/` because the task explicitly depends on local map image files.
|
||||
- Updated the historical hero CSS so the logo, match copy and map image form a responsive desktop header and collapse cleanly on mobile.
|
||||
- Shortened the RCON hero subtitle to keep the narrow mobile header from clipping while preserving the player table, hidden timeline, hidden confidence/source/base details and existing scoreboard data.
|
||||
|
||||
## Validation Result
|
||||
|
||||
To be completed by AI Platform Run / Codex CLI.
|
||||
- PASS: `node --check frontend/assets/js/historico-partida.js`
|
||||
- PASS: `node --check frontend/assets/js/historico.js`
|
||||
- PASS: `node --check frontend/assets/js/historico-recent-live.js`
|
||||
- PASS: `python -m compileall backend/app`
|
||||
- PASS: `powershell -ExecutionPolicy Bypass -File scripts/run-integration-tests.ps1`
|
||||
- PASS: `powershell -ExecutionPolicy Bypass -File scripts/run-rcon-data-pipeline-tests.ps1`
|
||||
- Note: the script completed successfully but emitted existing `ResourceWarning` messages from backend unittest sqlite connections.
|
||||
- PASS: `docker compose up -d --build backend frontend`
|
||||
- PASS: `Invoke-WebRequest "http://localhost:8000/health" | Select-Object -ExpandProperty Content`
|
||||
- PASS: `Invoke-WebRequest "http://localhost:8000/api/historical/recent-matches?server=all-servers&limit=10" | Select-Object -ExpandProperty Content`
|
||||
- Manual/rendered verification: Browser plugin was listed, but the required Node runtime tool was not exposed in this session; used local Chrome headless fallback.
|
||||
- Verified rendered DOM contains `./assets/img/maps/carentan-day.webp`, `3 : 2`, `Ganador: Aliados`, `Carentan`, `1 h 30 min`, `AntonioPruna` and `M1 GARAND`.
|
||||
- Verified timeline section remains hidden and no `snapshot`, Elo/MVP block or Comunidad Hispana #03 appeared in the rendered checks.
|
||||
- Captured screenshots outside the repository at `C:\Temp\task-136-match-detail-final.png` and `C:\Temp\task-136-match-detail-mobile-final-3.png`.
|
||||
|
||||
## Change Budget
|
||||
|
||||
@@ -22,15 +22,20 @@
|
||||
.historical-hero__copy {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.historical-hero__layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 300px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(190px, 260px) minmax(0, 0.82fr) minmax(320px, 0.9fr);
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.historical-hero__layout > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.historical-logo-frame {
|
||||
width: min(300px, 100%);
|
||||
min-height: 220px;
|
||||
@@ -42,6 +47,45 @@
|
||||
|
||||
.historical-hero__text {
|
||||
max-width: 60ch;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.historical-map-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(210, 182, 118, 0.2);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(19, 24, 16, 0.9), rgba(10, 13, 9, 0.72));
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 18px 40px rgba(0, 0, 0, 0.24);
|
||||
}
|
||||
|
||||
.historical-map-hero[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.historical-map-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border: 1px solid rgba(210, 182, 118, 0.12);
|
||||
border-radius: inherit;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(7, 9, 7, 0.24), transparent 34%),
|
||||
linear-gradient(180deg, transparent 54%, rgba(7, 9, 7, 0.44));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.historical-map-hero__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 220px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.historical-content {
|
||||
@@ -657,7 +701,24 @@
|
||||
}
|
||||
|
||||
.historical-hero__layout {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.historical-hero__copy,
|
||||
.historical-hero__copy > div,
|
||||
.historical-hero__text {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.historical-map-hero {
|
||||
width: 100%;
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.historical-map-hero__image {
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.historical-selector {
|
||||
|
||||
BIN
frontend/assets/img/maps/carentan-day.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
frontend/assets/img/maps/driel-day.webp
Normal file
|
After Width: | Height: | Size: 136 KiB |
BIN
frontend/assets/img/maps/elalamein-day.webp
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
frontend/assets/img/maps/elsenbornridge-day.webp
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
frontend/assets/img/maps/foy-day.webp
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
frontend/assets/img/maps/hill400-day.webp
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
frontend/assets/img/maps/hurtgenforest-day.webp
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
frontend/assets/img/maps/kharkov-day.webp
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
frontend/assets/img/maps/kursk-day.webp
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
frontend/assets/img/maps/mortain-day.webp
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
frontend/assets/img/maps/omahabeach-day.webp
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
frontend/assets/img/maps/purpleheartlane-rain.webp
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
frontend/assets/img/maps/smolensk-day.webp
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/assets/img/maps/stmariedumont-day.webp
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
frontend/assets/img/maps/stmereeglise-day.webp
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
frontend/assets/img/maps/tobruk-dawn.webp
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
frontend/assets/img/maps/tobruk-day.webp
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
frontend/assets/img/maps/utahbeach-day.webp
Normal file
|
After Width: | Height: | Size: 68 KiB |
@@ -20,6 +20,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
timelineNote: document.getElementById("match-detail-timeline-note"),
|
||||
timelineState: document.getElementById("match-detail-timeline-state"),
|
||||
timelineGrid: document.getElementById("match-detail-timeline-grid"),
|
||||
mapHero: document.getElementById("match-detail-map-hero"),
|
||||
mapImage: document.getElementById("match-detail-map-image"),
|
||||
};
|
||||
|
||||
if (!serverSlug || !matchId) {
|
||||
@@ -66,6 +68,7 @@ function renderMatchDetail(item, nodes) {
|
||||
nodes.title.textContent = mapName;
|
||||
nodes.summary.textContent = `${serverName} - ${formatDetailSubtitle(item)}`;
|
||||
nodes.note.textContent = "";
|
||||
renderMapHero(item, mapName, nodes);
|
||||
nodes.grid.innerHTML = renderScoreboardDetail(item, { mapName, serverName });
|
||||
renderPlayerSection(item, nodes);
|
||||
hideTimelineSection(nodes);
|
||||
@@ -74,6 +77,28 @@ function renderMatchDetail(item, nodes) {
|
||||
nodes.grid.hidden = false;
|
||||
}
|
||||
|
||||
function renderMapHero(item, mapName, nodes) {
|
||||
if (!nodes.mapHero || !nodes.mapImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mapImagePath = resolveMapImagePath(item, mapName);
|
||||
if (!mapImagePath) {
|
||||
nodes.mapImage.removeAttribute("src");
|
||||
nodes.mapImage.alt = "";
|
||||
nodes.mapHero.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.mapImage.src = mapImagePath;
|
||||
nodes.mapImage.alt = mapName;
|
||||
nodes.mapImage.onerror = () => {
|
||||
nodes.mapImage.removeAttribute("src");
|
||||
nodes.mapHero.hidden = true;
|
||||
};
|
||||
nodes.mapHero.hidden = false;
|
||||
}
|
||||
|
||||
function renderScoreboardDetail(item, { mapName, serverName }) {
|
||||
const result = item.result || {};
|
||||
const alliedScore = Number.isFinite(Number(result.allied_score))
|
||||
@@ -273,6 +298,36 @@ function resolveMatchFactions(item, mapName) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveMapImagePath(item, mapName) {
|
||||
const normalizedMap = normalizeLookupText(
|
||||
`${item.map?.name || ""} ${item.map?.pretty_name || ""} ${mapName || ""}`,
|
||||
).replaceAll(" ", "");
|
||||
const mapAssetByKey = {
|
||||
carentan: "carentan-day.webp",
|
||||
driel: "driel-day.webp",
|
||||
elalamein: "elalamein-day.webp",
|
||||
elsenbornridge: "elsenbornridge-day.webp",
|
||||
foy: "foy-day.webp",
|
||||
hill400: "hill400-day.webp",
|
||||
hurtgenforest: "hurtgenforest-day.webp",
|
||||
kharkov: "kharkov-day.webp",
|
||||
kursk: "kursk-day.webp",
|
||||
mortain: "mortain-day.webp",
|
||||
omahabeach: "omahabeach-day.webp",
|
||||
purpleheartlane: "purpleheartlane-rain.webp",
|
||||
smolensk: "smolensk-day.webp",
|
||||
stmariedumont: "stmariedumont-day.webp",
|
||||
stmereeglise: "stmereeglise-day.webp",
|
||||
tobrukdawn: "tobruk-dawn.webp",
|
||||
tobruk: "tobruk-day.webp",
|
||||
utahbeach: "utahbeach-day.webp",
|
||||
};
|
||||
const matchedKey = Object.keys(mapAssetByKey).find((key) =>
|
||||
normalizedMap.includes(key),
|
||||
);
|
||||
return matchedKey ? `./assets/img/maps/${mapAssetByKey[matchedKey]}` : "";
|
||||
}
|
||||
|
||||
function normalizeLookupText(value) {
|
||||
return String(value || "")
|
||||
.normalize("NFD")
|
||||
@@ -284,10 +339,10 @@ function normalizeLookupText(value) {
|
||||
|
||||
function formatDetailSubtitle(item) {
|
||||
if (item.capture_basis === "rcon-materialized-admin-log") {
|
||||
return "Partida RCON materializada";
|
||||
return "RCON";
|
||||
}
|
||||
if (item.capture_basis === "rcon-competitive-window") {
|
||||
return "Partida RCON registrada";
|
||||
return "RCON";
|
||||
}
|
||||
if (item.result_source === "public-scoreboard-match") {
|
||||
return "Partida del scoreboard";
|
||||
|
||||
@@ -44,6 +44,18 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<figure class="historical-map-hero" id="match-detail-map-hero" hidden>
|
||||
<img
|
||||
class="historical-map-hero__image"
|
||||
id="match-detail-map-image"
|
||||
src=""
|
||||
alt=""
|
||||
width="960"
|
||||
height="540"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||