docs: rewrite README as a showcase and add DEVELOPMENT.md

This commit is contained in:
Lucas Kalil 2026-07-04 17:32:21 -03:00
parent 6c537609a5
commit a3372f8399
10 changed files with 258 additions and 127 deletions

View file

@ -116,7 +116,16 @@ worldcup2026/
│ awards.json, keeper-stats.json, curiosities.json, │ awards.json, keeper-stats.json, curiosities.json,
│ all-time-baselines.json — absent = silent empty default │ all-time-baselines.json — absent = silent empty default
├── README.md Setup, GitHub Pages deploy, JSON maintenance guide ├── 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-update.md Real-data migration runbook (mock → real — DONE 2026-06-12)
├── how-refresh-data.md ★ Daily refresh runbook during the tournament: ├── how-refresh-data.md ★ Daily refresh runbook during the tournament:
│ results.json scores/status + one-time │ results.json scores/status + one-time

View file

@ -344,10 +344,10 @@ a separate commit keeps the data commit's diff clean.
- **Gotcha:** the Hostinger FTP account logs in **already inside `public_html`**, so `server-dir` is - **Gotcha:** the Hostinger FTP account logs in **already inside `public_html`**, so `server-dir` is
relative to it — do **not** prefix `public_html/` (causes `public_html/public_html/...`). Final relative to it — do **not** prefix `public_html/` (causes `public_html/public_html/...`). Final
path: `public_html/worldcup2026/`. If FTPS is rejected, switch `protocol` to `ftp`. path: `public_html/worldcup2026/`. If FTPS is rejected, switch `protocol` to `ftp`.
- `exclude` removes `.git*`, `.github/`, `.agents/`, `README.md`, `how-*.md`, `*-en.md` specs — only - `exclude` removes `.git*`, `.github/`, `.agents/`, **`docs/`**, `README.md`, **`DEVELOPMENT.md`**,
`index.html` + `assets/` + `data/` reach the site. New `data/` / `manifest.json` / `assets/icons/` `how-*.md`, `*-en.md` specs — only `index.html` + `assets/` + `data/` reach the site. New `data/` /
files **are** deployed. Incremental sync state (`.ftp-deploy-sync-state.json`) lives only on the `manifest.json` / `assets/icons/` files **are** deployed. Incremental sync state
server — don't commit it. (`.ftp-deploy-sync-state.json`) lives only on the server — don't commit it.
### Real-data migration (DONE 2026-06-12) ### Real-data migration (DONE 2026-06-12)
All 6 `data/*.json` hold real WC2026 data (sources: Wikipedia per-group + knockout articles, All 6 `data/*.json` hold real WC2026 data (sources: Wikipedia per-group + knockout articles,
@ -550,6 +550,21 @@ Edge fades via `mask-image` toggled by `updateTabFades()`; active tab kept visib
page). The time button collapses to a 🕐 icon at ≤420px (a11y intact via `data-i18n-aria`). This page). The time button collapses to a 🕐 icon at ≤420px (a11y intact via `data-i18n-aria`). This
supersedes the old "7681439 single-row header" note. supersedes the old "7681439 single-row header" note.
### Docs — README showcase + DEVELOPMENT.md split (2026-07-04)
The root README was reframed (via /grill-me) from a dev/maintenance guide into a **non-technical
showcase** (English): tagline, shields.io badges, prominent live-demo link
(**https://lucaskalil.com/worldcup2026** — the public URL; not previously recorded anywhere),
per-page **screenshot gallery** (Home/Matches/Groups/Knockout/Stadiums/Stats), and a plain-language
"Under the hood" section. All the old technical content (run locally, project structure, JSON
maintenance, local storage, deploy, acceptance criteria, roadmap) moved to a new **`DEVELOPMENT.md`**;
the README's stale "mock data / GitHub Pages" framing was corrected to real-data + the real Hostinger
deploy. Screenshots live in **`docs/screenshots/*.png`**, captured with **headless Edge**
(`msedge --headless=new --window-size=1366,H --virtual-time-budget=6000 --lang=en-US --screenshot`,
`--lang=en-US` forces the EN UI; served from Claude Preview `worldcup2026` on :8126) — repeat that to
refresh them. **Deploy exclude updated** to drop `docs/` + `DEVELOPMENT.md` (docs never ship to the
live site — see Deploy runbook). No app-code/version change (`APP_VERSION` untouched; docs are
excluded from deploy).
### How to record a decision (after finishing a unit of work) ### How to record a decision (after finishing a unit of work)
1. Tick the item in `.agents/TODO.md`. 1. Tick the item in `.agents/TODO.md`.
2. Append the new decision/gotcha/pattern to the right section here (don't rewrite existing entries; 2. Append the new decision/gotcha/pattern to the right section here (don't rewrite existing entries;

164
DEVELOPMENT.md Normal file
View file

@ -0,0 +1,164 @@
# Developer guide — World Cup 2026 Hub
Everything you need to run, understand, deploy and maintain the site. For a non-technical overview of
what the project *is*, see the [README](README.md).
The app is a **static single-page app**: one `index.html`, vanilla ES-module JavaScript, and JSON
files as the only "database". There is **no backend, no build step, no bundler, and no framework**.
You maintain the site by editing JSON — the code never needs to change to update scores or teams.
---
## Run locally
```sh
python -m http.server
# then 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 (`file://`).
---
## Project structure
```
worldcup2026/
├── index.html SPA shell (header, tabs, hero, panels, modal root)
├── manifest.json PWA manifest · favicon.ico
├── assets/
│ ├── css/ style.css · bracket.css · stats.css · animations.css
│ ├── js/ app.js (entry) · schedule.js · groups.js · bracket.js
│ │ stats.js · modal.js · stadiums.js · storage.js
│ │ i18n.js · calendar.js
│ ├── images/ flags/*.svg · stadiums/*.svg
│ └── icons/ PWA app icons + favicons
└── 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, + optional stats)
├── stadiums.json name, city, capacity, image, IANA timezone
└── bracket-config.json Round-of-32 slots + best-third assignment
```
---
## Maintaining the data
> The data is **real World Cup 2026 data**, refreshed as the tournament progresses. Everything the
> site shows is derived from the six `data/*.json` files — no code changes required.
>
> The day-to-day refresh routine is documented in
> [`how-refresh-data.md`](how-refresh-data.md); the original mock → real migration is kept as a schema
> reference in [`how-update.md`](how-update.md).
### 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 matches decided on penalties.
### 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 from the standings.
### 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 (per FIFA's official combination table):
```json
"thirdPlaceAssignment": { "1": "D", "2": "F", "3": "B", "…": "…" }
```
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 SVGs in `assets/images/` with new artwork keeping the same file names (or update the
JSON paths).
### UI labels (EN / PT)
Every user-facing string goes through `t(key)` — add new labels to **both** dictionaries in
`assets/js/i18n.js`. Data values (team/stadium names, cities) come from JSON and are **not**
translated.
---
## Local storage
The app never modifies the JSON data. User state lives in the browser under `wc2026_*` keys:
| 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, favourites and preferences.
---
## Deployment
The live site is deployed automatically to **Hostinger over FTP** by GitHub Actions
([`.github/workflows/deploy.yml`](.github/workflows/deploy.yml)) on every push to `master`, and is
served at **[lucaskalil.com/worldcup2026](https://lucaskalil.com/worldcup2026)**.
- The workflow needs three repository secrets: `FTP_SERVER`, `FTP_USERNAME`, `FTP_PASSWORD`.
- Development/documentation files (`README.md`, `DEVELOPMENT.md`, `docs/`, `.agents/`, the specs and
`how-*.md`) are **excluded** from the upload — only `index.html`, `assets/` and `data/` reach the
site.
Because every asset and data path in the code is **relative** (never starting with `/`), the same
folder also works unchanged on **GitHub Pages** or any other static host — just publish the directory.
When editing paths, always keep them relative.
---
## 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 a static host (all paths relative, no server-side code)
- [x] Works on desktop and mobile (≤767 / 7681100 / 1100+ 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
**Performance:** total JS ≈ 74 KB across the ES modules (budget: < 300 KB), no external dependencies,
no blocking third-party requests.
---
## Roadmap (spec §19)
Dark/light theme, real-time statistics via a results API, FIFA ranking integration, World Cup history,
expanded player-level stats, and push notifications. A service-worker offline mode (PWA "Tier 2") is
designed but deliberately deferred — see [`.agents/issues.md`](.agents/issues.md).
---
## Internal documentation
Deeper architecture notes, decisions and gotchas live in the [`.agents/`](.agents/) folder
(`project-map.md`, `project-memory.md`, `stats-screen-plan.md`, `issues.md`, `TODO.md`). Read those
before making significant changes.

187
README.md
View file

@ -1,155 +1,98 @@
# World Cup 2026 Hub <div align="center">
A static, single-page hub for the FIFA World Cup 2026 (Mexico · USA · Canada, 48 teams): # 🏆 World Cup 2026 Hub
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). ### Follow the entire FIFA World Cup 2026 in one beautiful place — the schedule, live group standings, an interactive knockout bracket you can predict, the stadiums, and full tournament stats.
[![Live demo](https://img.shields.io/badge/Live_demo-lucaskalil.com%2Fworldcup2026-e6b800?style=for-the-badge)](https://lucaskalil.com/worldcup2026)
![Vanilla JS](https://img.shields.io/badge/Vanilla_JS-no_frameworks-f7df1e?style=flat-square)
![Zero dependencies](https://img.shields.io/badge/Dependencies-zero-2ea44f?style=flat-square)
![PWA](https://img.shields.io/badge/PWA-installable-5a0fc8?style=flat-square)
![JavaScript size](https://img.shields.io/badge/JavaScript-~74_KB-00b4d8?style=flat-square)
![Languages](https://img.shields.io/badge/UI-EN_%2F_PT-lightgrey?style=flat-square)
<img src="docs/screenshots/home.png" alt="World Cup 2026 Hub home screen — the next match with a live countdown and a tournament overview" width="100%">
</div>
--- ---
## Features ## What is this?
- **Schedule** — all 104 matches with filters (date, group, phase, team, stadium), **World Cup 2026 Hub** is a fan-made website for the FIFA World Cup 2026 — the first tournament
free-text search, date sorting, and a "My matches" favorites filter. hosted across **Mexico, the USA and Canada**, with **48 teams** and **104 matches**.
- **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.
- **Stats** — a sub-navigated screen (Overview · Teams · Records · Comparator):
tournament-to-date aggregates and goals-by-stage/round charts; a verdict hero
(champion + podium) that takes over once the final is played; a final ranking
148 by stage reached; team record cards (biggest win, win streak, champion's
path); a "format debuts" band; and an A-vs-B team comparator. Sections appear
only when they have data (graceful degradation).
- Responsive (mobile / tablet / desktop), keyboard-accessible, honors
`prefers-reduced-motion`.
## Run locally It pulls the whole tournament onto a single, fast page: when the next match kicks off, how every
group is shaping up, who advances, where the games are played, and how *you'd* fill out the knockout
bracket. Results roll in as the tournament goes, and you can flip the entire interface between
**English and Português** at any time.
```sh Nothing to install (although you can — see [Under the hood](#-under-the-hood-for-the-curious)), no
python -m http.server sign-up, no ads. Just open it.
# open http://localhost:8000
```
Any static file server works. A server **is required** — opening `index.html` directly > 🔗 **Try it now → [lucaskalil.com/worldcup2026](https://lucaskalil.com/worldcup2026)**
from disk fails because browsers block `fetch()` of local JSON files.
## Deploy to GitHub Pages ---
1. Push this repository to GitHub. ## 🎬 Explore the pages
2. Repository **Settings → Pages → Source**: deploy from branch, `main` / root.
3. Done — the site works under `https://<user>.github.io/<repo>/` because every
asset and data path in the code is **relative** (never start a path with `/`
when editing).
## Project structure ### 🏠 Home
*The next match with a live countdown, plus the tournament at a glance.*
``` <img src="docs/screenshots/home.png" alt="Home — next match (Paraguay vs France, Round of 16) with a countdown and overview cards" width="100%">
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 ### 📅 Matches
*All 104 games in one place — search by team, city or stadium, filter by date, group, phase, team or venue, and flag your favourites with “My matches”.*
> The current files contain **mock data** (real country names, fictional results) <img src="docs/screenshots/matches.png" alt="Matches — a searchable, filterable grid of all 104 fixtures with scores and venues" width="100%">
> so every feature can be exercised. Replace them with real data as the
> tournament unfolds — no code changes needed.
### Updating a result ### 📊 Groups
*Live standings for all 12 groups, worked out automatically from the results — points, goal difference, goals scored — with clear markers for who qualifies (and whos chasing a best-third-place spot).*
Edit the match's entry in `data/results.json`: <img src="docs/screenshots/groups.png" alt="Groups — standings tables for all twelve groups AL with qualification highlights" width="100%">
```json ### 🏆 Knockout bracket
{ "matchId": 74, "homeScore": 1, "awayScore": 1, *The centrepiece. A gorgeous interactive bracket that fills itself in as teams advance. Hover a team to trace its whole path to the final, zoom and pan around, switch between three layouts — **and predict it yourself**: pick winners, watch the rounds re-draw, then see how your picks score against the real results. You can even share your bracket as a link.*
"penalties": { "home": 4, "away": 3 }, "status": "finished" }
```
- `status`: `scheduled``live``finished`. Standings and the bracket only <img src="docs/screenshots/bracket.png" alt="Knockout — the center-out wallchart bracket with prediction and challenge features" width="100%">
count `finished` matches.
- `penalties` is optional — only for knockout draws.
### Adding / changing matches ### 🏟️ Stadiums
*All 16 host venues across the three countries, each with its capacity and a jump straight to the matches played there.*
`data/matches.json`. **All times are UTC** (the UI converts to local or stadium <img src="docs/screenshots/stadiums.png" alt="Stadiums — cards for the sixteen host venues with capacity and match links" width="100%">
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 ### 📈 Stats
*A tournament stats screen: goals by stage and by round, records, a full 148 ranking and a head-to-head team comparator. Once the final is played, a champion “verdict” takes over the top of the page.*
`data/bracket-config.json` is **the only file to edit** once the 8 best <img src="docs/screenshots/stats.png" alt="Stats — tournament-to-date totals, goals-by-stage and goals-by-round charts" width="100%">
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 ## ✨ Under the hood (for the curious)
"Best 3rd #N" placeholder. The 16 `round32` entries define the bracket order
(array position = bracket position) — they normally never change.
### Teams, stadiums, images You don't need to be a developer to appreciate a few things that make this special:
- `teams.json``flag` is a path relative to `assets/images/` (e.g. `flags/bra.svg`). - **⚡ Loads in a blink.** The whole site is about **74 KB** of JavaScript — smaller than a single
- `stadiums.json``timezone` must be a valid IANA name (e.g. `America/Mexico_City`); phone photo — with **no frameworks and no libraries**. It's just clean, hand-written code.
it drives the "stadium time" display and stays correct across DST. - **🔮 Predict the bracket.** Choose winners and the bracket redraws all the way to the final; your
- Replace the placeholder SVGs in `assets/images/` with real artwork keeping the picks are saved in your browser and can be handed to a friend as a link to compare.
same file names (or update the JSON paths). - **🔄 Always current.** New scores appear on their own — no need to refresh the page.
- **📱 Works everywhere.** Phone, tablet or desktop, the layout adapts to fit.
- **⬇️ Installable.** Add it to your home screen and it opens like a real app (it's a PWA).
- **🌍 Two languages.** Every label is available in English and Portuguese, switchable on the fly.
- **♿ Built to be usable by everyone.** Full keyboard navigation, screen-reader labels, and it
respects the system “reduce motion” setting.
### UI labels (EN/PT) Curious how it's put together, or want to run it yourself? See **[DEVELOPMENT.md](DEVELOPMENT.md)**.
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 <div align="center">
| Key | Content | Made by **[Lucas Kalil](https://lucaskalil.com)** · Vanilla HTML, CSS & JavaScript.
|---|---|
| `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 <sub>A fan-made project for the love of the game. Not affiliated with, endorsed by, or sponsored by
is never modified by the app. FIFA. All team and venue names belong to their respective owners.</sub>
## Acceptance criteria (spec §18) </div>
- [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 / 7681439 / 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

BIN
docs/screenshots/groups.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

BIN
docs/screenshots/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 KiB

BIN
docs/screenshots/stats.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB