The Fast Fixture-Entry UI: Click-to-Pair Teams Into a Gameweek
Premier League Predictor: Astro
Chapter 4 · The Fast Fixture-Entry UI: Click-to-Pair Teams Into a Gameweek
Ten fixtures, every single gameweek, for 38 gameweeks — filling that in through two dropdowns per fixture would be genuinely tedious. This chapter builds the real alternative promised back in Chapter 1: a real Astro page with 20 always-visible team buttons, clicked directly into place, backed by two new API routes for gameweeks and fixtures.
Creating a Gameweek
UNIQUE (season_id, number) already stops a genuinely duplicate gameweek from ever being stored. better-sqlite3 reports the violation as a real SqliteError instance with a .code property matching SQLite's own extended result codes — 'SQLITE_CONSTRAINT_UNIQUE' here, and 'SQLITE_CONSTRAINT_CHECK' for the fixture chapter's own team-can't-play-itself rule below. Checking that exact string is enough to turn a real database guarantee into a genuinely useful 409 response. There's no equivalent to SQLAlchemy's own required db.rollback() step here either — a single .run() call that throws simply never took effect at all, so there's no half-finished session state left behind that needs explicitly clearing before the database can be used again.
Creating a Fixture: The Real Server-Side Guard
POST handler above is that application layer — a real, explicit query across every existing fixture in the gameweek, not a database constraint, run in plain JavaScript exactly the same way the FastAPI sibling runs it in Python. Checking home_team_id === away_team_id here as well, even though Chapter 2's own CHECK constraint already blocks it at the database level, means a bad request gets a clean 400 with a real message instead of an unhandled SqliteError — the exact same "friendly error over a raw crash" reasoning as the gameweek route above.
The Click-to-Pair Interface
The whole point: every one of the season's 20 teams is a button, always visible, always one click away — no scrolling through a dropdown to find a name. A minimal Astro page for now; Chapter 10 replaces the hardcoded seasonId/gameweekId below with a real selector.
<script> tag written directly inside an Astro page's own template, with no extra attributes, is automatically processed by Astro: bundled, converted to a real ES module, and deduplicated. That processing has a real, non-obvious consequence — a module's own top-level functions are scoped to that module, not attached to the global window object the way an ordinary, unprocessed script's functions would be. Writing <button onclick="clearSelection()"> the way the FastAPI sibling's own plain, unbundled static file safely can would silently fail here, since the browser's own inline event-handler attribute looks for clearSelection on window and finds nothing there. Every button interaction above is wired up with a real addEventListener call instead — Astro's own documented, recommended fix — which works regardless of whether the function it's calling was ever exposed globally.
usedTeamIds above lives only in the page's own memory — it starts empty every time the page loads, and only grows as fixtures are actually added during that visit. Reload the page halfway through entering a gameweek and every team button re-enables, even ones already fixtured. That's fine, because the fixture route's own server-side check (querying every existing fixture in the gameweek) is the real authority — a stale client would simply get a real 409 back rather than silently creating a bad duplicate. A more complete version would call GET /api/gameweeks/{id}/fixtures on page load and seed usedTeamIds from the real, current state, rather than assuming a fresh visit means a fresh gameweek — left out here to keep the example focused on the click-to-pair interaction itself.
Where This Course Is Headed
Recording all four prediction sources against each fixture this chapter creates (Chapter 5); entering results — the real UPDATE Chapter 2 set up, filling in the home_score/away_score this chapter's own fixtures still leave null (Chapter 6); the real league table (Chapter 7); the prediction league table (Chapter 8); promotion and relegation (Chapter 9); and Astro islands replacing this chapter's own hardcoded seasonId/gameweekId with a real gameweek/season selector (Chapter 10).
Hands-On Exercises
Explain why the gameweek and fixture routes both check their own rule in JavaScript (the 1-38 range, home_team_id === away_team_id) even though a database-level guarantee (a CHECK or UNIQUE constraint) already exists for each one.
📄 View solutionExplain why usedTeamIds is described as "convenience, not the real rule," and describe exactly what happens — on both the client and the server — if a stale page somehow submits a fixture for a team that's already been used in that gameweek.
📄 View solutionBuild the full click-to-pair page yourself against a real season and gameweek, enter all 10 fixtures for a gameweek, then explain what would actually happen if the Clear and Add Fixture buttons used inline onclick attributes instead of addEventListener, given how Astro processes this script by default.
📄 View solutionChapter 4 Quick Reference
- POST /api/seasons/{id}/gameweeks — creates a gameweek (1-38); catches better-sqlite3's own SqliteError with code 'SQLITE_CONSTRAINT_UNIQUE' as a friendly 409, no rollback step needed
- POST /api/gameweeks/{id}/fixtures — the real authority for "no team twice in a gameweek," checked by querying every existing fixture in that gameweek
- Frontend — a real Astro page with an inline, Astro-processed script; 20 always-visible team buttons instead of dropdowns
- Genuine Astro gotcha — a processed script runs as an ES module, so its functions aren't on window; inline onclick attributes silently fail, and addEventListener is the real, documented fix
- Click-to-pair flow — first click fills Home, second (different team) fills Away, then Add Fixture
- Real limit — client-side usedTeamIds resets on page reload; the server-side check in the fixture route is what actually prevents a bad duplicate
- Next chapter: Recording the user, expert, guest(s) & AI predictions per fixture