New
Apps 1,895Makers 1,890This week 1,690
Promote your app−30%
Premier League fixture viewer screenshot 1
0views in
the last 30 days

Premier League fixture viewer

Unofficial Premier League fixture viewer — kickoff times in your timezone, where to watch

Built with Claude Code

by ismayc · Unclaimed · from USUnited States · added Oct 29, 2026
no ratings0 comments

Premier League fixture viewer

A React + Vite web app showing all 380 matches of the Premier League season in your timezone, with where to watch, the live table, season statistics, team sheets, and every final table since the competition began in 1992-93.

🔗 Live: https://premier-league-viewer.netlify.app · https://ismayc.github.io/premier-league/

The fixtures, table and stats views

Features

  • Your timezone — kickoff times auto-convert to your detected timezone (switchable to 15+), with the zone shown on each card and UK time alongside in the match detail, so the app can be checked against any published fixture list.
  • Five views — a chronological Fixtures list grouped by day, a Monday–Sunday Week calendar, the live Table, season Stats, and History back to 1992-93.
  • Fixture cards — each match shows kickoff time and zone (or a Final / Live badge), both clubs, the score, the venue, and where to watch once broadcasters are assigned. Styled after a clean schedule card and stacked so club names stay legible on a phone.
  • Follow clubs — star any club to highlight it everywhere and filter the fixtures to just the clubs you follow (saved in your browser).
  • Live alerts — switch on the bell and the app toasts goals, red cards, kick-off and full time as they happen, narrowed to the clubs you follow. Detected by diffing successive polls, so it needs no play-by-play feed.
  • My services — tell the app which streaming services and TV packages you have and the fixtures list can be narrowed to matches you can actually watch. Kept in your browser rather than the URL, since a shared link should not filter someone else's list by your subscriptions.
  • Next-kickoff bar — a live countdown to the next match still to come.
  • International breaks, named — the league stops for two or three weeks in September/October, November and March, and a fixture list that silently jumps a fortnight looks like a bug. Every view that shows the hole now captions it: between the two day sections it separates, between two collapsed matchweeks, either side of the weeks the Week grid skips, inside a club's next fixtures, and above the next-match banner — with a countdown to the first kickoff back — while a break is on. Opening a match on either edge of one says which edge. Derived from the fixture list itself (see Design notes), so there is no calendar of windows to keep up to date.
  • Live table — the standings computed from results as they land, with home/away splits, recent form, and the Champions League / Europa / Conference / relegation bands (named in a legend, never colour alone).
  • Stats — player leaderboards across ten categories (goals, assists, shots, saves, cards…) with a season switcher back to 2017-18; click a player for their season tally plus position, nationality and height. Plus season totals and an attack-and-defence chart for the current season or any past one.
  • History — every final table since 1992-93, computed from match results (not copied) and verified; an all-time table ranked by points and points-per-match; and any club's finishing position season by season as a chart.
  • Team drawer — click a club anywhere for its form, home and away records, upcoming fixtures, leading scorers, and full season-by-season history.
  • Team sheets — opening a match shows both formations, the starting XI by line, the bench, and the substitutions with the minute each was made. Click a player to expand their match — goals, assists, shots, fouls, cards — with their position, nationality and height. (Lineups appear about an hour before kickoff.)
  • Add to calendar — per-match and whole-season .ics export, optionally filtered to the clubs you follow.
  • Spoiler-free mode — on by default; scores are hidden globally, so a finished match reads as an ordinary upcoming card until you open it. One header toggle turns scores on, and that choice travels in a shared link.
  • Light/dark theme — follows your system preference, with no flash on load.
  • Shareable URLs — view, timezone, spoiler mode, club and season persist to the query string; links unfurl with a title/description/image preview in chat apps.
  • Accessible — keyboard-navigable, focus-trapped modals that restore focus on close, screen-reader labels on live/score badges, and data never carried by colour alone.
  • Live results — final scores and in-match score + clock overlaid from ESPN's public scoreboard (no API key) while games are underway, auto-refreshed; if the fetch fails the committed data still renders.

Views

View What it shows
Fixtures The season as a chronological list, grouped by day, with a next-kickoff countdown. Filter to followed clubs, show or hide played matches, export to calendar.
Week A Monday-to-Sunday grid. Makes an empty midweek and a congested festive period legible at a glance.
Table The live league table, with home/away splits, recent form, and European / relegation bands.
Stats League totals, player leaderboards across ten categories with a season switcher, per-player biographies, and a per-club goal-difference chart for any season.
History Every final table since 1992-93, an all-time table, and any club's finishing position season by season.

Running it

npm install
npm run dev

npm test runs the suite; npm run test:coverage adds the coverage report; npm run build produces a static dist/.

The suite is held at 100% coverage — statements, branches, functions and lines — and vite.config.js sets thresholds so CI fails if anything ships untested. Test files run serially (fileParallelism: false) because Vitest's v8 coverage provider races when several workers finish at once and dies trying to read a temp file that has already been cleaned up.

The suite runs against frozen data. A plugin in vite.config.js resolves every import of the four modules the refresh rewrites (fixtures.js, players.js, teams.js, and history.js in src/data/) to a frozen copy in test/fixtures/frozen/, so a twice-daily refresh cannot move the coverage gate. The real modules have their own suite: npm run test:data runs test/live/ (invariants on the fixtures, the table, the leaderboards, and every historical season, plus a smoke render of every view) with no coverage threshold. That live suite is the gate the refresh workflow runs before it commits new data, and CI runs it on every push too.

