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,
│ 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-refresh-data.md ★ Daily refresh runbook during the tournament:
│ 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
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`.
- `exclude` removes `.git*`, `.github/`, `.agents/`, `README.md`, `how-*.md`, `*-en.md` specs — only
`index.html` + `assets/` + `data/` reach the site. New `data/` / `manifest.json` / `assets/icons/`
files **are** deployed. Incremental sync state (`.ftp-deploy-sync-state.json`) lives only on the
server — don't commit it.
- `exclude` removes `.git*`, `.github/`, `.agents/`, **`docs/`**, `README.md`, **`DEVELOPMENT.md`**,
`how-*.md`, `*-en.md` specs — only `index.html` + `assets/` + `data/` reach the site. New `data/` /
`manifest.json` / `assets/icons/` files **are** deployed. Incremental sync state
(`.ftp-deploy-sync-state.json`) lives only on the server — don't commit it.
### Real-data migration (DONE 2026-06-12)
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
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)
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;

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):
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.
# 🏆 World Cup 2026 Hub
**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),
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.
- **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`.
**World Cup 2026 Hub** is a fan-made website for the FIFA World Cup 2026 — the first tournament
hosted across **Mexico, the USA and Canada**, with **48 teams** and **104 matches**.
## 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
python -m http.server
# open http://localhost:8000
```
Nothing to install (although you can — see [Under the hood](#-under-the-hood-for-the-curious)), no
sign-up, no ads. Just open it.
Any static file server works. A server **is required** — opening `index.html` directly
from disk fails because browsers block `fetch()` of local JSON files.
> 🔗 **Try it now → [lucaskalil.com/worldcup2026](https://lucaskalil.com/worldcup2026)**
## 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://<user>.github.io/<repo>/` because every
asset and data path in the code is **relative** (never start a path with `/`
when editing).
## 🎬 Explore the pages
## Project structure
### 🏠 Home
*The next match with a live countdown, plus the tournament at a glance.*
```
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
```
<img src="docs/screenshots/home.png" alt="Home — next match (Paraguay vs France, Round of 16) with a countdown and overview cards" width="100%">
## 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)
> so every feature can be exercised. Replace them with real data as the
> tournament unfolds — no code changes needed.
<img src="docs/screenshots/matches.png" alt="Matches — a searchable, filterable grid of all 104 fixtures with scores and venues" width="100%">
### 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
{ "matchId": 74, "homeScore": 1, "awayScore": 1,
"penalties": { "home": 4, "away": 3 }, "status": "finished" }
```
### 🏆 Knockout bracket
*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.*
- `status`: `scheduled``live``finished`. Standings and the bracket only
count `finished` matches.
- `penalties` is optional — only for knockout draws.
<img src="docs/screenshots/bracket.png" alt="Knockout — the center-out wallchart bracket with prediction and challenge features" width="100%">
### 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
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.
<img src="docs/screenshots/stadiums.png" alt="Stadiums — cards for the sixteen host venues with capacity and match links" width="100%">
### 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
third-placed teams are known. Map each slot to a group letter:
<img src="docs/screenshots/stats.png" alt="Stats — tournament-to-date totals, goals-by-stage and goals-by-round charts" width="100%">
```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.
## ✨ Under the hood (for the curious)
### 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`).
- `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).
- **⚡ Loads in a blink.** The whole site is about **74 KB** of JavaScript — smaller than a single
phone photo — with **no frameworks and no libraries**. It's just clean, hand-written code.
- **🔮 Predict the bracket.** Choose winners and the bracket redraws all the way to the final; your
picks are saved in your browser and can be handed to a friend as a link to compare.
- **🔄 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 |
|---|---|
| `wc2026_simulation` | `{ "R32-6": { "winner": "FRA", "score": "2-1" }, … }` |
| `wc2026_favorites` | `["BRA", "MEX"]` |
| `wc2026_prefs` | `{ "lang": "en"\|"pt", "timeMode": "local"\|"stadium", "lastTab": "bracket" }` |
Made by **[Lucas Kalil](https://lucaskalil.com)** · Vanilla HTML, CSS & JavaScript.
Clearing site data resets picks, favorites, and preferences — the JSON content
is never modified by the app.
<sub>A fan-made project for the love of the game. Not affiliated with, endorsed by, or sponsored by
FIFA. All team and venue names belong to their respective owners.</sub>
## 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 / 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.
</div>

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