diff --git a/assets/css/style.css b/assets/css/style.css index fb15744..e693055 100644 --- a/assets/css/style.css +++ b/assets/css/style.css @@ -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 { diff --git a/assets/js/groups.js b/assets/js/groups.js index 608d91f..eab4e1b 100644 --- a/assets/js/groups.js +++ b/assets/js/groups.js @@ -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() {
${Object.entries(standings).map(([letter, rows]) => groupCardHTML(letter, rows)).join('')}
+ ${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 `${t(`standings.${key}`)}`; + }) + .join(''); + + return ` +
+
+

${t('standings.thirdTitle')}

+
+

${t('standings.thirdNote')}

+

+ ${t('standings.qualified')} + ${t('standings.eliminated')} +

+
+ + + + + + + ${headers} + + + + ${ranking.map(thirdRowHTML).join('')} +
#${t('standings.team')}${t('standings.group')}
+
+
`; +} + +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 + ? `` + : ``; + return ` + + ${entry.rank} + + + ${team.name} + + + ${entry.group} + ${entry.played} + ${entry.won} + ${entry.drawn} + ${entry.lost} + ${entry.gf} + ${entry.ga} + ${entry.gd > 0 ? '+' : ''}${entry.gd} + ${entry.points} + ${status} + `; +} + // Abbreviation key shown only on small screens (where the header hover tooltips // don't fire); reuses the shared .stats-legend styling. function legendHTML() { diff --git a/assets/js/i18n.js b/assets/js/i18n.js index b75ddb2..a20349a 100644 --- a/assets/js/i18n.js +++ b/assets/js/i18n.js @@ -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',