How the data works

The entire season, every historical table, and the player leaderboards are committed to the repository as generated JavaScript modules. The app therefore renders completely with no network request at all. A small live overlay polls ESPN's scoreboard for matches in progress and patches scores in on top; if that fetch fails, the app is stale rather than broken.

scripts/fetch-fixtures.mjs   → src/data/teams.js, src/data/fixtures.js, public/logos/
scripts/fetch-stats.mjs      → src/data/players.js
scripts/fetch-history.mjs    → src/data/history.js

Refresh everything with npm run fetch:all. The scripts use Node built-ins only — no imports from node_modules — so a scheduled refresh can run without an npm ci first.

Why historical tables are computed, not fetched

ESPN's standings archive only reaches 2002-03. openfootball publishes match results back to 1992-93, so fetch-history.mjs parses those results and computes each table itself, applying the same ordering rules the live table uses — points, then goal difference, then goals scored. The Premier League has never used head-to-head to separate clubs for a placing.

That makes the tables verifiable rather than merely copied. Before writing, every season is checked against invariants that must hold for any correct parse: total appearances equal twice the match count, goal difference across the league sums to zero, and points equal three per decisive match plus two per draw. A season that fails is reported rather than silently written. Those same invariants run again in test/history-data.test.js over the committed data, so a bad refresh fails the build instead of shipping a wrong table.

Known data caveats

  • The 2026-27 season has not kicked off, so the table is empty and the Stats view's player leaderboards default to the most recent season that has data. They switch over automatically once matches are played.
  • ESPN publishes no player leaders for 2020-21. That season is absent from the Stats switcher rather than shown as zeroes.
  • Broadcasters are assigned only weeks ahead, so most of a freshly-fetched season carries no listing at all. The scheduled data refresh fills them in as they are published, and the "on my services" filter deliberately keeps a match whose broadcaster is still unannounced — not yet listed is not the same as not available.
  • The first three seasons had 22 clubs and 42 matches. Point totals from 1992-93 to 1994-95 are not comparable with later seasons, and the History view says so. Four clubs were relegated in 1994-95 to cut the League to 20.
  • Conference League qualification is usually decided by domestic cup results, which this app does not track, so the sixth-place band is indicative only.

Design notes

No router, no state library. The set of things a viewer can choose — view, timezone, club, whether scores are hidden — is small enough to live in useState and serialise into the query string, which has the useful property that any state worth reaching is shareable. Only non-default values are written, so a first-time URL stays clean.

One impure boundary. App.jsx fetches and merges; everything downstream is a pure function of the merged fixture list. That is why nearly all the logic is testable without a DOM.

Data-mark colours are separate from UI colours. The accent never encodes data, so a coloured mark is never mistaken for a control. The chart and league-zone palettes were checked for colour-vision separation and contrast against both the light and dark surfaces, and colour never carries meaning alone — positions are always printed, zones are named in a legend, and every bar is labelled with its value.

International breaks are inferred, not fetched. The feed carries this competition and nothing else: no field says the players are away with their countries. What the fixture list does carry is the hole, and src/utils/breaks.js calls one an international break on two conditions — it runs at least eleven clear days, and its midpoint falls in a month FIFA holds a men's window (September, October, November, March, June). The league's own rhythm is weekly and a cup weekend or the mid-season break opens a week or a little more: in the 2026-27 list the domestic holes run 7, 8 and 9 clear days and the international ones 13, 19 and 20, so the threshold sits in open space between the two populations rather than splitting either. Both tests describe the shape of a league calendar rather than one season's dates, so a refreshed fixture list re-derives its own breaks. What the app never claims is which countries are playing or when the window itself opens: the note says only what the fixture list can support — no league football for N days, back on this date.

Kickoffs are stored as UTC instants, never as wall-clock strings. Premier League times are published in UK time, which is exactly what a naive "3pm Saturday" gets wrong for anyone outside Britain twice a year when the clocks shift. The detail view shows both your local time and UK time so the app can be checked against any published fixture list.

Data sources

  • Fixtures, clubs, crests, live scores, broadcasters — ESPN's public, keyless, CORS-open feeds. The fetch scripts snapshot the season into the repo; the app overlays live scores at runtime.
  • Player statistics — ESPN's core-API leaders endpoint, per season back to 2017-18 (2020-21 is absent upstream and omitted rather than shown as zeroes).
  • Historical tables — computed from openfootball/england match results (public domain), which reach back to 1992-93 where ESPN's standings archive stops at 2002-03. Each season is verified against structural invariants before it is written (see Why historical tables are computed, not fetched above).

Broadcast listings come from ESPN's US-region feed, so they name US broadcasters (NBC, Peacock, USA Network) rather than UK ones.

Credits

Created by Chester Ismay. Source on GitHub. Companion to the World Cup 2026 schedule viewer.

The favicon lion is Lion by Lorc from game-icons.net, used under CC BY 3.0 and recoloured to the app's accent. It is not the Premier League's crowned-lion mark.

Disclaimer

An unofficial, non-commercial fan project. Not affiliated with, endorsed by, or sponsored by the Premier League. "Premier League", and club, broadcaster, and competition names and crests are trademarks of their respective owners. Fixtures, statistics and live scores come from ESPN; historical results come from the public-domain OpenFootball project.

Licensed under the MIT License.

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#football · #fixtures · #premier-league