18 KiB
Project Map — World Cup 2026 Hub
Navigation map of the codebase. Use this to find which file owns a concern before reading code.
Status 2026-06-12 (all 12 steps + real-data migration done): everything works with real World Cup 2026 data — all views, bracket interactions, simulation, responsive/a11y pass, favorites, time toggle, challenge, share link,
.icsexport. Remaining: keepresults.jsoncurrent, fillthirdPlaceAssignmentafter the group stage (~Jun 27), Lighthouse run + GitHub Pages deploy. Spec source of truth:world-cup-2026-hub-spec-en.md+complement-spec-worldcup2026-en.md(complement wins on conflict).Branch note (2026-06-16): the full post-Cup Stats screen (
.agents/stats-screen-plan.md, stages A–J) is being built onfeature/stats-final-screen(merges tomasterat the end of the Cup). Stages A–D + F + J(round 1 polish) done and MERGED TO MASTER (2026-06-17) — built on that branch (degradation engine + fault-tolerantloadData+ sticky scrollspy sub-nav + flag monogram fallback; verdict-or-aggregate hero + goals-by-round chart; final ranking 1–48 by stage-reached + favorite-row highlight + team record cards; Records section = match records + format-48 debuts band; team comparator with diverging bars). Stage E skipped. Sub-nav live chips: Overview · Teams · Records · Comparator. Stage E (in-tab results archive) skipped by decision — the Matches tab stays the single surface for browsing; the "See all matches →" link is kept.masterkeeps the partial Stats tab + daily refreshes. Descriptions below reflect the branch.
File tree
worldcup2026/
├── .agents/ ← Internal documentation for AI agents
│ ├── project-map.md This file
│ ├── project-memory.md Context, decisions, gotchas
│ ├── stats-screen-plan.md Plan for the post-tournament "final stats" screen
│ │ (stages A–D+F+J merged to master; G/H/I pending)
│ ├── issues.md Deferred optimization proposals (event-driven
│ │ scheduling, PWA Tier 2; live-refresh = shipped pointer)
│ └── TODO.md 12-step build checklist + stats stages
│
├── .github/workflows/
│ └── deploy.yml CI: FTP deploy to Hostinger on push to master
│ (needs FTP_SERVER/USERNAME/PASSWORD secrets)
│ .gitignore OS/editor junk
│
├── index.html ★ SPA shell — header, nav tabs (Home, Matches,
│ Groups, Knockout, Stadiums, Stats), hero, dashboard,
│ modal container; loads app.js as ES module.
│ <head> has the PWA block (manifest link, theme-color,
│ favicons, apple-mobile-web-app-* meta)
│
├── manifest.json PWA web app manifest (name/short_name, standalone,
│ theme/background #081421, icons[]) — relative paths
│ (start_url ".", scope "./") for the subpath deploy
│ favicon.ico Root favicon (16+32, from the trophy logo)
│
├── assets/
│ ├── css/
│ │ ├── style.css ★ Palette variables, glassmorphism base, layout,
│ │ │ components — mobile-first
│ │ ├── bracket.css Knockout views: stadium-night stage, wallchart cards
│ │ │ (heat toward the Final) + SVG connectors, radial orbit
│ │ │ tokens (tk-* states, route lines), rounds-pager cards
│ │ │ + chips, view switch, path highlight, sim/challenge
│ │ │ styles; all motion gated by reduced-motion
│ │ ├── stats.css Stats tab: hero "pulse", overview cards, goals-by-stage chart
│ │ └── animations.css Entry (fade-in, slide-up/left) + interaction
│ │ (hover-scale/glow, pulse, line-draw)
│ ├── js/
│ │ ├── app.js ★ Entry point: loadData() (Promise.all over data/),
│ │ │ tab routing + lastTab (active-tab scroll-into-view +
│ │ │ edge fades on the scrollable nav), formatMatchTime(), dashboard,
│ │ │ clock-driven hero (matchState/findFeaturedMatches +
│ │ │ 1s heroTick: hybrid JSON+clock, 2h/3h window; stacks
│ │ │ simultaneous group-final matches, one shared timer),
│ │ │ live data refresh (startResultsPolling: 90s poll of
│ │ │ results.json, no-store + ?t, content signature, pauses
│ │ │ when tab hidden, stops at FINAL; on change also refetches
│ │ │ bracket-config.json; fires `datachange`);
│ │ │ loadOptional() = fault-tolerant fetch of the stats screen's
│ │ │ optional data layers (absent → silent empty default);
│ │ │ trackHeaderHeight() keeps the --header-h CSS var live
│ │ ├── schedule.js Match list, filters (incl. occurrence toggle
│ │ │ Played/Upcoming via hybrid matchState), search,
│ │ │ sort, "My Matches"; 60s clock-tick re-render
│ │ ├── groups.js Standings computation (3/1/0, GD, GF) + group tables
│ │ ├── stadiums.js Stadium cards + "view matches" cross-link
│ │ ├── bracket.js ★ Bracket tree resolution, resolveBracketTeams(),
│ │ │ simulation, challenge score, share prediction;
│ │ │ 3 switchable views (wc2026_prefs.bracketView):
│ │ │ computeWallchartLayout() center-out wallchart,
│ │ │ computeRadialLayout()+radialInnerHTML() orbit view
│ │ │ (flag tokens on rings, trophy center), rounds pager
│ │ │ (button-only); fit-to-chart zoom (fit = "100%")
│ │ ├── modal.js Match detail modal (ARIA dialog)
│ │ ├── storage.js localStorage wrapper — wc2026_* keys, auto-JSON
│ │ ├── i18n.js EN/PT-BR dicts + t(key), lang toggle
│ │ ├── stats.js ★ Stats tab: tournament-to-date aggregates (finished
│ │ │ matches only); verdict-or-aggregate hero + overview + goals-by-stage/round +
│ │ │ 48-team table ranked 1–48 by stage-reached (sortable, # = canonical rank) +
│ │ │ favorite-row highlight + 6 leader cards (LEADER_CARDS:
│ │ │ best attack/defense, most clean sheets/wins/conceded, best
│ │ │ GD) that rotate through teams TIED on the metric
│ │ │ (setupLeaderCarousels: auto 3.5s, pause hover/focus, arrows
│ │ │ + dots/counter, timers cleared each render) + team record
│ │ │ cards (win streak, champion path) + Records section (biggest win/high-scoring
│ │ │ match → modal, format-48 debuts band) + team comparator
│ │ │ (A-vs-B diverging bars). SECTIONS registry (graceful-
│ │ │ degradation gate: section + chip render only if available,
│ │ │ else removed from DOM) + sticky scrollspy sub-nav (anchor
│ │ │ chips, hash-safe) + flagImg fallback; imports getBracketTree/getFavorites/openMatchModal. Grows into
│ │ │ the post-cup plan (.agents/stats-screen-plan.md, A–J).
│ │ └── calendar.js .ics export (RFC 5545, CRLF, Blob download)
│ ├── images/ Team flag SVGs, stadium placeholders
│ └── icons/ PWA app icons (from the header trophy logo): icon.svg
│ (master + manifest SVG), icon-192/512.png (purpose any),
│ icon-maskable-192/512.png (safe-zone padded),
│ apple-touch-icon.png (180), favicon-16/32.png, favicon.ico
│
├── data/ All content — REAL WC2026 data since 2026-06-12
│ ├── teams.json 48 real qualifiers: { id, name, flag } (FIFA codes)
│ ├── groups.json Official draw { "A": [4 team ids], ... } × 12 (A–L)
│ ├── matches.json 104 real fixtures; UTC times; ids 1–72 chronological
│ │ group games, 73–104 = FIFA match numbers (bracketRef)
│ ├── results.json { matchId, homeScore, awayScore, penalties?, status } —
│ │ update as the tournament progresses
│ ├── stadiums.json 16 real venues: { id, name, city, capacity, image, timezone }
│ ├── bracket-config.json ★ official R32 structure + thirdPlaceAssignment (all null) —
│ │ the ONLY file to edit once real 3rd places are known
│ │ (slot → allowed-groups table in project-memory.md)
│ └── (optional, NOT yet created) stats-screen data layers loaded fault-tolerantly by
│ loadOptional(): players.json, player-events.json,
│ awards.json, keeper-stats.json, curiosities.json,
│ all-time-baselines.json — absent = silent empty default
│
├── README.md ★ Non-technical SHOWCASE (2026-07-04): tagline, badges,
│ live-demo link (lucaskalil.com/worldcup2026), per-page
│ screenshot gallery, plain-language "Under the hood".
│ Dev content lives in DEVELOPMENT.md now.
├── DEVELOPMENT.md Developer guide (2026-07-04): run locally, project
│ structure, JSON maintenance, local storage, deploy,
│ acceptance criteria, roadmap — split out of the old README
├── docs/screenshots/ 6 PNGs (home/matches/groups/bracket/stadiums/stats) for the
│ README gallery — captured via headless Edge, EN UI, 1366px.
│ Excluded from the FTP deploy (docs-only)
├── how-update.md Real-data migration runbook (mock → real — DONE 2026-06-12)
├── how-refresh-data.md ★ Daily refresh runbook during the tournament:
│ results.json scores/status + one-time
│ thirdPlaceAssignment; everything else frozen
├── world-cup-2026-hub-spec-en.md Main spec
└── complement-spec-worldcup2026-en.md Complement spec (precedence on conflict)
★ = critical files. Most changes touch one of them.
Key flows
1. Data load → render
index.html (type="module")
└─ app.js: loadData() ── Promise.all ──> data/*.json
├─ hero + dashboard (app.js)
├─ schedule.js ──┐
├─ groups.js ├── render into tab panels
├─ bracket.js ──┘
└─ modal.js (on match click, from any view)
2. Bracket resolution
groups.json + results.json
└─ groups.js: standings (pts 3/1/0 → GD → GF)
└─ bracket.js: R32 from bracket-config.json
├─ type:"group" → standings[ref][pos-1]
├─ type:"third" → standings[thirdPlaceAssignment[slot]][2] (null → placeholder)
└─ R16…FINAL: sequential pairing of winners (0-1→0, 2-3→1, …)
└─ wc2026_simulation overlays user picks (never mutates JSON)
3. Time display
matches.json time (UTC) ── formatMatchTime(match, stadium, mode)
├─ mode "local" → Intl.DateTimeFormat() (browser tz)
└─ mode "stadium" → Intl.DateTimeFormat({ timeZone: stadium.timezone })
Conventions
Imports
- ES Modules only (
type="module"), relative paths, no bundler/CDN.
Naming
- Team ids: 3-letter uppercase (
MEX,BRA). Knockout match ids:R32-1…R32-16,R16-1…,QF-1…,SF-1/SF-2,THIRD-PLACE,FINAL. - localStorage keys prefixed
wc2026_, accessed only throughstorage.js.
Content / i18n
- All user-facing strings go through
i18n.jst(key)— never hardcode UI text in HTML/JS. - Data values (team names, stadium names, cities) come from JSON and are not translated.
Where is each thing?
| Question | Answer |
|---|---|
| Where do I update scores / match status? | data/results.json |
| Where do I set the 8 best third-place teams? | data/bracket-config.json → thirdPlaceAssignment |
| Where do I add/translate a UI label? | assets/js/i18n.js (both EN and PT dicts) |
| Where is the standings math? | assets/js/groups.js |
| Where are knockout teams resolved (placeholders, TBD)? | assets/js/bracket.js → resolveBracketTeams() |
| Where is simulation state stored/cleared? | localStorage key wc2026_simulation, via assets/js/storage.js |
| Where do I change colors/theme? | CSS variables at the top of assets/css/style.css |
| Where do I add a stadium? | data/stadiums.json + image in assets/images/ |
| Where do I change the app name / install icon / theme color? | manifest.json (name/short_name/theme) + assets/icons/ (regenerate PNGs from icon.svg) + PWA <meta> in index.html <head> |
| How do I replace mock data with real WC2026 data? | how-update.md (root) — done 2026-06-12; kept as schema reference |
| How do I update scores during the tournament? | how-refresh-data.md (root) — daily results.json routine + thirdPlaceAssignment how-to |
Main functions
(Planned signatures from the complement spec — confirmed/updated as each step is implemented.)
| Function | File | Parameters | Returns | Description |
|---|---|---|---|---|
loadData |
assets/js/app.js |
() |
Promise<AppData> |
Fetches all data/*.json in parallel, caches in memory |
formatMatchTime |
assets/js/app.js |
(match, stadium, mode) |
string |
UTC → display time; mode is "local" or "stadium" |
matchState |
assets/js/app.js |
(match, result, now) |
'over' | 'live' | 'upcoming' |
Hybrid JSON+clock state (finished/live win; else clock advances at kickoff/kickoff+window). Used by the hero and the schedule occurrence filter / "Awaiting result" chip |
get / set |
assets/js/storage.js |
(key, fallback) / (key, value) |
any / void |
localStorage wrapper, auto JSON parse/stringify |
t |
assets/js/i18n.js |
(key) |
string |
Translated UI string for current lang |
resolveBracketTeams |
assets/js/bracket.js |
(matchOrRef) |
{ home, away } of { team, label } |
Display slots for any match (group or knockout); reused by schedule/modal/filters |
getBracketTree |
assets/js/bracket.js |
() |
{ rounds, third, nodesByRef, champion } |
Cached resolved bracket tree |
invalidateBracket |
assets/js/bracket.js |
() |
void |
Drop the cached tree (simulation overlay, step 9) |
calculateChallengeScore |
assets/js/bracket.js |
(simulation, results, bracketTree) |
{ correct, total, byPhase } |
Compares user picks vs finished results |
getShareableLink |
assets/js/bracket.js |
() |
string |
Current URL + ?prediction=base64(simulation) |
loadPredictionFromURL |
assets/js/bracket.js |
() |
void |
Decodes, validates, confirms before overwriting local simulation |
toggleFavorite |
assets/js/storage.js |
(teamId) |
void |
Add/remove team in wc2026_favorites |
computeStandings |
assets/js/groups.js |
() |
{ [letter]: Row[] } |
Sorted standings per group, finished matches only |
isGroupFinished |
assets/js/groups.js |
(letter) |
boolean |
All 6 group matches have status finished |
navigateTo |
assets/js/app.js |
(tab) |
void |
Programmatic tab switch (cross-view links) |
setStadiumFilter |
assets/js/schedule.js |
(stadiumName) |
void |
Reset filters + show one stadium's matches |
getFavoriteMatches |
assets/js/bracket.js |
(matches, favorites) |
Match[] |
Matches involving favorited teams (resolved slots); used by schedule |
calculateChallengeScore |
assets/js/bracket.js |
(simulation, results, bracketTree) |
{ correct, total, byPhase } |
Picks vs real finished knockout results |
getShareableLink / loadPredictionFromURL |
assets/js/bracket.js |
() |
string / void |
base64 ?prediction= export/import with validation + confirm |
exportMatchToICS |
assets/js/calendar.js |
(match, stadium) |
void |
RFC 5545 VEVENT (UTC, 2h, CRLF, escaped TEXT), Blob download |