- Java 69%
- JavaScript 20.9%
- CSS 5.8%
- HTML 3.6%
- Dockerfile 0.7%
| src | ||
| .dockerignore | ||
| .gitignore | ||
| DEPLOYMENT.md | ||
| Dockerfile | ||
| friends.json | ||
| groups.json | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| pom.xml | ||
| quotes.json | ||
| README.md | ||
| screenshot.png | ||
| vitest.config.js | ||
World Cup Sweepstakes Dashboard
Tracks which friend/team is the dirtiest (cards) and worst (points / goal difference) across your group's World Cup sweepstakes.
- Backend: Java 17 + Spring Boot (REST API + static frontend)
- Frontend: plain HTML/CSS/JS, polls the backend every 60s
- Data source: API-Football v3
Contents
- Get an API-Football key
- Configure friends & teams
- Configure the tournament
- Run it
- How it works
- Scoring
- API endpoint
- Deploying
- Forking this for your own group or tournament
- Who built this
Get an API-Football key
Sign up at API-Football and grab an API key from your dashboard. Note that the free plan's season coverage is limited, check their docs for which seasons/competitions your plan includes.
Set the key as an environment variable before running the app:
export API_FOOTBALL_KEY=your-key-here
Configure friends & teams
Edit friends.json at the project root. Each entry is a friend and their teams.
[
{"friend": "Alice", "teams": ["Argentina", "France", "Japan", "Morocco"], "flag": "🏴"},
...
]
flag is optional. Any emoji or short text, shown next to the friend's name
on the dashboard.
This file is read from disk on every app startup, so you can edit it without rebuilding.
Quotes
Edit quotes.json
A list of quotes (inspriational or maybe some friend lore) shown on "Today's Fixtures" when two friends' teams face each other:
[
"Bragging rights are on the line tonight."
]
Each fixture gets a quote picked deterministically from this list.
Also read from disk on startup, no rebuild needed.
Configure the tournament
In src/main/resources/application.yml:
api-football:
league-id: 1 # 1 = FIFA World Cup in API-Football
season: 2026 # tournament year
Run it
mvn spring-boot:run
Then open http://localhost:8080.
How it works
The app refreshes fixtures from API-Football on startup and at defined intervals (sweepstakes.refresh-interval-ms)
It caches finished-fixture results to data/fixture-cache.json so they're never re-fetched.
The dashboard polls /api/leaderboard every 60s, just reading the cached result from memory.
Scoring
- Dirtiest (team or friend) = yellow cards + 2 × red cards + 0.5 × fouls + 3 × penalties conceded.
- Worst = lowest points (3/win, 1/draw), tie-broken by most goals conceded, then most shots conceded.
API endpoint
GET /api/leaderboardcurrent aggregated stats (JSON)
Deploying
See DEPLOYMENT.md for Cloud Run deployment and custom domain setup.
Forking this for your own group or tournament
This repo is intentionally generic. The dashboard, scoring, and "Today's
Fixtures" logic don't assume any specific tournament, friend count, or number
of teams per friend. friends.json, quotes.json and groups.json ship with
placeholder content you can edit directly:
-
Fill in your own group:
friends.json: one entry per person with their team picks (any number of teams per person). Team names must match API-Football's naming for the competition you're tracking.quotes.json: any list of quotes or friendship lore, shown on fixture cards when two friends' teams play each other.groups.json: the tournament's group-stage draw (group name -> team names). Update for your competition.
-
Set the competition in
src/main/resources/application.yml:api-football: league-id: 1 # look up the right ID for your competition in API-Football's docs season: 2026 # the season/year for that competition -
Update branding
index.html(title/heading) andstyle.css(color theme) are currently World Cup 2026-themed; tweak for your event. -
Tune scoring (optional) The highly advanced dirty-score formula (yellow/red cards, fouls, penalties) is in
TeamRecord.javaif you want different weightings. -
Run tests with
mvn test && npm test -
Run locally (
mvn spring-boot:run) to check everything loads, then deploy per DEPLOYMENT.md with your own GCP project and domain.
Who built this
Made with love as a pair programming endeavour between me (a human) and claude code. Respect our robot overlords. #noyolo