mirror of
https://github.com/LucasKalil-Programador/world-2026-hub.git
synced 2026-07-04 17:41:28 -03:00
feat(groups): add best third-placed teams ranking table
Ranks the 12 third-placed teams across groups (Pts -> GD -> GF -> id) and marks the top 8 that advance to the R32. Full-width section below the group cards in the Grupos tab, gated on all 12 groups finished (omitted from the DOM otherwise). Reuses .standings-table styling, header tooltips and the favorite-row highlight: gold rows + check for the 8 qualified, muted rows for 9-12, a dashed cut line between. computeThirdPlaceRanking() only ranks for display; the slot->group allocation stays in bracket-config.json. Bumps APP_VERSION to v1.0.3 (also covers the hero knockout-resolution fix).
This commit is contained in:
parent
22a157197b
commit
adb8cce441
3 changed files with 162 additions and 1 deletions
|
|
@ -785,6 +785,65 @@ button {
|
|||
}
|
||||
}
|
||||
|
||||
/* best third-placed teams table (shown once all 12 groups finish) */
|
||||
.third-place {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem 1.1rem 1.2rem;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
background: rgba(16, 36, 59, 0.55);
|
||||
}
|
||||
|
||||
.third-place-note {
|
||||
margin: 0 0 0.7rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.third-place-keys {
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
|
||||
.legend-dot.out {
|
||||
background: var(--text-secondary);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.third-place-scroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.standings-table .col-group {
|
||||
font-weight: 600;
|
||||
color: var(--accent-gold);
|
||||
}
|
||||
|
||||
.third-place-table tr.row-third td:first-child {
|
||||
box-shadow: inset 3px 0 0 var(--accent-gold);
|
||||
}
|
||||
|
||||
.third-place-table tr.row-out td {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.third-place-table tr.cut {
|
||||
border-top: 2px dashed rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
|
||||
.third-place-table .col-status {
|
||||
width: 1.6rem;
|
||||
}
|
||||
|
||||
.third-place-table .qual-yes {
|
||||
color: var(--accent-gold);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.third-place-table .qual-no {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ stadiums */
|
||||
|
||||
.stadiums-grid {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,24 @@ export function isGroupFinished(letter) {
|
|||
.every((m) => resultByMatchId.get(m.id)?.status === 'finished');
|
||||
}
|
||||
|
||||
function allGroupsFinished() {
|
||||
return Object.keys(getData().groups).every(isGroupFinished);
|
||||
}
|
||||
|
||||
// Best third-placed teams: each group's 3rd-placed row, ranked across all 12
|
||||
// groups by the same criteria as within a group (points → GD → GF → id). The top
|
||||
// 8 advance to the Round of 32. This only ranks the thirds for display — the
|
||||
// slot→group allocation itself lives in bracket-config.json (filled from FIFA's
|
||||
// official combination table), not derived here.
|
||||
export function computeThirdPlaceRanking() {
|
||||
const standings = computeStandings();
|
||||
return Object.entries(standings)
|
||||
.map(([letter, rows]) => ({ ...rows[2], group: letter }))
|
||||
.sort((a, b) =>
|
||||
b.points - a.points || b.gd - a.gd || b.gf - a.gf || a.teamId.localeCompare(b.teamId))
|
||||
.map((row, i) => ({ ...row, rank: i + 1, qualified: i < 8 }));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------- render
|
||||
|
||||
export function initGroups() {
|
||||
|
|
@ -71,9 +89,83 @@ function render() {
|
|||
<div class="groups-grid">
|
||||
${Object.entries(standings).map(([letter, rows]) => groupCardHTML(letter, rows)).join('')}
|
||||
</div>
|
||||
${allGroupsFinished() ? thirdPlaceSectionHTML() : ''}
|
||||
${legendHTML()}`;
|
||||
}
|
||||
|
||||
// Best-third ranking table — rendered only once all 12 groups are finished (the
|
||||
// ranking is meaningless mid-stage). Reuses the .standings-table styling, the
|
||||
// header tooltips and the favorite-row highlight from the group cards.
|
||||
function thirdPlaceSectionHTML() {
|
||||
const ranking = computeThirdPlaceRanking();
|
||||
const headers = ['played', 'won', 'drawn', 'lost', 'gf', 'ga', 'gd', 'pts']
|
||||
.map((key) => {
|
||||
const tip = t(`tip.${key}`);
|
||||
const goals = key === 'gf' || key === 'ga' ? 'col-goals ' : '';
|
||||
return `<th class="${goals}has-tip" scope="col" data-tip="${tip}" aria-label="${t(`standings.${key}`)} — ${tip}">${t(`standings.${key}`)}</th>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<section class="third-place glass" aria-labelledby="third-place-title">
|
||||
<header class="group-card-header">
|
||||
<h3 id="third-place-title">${t('standings.thirdTitle')}</h3>
|
||||
</header>
|
||||
<p class="third-place-note">${t('standings.thirdNote')}</p>
|
||||
<p class="standings-legend third-place-keys">
|
||||
<span class="legend-item"><span class="legend-dot third"></span>${t('standings.qualified')}</span>
|
||||
<span class="legend-item"><span class="legend-dot out"></span>${t('standings.eliminated')}</span>
|
||||
</p>
|
||||
<div class="third-place-scroll">
|
||||
<table class="standings-table third-place-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th class="col-team" scope="col">${t('standings.team')}</th>
|
||||
<th class="col-group has-tip" scope="col" data-tip="${t('standings.group')}" aria-label="${t('standings.group')}">${t('standings.group')}</th>
|
||||
${headers}
|
||||
<th class="col-status" scope="col" aria-label="${t('standings.qualified')}"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${ranking.map(thirdRowHTML).join('')}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
function thirdRowHTML(entry, index) {
|
||||
const team = getData().teamById.get(entry.teamId);
|
||||
const fav = getFavorites().includes(team.id);
|
||||
const cls = [
|
||||
entry.qualified ? 'row-third' : 'row-out',
|
||||
index === 8 ? 'cut' : '',
|
||||
fav ? 'fav-row' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
const status = entry.qualified
|
||||
? `<span class="qual-yes" aria-label="${t('standings.qualified')}">✓</span>`
|
||||
: `<span class="qual-no" aria-label="${t('standings.eliminated')}">—</span>`;
|
||||
return `
|
||||
<tr class="${cls}">
|
||||
<td>${entry.rank}</td>
|
||||
<td class="col-team">
|
||||
<img class="flag" src="${flagSrc(team)}" alt="" width="22" height="15" loading="lazy">
|
||||
<span>${team.name}</span>
|
||||
<button class="fav-btn ${fav ? 'active' : ''}" data-fav="${team.id}"
|
||||
aria-pressed="${fav}" aria-label="${t('fav.toggle')} ${team.name}">${fav ? '★' : '☆'}</button>
|
||||
</td>
|
||||
<td class="col-group">${entry.group}</td>
|
||||
<td>${entry.played}</td>
|
||||
<td>${entry.won}</td>
|
||||
<td>${entry.drawn}</td>
|
||||
<td>${entry.lost}</td>
|
||||
<td class="col-goals">${entry.gf}</td>
|
||||
<td class="col-goals">${entry.ga}</td>
|
||||
<td>${entry.gd > 0 ? '+' : ''}${entry.gd}</td>
|
||||
<td class="col-pts">${entry.points}</td>
|
||||
<td class="col-status">${status}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// Abbreviation key shown only on small screens (where the header hover tooltips
|
||||
// don't fire); reuses the shared .stats-legend styling.
|
||||
function legendHTML() {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { getPrefs, setPref } from './storage.js';
|
||||
|
||||
// App version for footer display — bump this after any notable changes
|
||||
const APP_VERSION = 'v1.0.2';
|
||||
const APP_VERSION = 'v1.0.3';
|
||||
|
||||
const dicts = {
|
||||
en: {
|
||||
|
|
@ -73,6 +73,11 @@ const dicts = {
|
|||
'standings.legendTop2': 'Advance to the Round of 32',
|
||||
'standings.legendThird': 'In contention for best third place',
|
||||
'standings.inProgress': 'In progress',
|
||||
'standings.thirdTitle': 'Best third-placed teams',
|
||||
'standings.thirdNote': '8 of 12 advance to the Round of 32 — ranked by points, then goal difference, then goals for.',
|
||||
'standings.group': 'Group',
|
||||
'standings.qualified': 'Qualified',
|
||||
'standings.eliminated': 'Eliminated',
|
||||
'stadiums.capacity': 'Capacity',
|
||||
'stadiums.viewMatches': 'View matches',
|
||||
'status.scheduled': 'Scheduled',
|
||||
|
|
@ -248,6 +253,11 @@ const dicts = {
|
|||
'standings.legendTop2': 'Avançam aos 16 avos de final',
|
||||
'standings.legendThird': 'Na briga por melhor 3º lugar',
|
||||
'standings.inProgress': 'Em andamento',
|
||||
'standings.thirdTitle': 'Melhores terceiros colocados',
|
||||
'standings.thirdNote': '8 de 12 avançam aos 16 avos de final — ordenados por pontos, depois saldo de gols, depois gols pró.',
|
||||
'standings.group': 'Grupo',
|
||||
'standings.qualified': 'Classificado',
|
||||
'standings.eliminated': 'Eliminado',
|
||||
'stadiums.capacity': 'Capacidade',
|
||||
'stadiums.viewMatches': 'Ver partidas',
|
||||
'status.scheduled': 'Agendada',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue