From 129d55ff26126a52cc2c839c26933d20b8f005b5 Mon Sep 17 00:00:00 2001 From: Lucas Kalil Date: Fri, 12 Jun 2026 16:12:25 -0300 Subject: [PATCH] docs: add readme and real-data migration runbook --- README.md | 149 ++++++++++++++++++++++++++++++ how-update.md | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 README.md create mode 100644 how-update.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..224680d --- /dev/null +++ b/README.md @@ -0,0 +1,149 @@ +# World Cup 2026 Hub + +A static, single-page hub for the FIFA World Cup 2026 (Mexico · USA · Canada, 48 teams): +full match schedule, live group standings, an interactive knockout bracket with a +prediction/simulation mode, and a stadium guide. Built with vanilla HTML/CSS/JS — +no backend, no framework, no build step. All content lives in JSON files. + +**UI languages:** English / Português (toggle in the header, auto-detected on first visit). + +--- + +## Features + +- **Schedule** — all 104 matches with filters (date, group, phase, team, stadium), + free-text search, date sorting, and a "My matches" favorites filter. +- **Groups** — standings computed live from results (3/1/0 points, goal difference, + goals for), qualification highlights. +- **Knockout bracket** — generated dynamically from standings + `bracket-config.json`; + hover highlights a match's full path; mouse-wheel/pinch zoom; drag to pan. +- **Simulation mode** — pick winners and scores for unplayed knockout matches; picks + propagate through the rounds, persist locally, and never touch the JSON data. +- **Bracket challenge** — once real knockout results land, your saved picks are scored + ("X of Y picks correct", per phase). +- **Share prediction** — copy a link that carries your bracket picks (base64 in the URL). +- **Favorites** — star teams anywhere; their matches get highlighted across the app. +- **Time zones** — show kickoff times in your local time or the stadium's time. +- **Add to calendar** — download any match as an RFC 5545 `.ics` file. +- **Match modal** — details for every match, with space reserved for future stats. +- Responsive (mobile / tablet / desktop), keyboard-accessible, honors + `prefers-reduced-motion`. + +## Run locally + +```sh +python -m http.server +# open http://localhost:8000 +``` + +Any static file server works. A server **is required** — opening `index.html` directly +from disk fails because browsers block `fetch()` of local JSON files. + +## Deploy to GitHub Pages + +1. Push this repository to GitHub. +2. Repository **Settings → Pages → Source**: deploy from branch, `main` / root. +3. Done — the site works under `https://.github.io//` because every + asset and data path in the code is **relative** (never start a path with `/` + when editing). + +## Project structure + +``` +worldcup2026/ +├── index.html SPA shell (header, tabs, hero, panels, modal root) +├── assets/ +│ ├── css/ style.css · bracket.css · animations.css +│ ├── js/ app.js (entry) · schedule.js · groups.js · bracket.js +│ │ modal.js · stadiums.js · storage.js · i18n.js · calendar.js +│ └── images/ flags/*.svg · stadiums/*.svg (placeholders) +└── data/ ← the only thing you edit to maintain the site + ├── teams.json 48 teams: { id, name, flag } + ├── groups.json { "A": [4 team ids], … } × 12 + ├── matches.json 104 matches (UTC times; knockout uses bracketRef) + ├── results.json one entry per match: scores + status (+ penalties) + ├── stadiums.json name, city, capacity, image, IANA timezone + └── bracket-config.json Round-of-32 slots + best-third assignment +``` + +## Maintaining the data + +> The current files contain **mock data** (real country names, fictional results) +> so every feature can be exercised. Replace them with real data as the +> tournament unfolds — no code changes needed. + +### Updating a result + +Edit the match's entry in `data/results.json`: + +```json +{ "matchId": 74, "homeScore": 1, "awayScore": 1, + "penalties": { "home": 4, "away": 3 }, "status": "finished" } +``` + +- `status`: `scheduled` → `live` → `finished`. Standings and the bracket only + count `finished` matches. +- `penalties` is optional — only for knockout draws. + +### Adding / changing matches + +`data/matches.json`. **All times are UTC** (the UI converts to local or stadium +time). Group matches carry `homeTeam`/`awayTeam`; knockout matches carry a +`bracketRef` (`R32-1`…`R32-16`, `R16-1`…, `QF-…`, `SF-…`, `THIRD-PLACE`, `FINAL`) +and their teams are resolved automatically. + +### After the group stage: fill the third-place slots + +`data/bracket-config.json` is **the only file to edit** once the 8 best +third-placed teams are known. Map each slot to a group letter: + +```json +"thirdPlaceAssignment": { "1": "C", "2": "A", "3": null, … } +``` + +A slot's team becomes `standings[group][3rd]`. Slots left `null` show a +"Best 3rd #N" placeholder. The 16 `round32` entries define the bracket order +(array position = bracket position) — they normally never change. + +### Teams, stadiums, images + +- `teams.json` — `flag` is a path relative to `assets/images/` (e.g. `flags/bra.svg`). +- `stadiums.json` — `timezone` must be a valid IANA name (e.g. `America/Mexico_City`); + it drives the "stadium time" display and stays correct across DST. +- Replace the placeholder SVGs in `assets/images/` with real artwork keeping the + same file names (or update the JSON paths). + +### UI labels (EN/PT) + +Every UI string goes through `t(key)` — add new labels to **both** dictionaries +in `assets/js/i18n.js`. Data values (team/stadium names) are not translated. + +## Local storage + +| Key | Content | +|---|---| +| `wc2026_simulation` | `{ "R32-6": { "winner": "FRA", "score": "2-1" }, … }` | +| `wc2026_favorites` | `["BRA", "MEX"]` | +| `wc2026_prefs` | `{ "lang": "en"\|"pt", "timeMode": "local"\|"stadium", "lastTab": "bracket" }` | + +Clearing site data resets picks, favorites, and preferences — the JSON content +is never modified by the app. + +## Acceptance criteria (spec §18) + +- [x] All matches are loaded via JSON +- [x] All results are loaded via JSON +- [x] The bracket is generated dynamically (config + standings + winner pairing) +- [x] Works on GitHub Pages (all paths relative, no server-side code) +- [x] Works on desktop and mobile (≤767 / 768–1439 / 1440+ breakpoints) +- [x] Allows knockout-stage simulation (persisted, never mutates JSON) +- [x] Smooth animations (entry, hover, bracket line-draw; reduced-motion safe) +- [x] No backend dependency — fully static, works offline after first load + +**Performance:** total JS ≈ 74 KB across 9 ES modules (budget: < 300 KB), no +external dependencies, no blocking third-party requests. + +## Roadmap (spec §19) + +PWA install, dark/light theme, real-time statistics, results API, FIFA ranking, +World Cup history, team comparison, push notifications. diff --git a/how-update.md b/how-update.md new file mode 100644 index 0000000..ab0d69f --- /dev/null +++ b/how-update.md @@ -0,0 +1,251 @@ +# How to Update — Real Data Migration Guide + +This document is a self-contained runbook (and can be pasted as a prompt to an AI +agent) for replacing the **mock data** currently in `data/*.json` with **real FIFA +World Cup 2026** data. The app reads these files at runtime — if the JSON keeps the +exact same shape, **no code changes are needed**. + +--- + +## Ground rules — do not break these + +1. **Schema is frozen.** Every object key, every id format, every enum value + listed below must stay exactly as documented. The JS reads by exact field + name/value (e.g. `phase === "Group A"`, `status === "finished"`). +2. **Team ids are 3-letter uppercase codes** (FIFA/IOC style: `MEX`, `BRA`, `ARG`). + Whatever code you pick for a team must be used **identically** across + `teams.json`, `groups.json`, and every `homeTeam`/`awayTeam` in `matches.json`. +3. **All match `date`/`time` are UTC**, format `"YYYY-MM-DD"` / `"HH:MM"` (24h). + The UI converts to local/stadium time at render time — do not pre-convert. +4. **Paths stay relative** (`flags/xxx.svg`, `stadiums/xxx.svg`) — required for + GitHub Pages. +5. **Code files, localStorage keys, and i18n keys are out of scope.** This task + only touches files inside `data/` (plus optionally `assets/images/*`). +6. Work inside the **running preview** (`python -m http.server` / Claude Preview, + port 8126) and hard-reload after each file edit — `fetch()` doesn't work on + `file://` and JSON is aggressively cached by the browser (see gotcha #5 in + `.agents/project-memory.md`). + +--- + +## Where to find real data + +Use web search/fetch against authoritative sources, e.g.: + +- `https://www.fifa.com` — official site: confirmed teams, groups, fixtures, results, venues. +- `https://en.wikipedia.org/wiki/2026_FIFA_World_Cup` and its sub-articles + (groups, schedule, venues, squads) — usually the fastest single reference, + kept current during the tournament. + +Cross-check at least two sources for the schedule and current results, since +the tournament is live and Wikipedia/FIFA update at slightly different speeds. + +--- + +## Order of operations + +Each step depends on ids/names introduced by the previous one. Don't skip ahead. + +1. **`stadiums.json`** — confirm the host venues (names, cities, capacities, timezones). +2. **`teams.json`** — confirm the final 48 qualified teams and their codes. +3. **`groups.json`** — confirm the official group draw (A–L, 4 teams each), using ids from step 2. +4. **`bracket-config.json` → `round32`** — confirm the official Round-of-32 draw structure (fixed pre-tournament, rarely changes). +5. **`matches.json`** — full 104-match real schedule (dates/times/venues, `homeTeam`/`awayTeam` or `bracketRef`). +6. **`results.json`** — real scores/status as of today; future matches stay `scheduled`. +7. **`bracket-config.json` → `thirdPlaceAssignment`** — fill in once group-stage standings make it determinable (see step 7 detail below); leave `null` for slots not yet known. +8. **`assets/images/`** (optional) — swap placeholder SVGs for real flags/stadium art, same filenames or update the JSON path. + +--- + +## File-by-file reference + +### 1. `data/stadiums.json` — array, currently 30 entries + +```json +{ "id": 1, "name": "Estadio Azteca", "city": "Mexico City", "capacity": 87000, + "image": "stadiums/azteca.svg", "timezone": "America/Mexico_City" } +``` + +- `id`: integer, unique, referenced by nothing else (informational only — `matches.json` links by `name`/`city`, not `id`). +- `timezone`: valid IANA name — drives "stadium time" mode and must stay DST-correct. +- `image`: path relative to `assets/images/`. + +**Decision point:** the official 2026 tournament uses **16 venues** (3 in Mexico, +11 in USA, 2 in Canada), but this file currently lists **30** (the original bid +shortlist). Recommendation: trim to the 16 confirmed match venues so every row +in `stadiums.json` is actually used by `matches.json` — but confirm with the user +first if they'd rather keep the extra 14 as a broader "host city guide". + +### 2. `data/teams.json` — array, 48 entries + +```json +{ "id": "MEX", "name": "Mexico", "flag": "flags/mex.svg" } +``` + +- `id`: 3-letter uppercase code, **unique**, used as the cross-file key everywhere. +- `flag`: path relative to `assets/images/`, convention `flags/.svg`. +- `name`: display name, shown as-is (not translated — i18n covers UI strings only). + +### 3. `data/groups.json` — object, keys `"A"`–`"L"`, each an array of exactly 4 team ids + +```json +{ "A": ["MEX", "SUI", "KOR", "JAM"], "B": [...], ..., "L": [...] } +``` + +- 12 groups × 4 teams = 48 — must cover every id in `teams.json` exactly once. +- Order within the array doesn't affect standings (sorted by points/GD/GF), but + conventionally keep host nation first where applicable. + +### 4. `data/bracket-config.json` + +```json +{ + "round32": [ + { "id": "R32-1", "home": { "type": "group", "ref": "A", "pos": 1 }, + "away": { "type": "third", "slot": 1 } }, + ... + ], + "thirdPlaceAssignment": { "1": null, "2": null, ..., "8": null } +} +``` + +- `round32`: 16 entries, `id` = `"R32-1"`..`"R32-16"` (order = bracket position, + feeds `R16-1`..`R16-8` by sequential pairing `0-1→0, 2-3→1, …`, then `QF`, `SF`, + `FINAL`, with `THIRD-PLACE` from the two `SF` losers). + - `home`/`away` each are either: + - `{ "type": "group", "ref": "", "pos": 1 | 2 }` → group winner/runner-up, or + - `{ "type": "third", "slot": 1-8 }` → one of the 8 best third-placed teams. + - **Only edit `ref`/`pos`/`slot` values** if the *official* Round-of-32 draw + structure differs from what's encoded here — don't change `id` values or + array order (that's the bracket's visual/geometric layout, tied to the CSS). +- `thirdPlaceAssignment`: maps slot `"1"`–`"8"` → group letter `"A"`–`"L"` or + `null`. Fill in **step 7**, after the group stage actually finishes, following + FIFA's official "ranking of third-placed teams" rules. Until a slot is filled, + `resolveBracketTeams()` shows a "Best 3rd #N" placeholder — that's expected and + correct for slots not yet determined. + +### 5. `data/matches.json` — array, 104 entries, ids `1`–`104` + +ID ranges (keep this layout — `resultByMatchId` is keyed by `id`, not position, +but contiguous chronological ids match the existing convention): + +| ids | phase | count | +|---|---|---| +| 1–72 | `"Group A"` … `"Group L"` (6 matches each) | 72 | +| 73–88 | `"Round of 32"` | 16 | +| 89–96 | `"Round of 16"` | 8 | +| 97–100 | `"Quarterfinals"` | 4 | +| 101–102 | `"Semifinals"` | 2 | +| 103 | `"Third Place"` | 1 | +| 104 | `"Final"` | 1 | + +**Group-stage match:** + +```json +{ "id": 1, "phase": "Group A", "date": "2026-06-11", "time": "16:00", + "stadium": "Estadio Azteca", "city": "Mexico City", + "homeTeam": "MEX", "awayTeam": "SUI" } +``` + +- `homeTeam`/`awayTeam` **must** be 2 of the 4 ids listed for that group in + `groups.json` (see integrity rule #1 below — violating this crashes the + standings table). +- `stadium`/`city` must match a `name`/`city` pair in `stadiums.json` exactly + (case-sensitive string match used for cross-links and the stadiums view). + +**Knockout match:** + +```json +{ "id": 74, "phase": "Round of 32", "date": "2026-06-29", "time": "16:00", + "stadium": "Gillette Stadium", "city": "Boston", "bracketRef": "R32-2" } +``` + +- No `homeTeam`/`awayTeam` — teams are resolved at runtime from standings + + `bracket-config.json`. +- `bracketRef` must be exactly one of: `R32-1`..`R32-16`, `R16-1`..`R16-8`, + `QF-1`..`QF-4`, `SF-1`, `SF-2`, `THIRD-PLACE`, `FINAL` — each value used **once**. + +### 6. `data/results.json` — array, 104 entries, one per match id + +```json +{ "matchId": 1, "homeScore": 1, "awayScore": 1, "status": "finished" } +{ "matchId": 61, "homeScore": 1, "awayScore": 0, "status": "live" } +{ "matchId": 62, "homeScore": null, "awayScore": null, "status": "scheduled" } +{ "matchId": 74, "homeScore": 1, "awayScore": 1, "status": "finished", + "penalties": { "home": 4, "away": 3 } } +``` + +- `status`: `"scheduled"` (not started, scores `null`) → `"live"` (in progress, + real scores) → `"finished"` (full-time). +- `penalties`: **optional**, only on knockout matches finished level after + extra time — `homeScore`/`awayScore` stay the 90+30-min score (can be equal). +- Set this from real, up-to-date results as of "today". Anything not yet played + stays `"scheduled"` with `null`/`null` — this is the normal/expected state for + future fixtures, not a placeholder to "fix". +- Only `"finished"` matches feed standings (`groups.js`) and bracket resolution + (`bracket.js`) — a `"live"` match's score does **not** affect either. + +--- + +## Cross-file integrity checklist + +Run through this after editing, **before** trusting the preview: + +1. **Every Group-stage `homeTeam`/`awayTeam` in `matches.json` is one of the 4 + ids in `groups.json[]`.** If not, `computeStandings()` + throws (`rows.get(...)` is `undefined`) and the Groups tab breaks entirely. +2. **`groups.json` covers all 48 `teams.json` ids exactly once**, 4 per group, A–L. +3. **`results.json` has exactly one entry per `matches.json` id, 1–104, no gaps/dupes.** +4. **Every `bracketRef` value (`R32-1`..`R32-16`, `R16-1`..`R16-8`, `QF-1`..`QF-4`, + `SF-1`, `SF-2`, `THIRD-PLACE`, `FINAL`) appears exactly once** across the 32 + knockout entries in `matches.json`. +5. **`matches.json`'s `stadium`/`city` strings exist in `stadiums.json`** (exact + string match — used by `setStadiumFilter` cross-links). +6. **`bracket-config.json.round32[i].home/away` group `ref` values are valid + letters A–L** and `pos`/`slot` are in range (`pos`: 1-2, `slot`: 1-8), each + `slot` 1-8 used exactly once across the 16 entries. +7. **No team id appears in two different groups.** + +--- + +## Optional: real artwork + +`assets/images/flags/*.svg` and `assets/images/stadiums/*.svg` are currently +placeholders. To replace: + +- Keep the **same filenames** referenced by `teams.json`/`stadiums.json` (e.g. + `flags/mex.svg`), or update the `flag`/`image` JSON value if you use different + names. +- SVG is not required by the code — any image format works as long as the path + in JSON matches the actual file extension. + +--- + +## Verification steps + +1. `preview_start` (or `python -m http.server` from the project root), then hard + reload (`location.reload()` after `fetch(..., { cache: 'reload' })`, or + DevTools "Empty Cache and Hard Reload"). +2. **Home** — hero shows the right live/next match; dashboard counts look sane. +3. **Matches** — spot-check a few real fixtures (date/time/venue), filters + (group/phase/team/stadium) still populate correctly. +4. **Groups** — all 12 tables show real teams, standings order matches a + real-world source for finished matchdays. +5. **Knockout** — R32 slots show real teams where standings allow, placeholders + ("Group X Winner", "Best 3rd #N") elsewhere; hover-path highlight and zoom + still work. +6. **Stadiums** — cards match the trimmed/confirmed venue list; "view matches" + cross-link filters correctly. +7. Toggle **EN/PT** and **local/stadium time** — no console errors, times shift + correctly across at least two different stadium timezones. + +--- + +## After finishing + +Per this project's conventions, update `.agents/`: + +- Check off the relevant items in `.agents/TODO.md` section 6 (Post-launch). +- Append a dated entry to `.agents/project-memory.md` documenting: data source + used, the stadiums 30→16 decision (and final list if trimmed), and the + `thirdPlaceAssignment` status (filled / partially filled / still null).