尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證非官方 NBA 賽季檢視工具 — 開球時間依您的時區、觀看平台、即時比分、每週行事曆、季後賽事表 + play‑in、聯盟排名、聯盟領袖、追蹤球隊與行事曆訂閱。React + Vite。
AI 翻譯
Unofficial NBA season viewer — tip-off times in your timezone, where to watch, live scores, weekly calendar, playoff bracket + play-in, conference standings, league leaders, follow-teams, and calendar subscription. React + Vite.
取自儲存庫 README
正在載入完整 README。 以下為已儲存的摘錄。
🔗 Live: ismayc.github.io/nba-schedule ·
the-nba-schedule.netlify.app
A React + Vite web app showing every game of the NBA season in your timezone —
with live scores, where to watch on your streaming services, conference standings, the
playoff bracket, and league leaders.
No backend, no API keys, no tracking. The whole app is a static bundle plus a committed
snapshot of the season.檢視完整 README ↗🔗 Live: ismayc.github.io/nba-schedule · the-nba-schedule.netlify.app
A React + Vite web app showing every game of the NBA season in your timezone — with live scores, where to watch on your streaming services, conference standings, the playoff bracket, and league leaders.
No backend, no API keys, no tracking. The whole app is a static bundle plus a committed snapshot of the season.
| View | What it does |
|---|---|
| 📋 Schedule | Every game grouped by the calendar day you see, opening on today — previous days are hidden behind a toggle. Filter by team or by the teams you follow. |
| 📆 Week | A Sun–Sat grid you can page through, collapsing to a two-column agenda on a phone. |
| 📊 Regular Season | Both conference tables side by side, with the playoff cut and the play-in band marked. W/L, PCT, GB, home/road splits, last-10, streak, net rating. |
| 🏆 Playoffs | Two conference brackets (East and West) into the Finals, where each slot is a best-of-seven series. Projected from current seeding until the real field is set. Below it, both Play-In Tournaments — every game, with the seed it settled and who it knocked out. |
| 🎯 Radial | The same brackets as concentric rings — one wheel per conference, seeds outside and the conference champion in the middle — flanking the Finals. |
| 📈 Stats | Season averages, league leaders across 8 categories, scoring margin, and the playoff race — by conference — with magic numbers. |
| 📜 History | Every completed season back to 2020-21, in four modes: one season in full (final standings, play-in ladder, bracket); that season's stats (totals, the same nine leaderboards as the live view, scoring margin); every team that reached the playoffs through the play-in and what it did with the seed; and every champion. |
Star a team from any game card, standings row, or team panel to highlight it
across every view, filter the schedule to "My teams", and scope live alerts to it.
Clicking any team opens a team panel — splits, form, leading scorers, and what's
next. Each game card also flags where to watch — a 📺 badge naming the streaming
services that carry it (YouTube TV, Prime Video, Peacock). Plus: light/dark themes,
spoiler-free mode, shareable URLs, live alerts for notable moments, a game-detail modal
with a quarter line score, starting lineups, and season series, an auto-updating
webcal:// calendar subscription (plus one-time .ics download, whole season or your
teams), and installable-PWA support.
Everything comes from ESPN's public, keyless, CORS-open feeds.
The season is committed, not fetched. scripts/fetch-schedule.mjs generates
src/data/schedule.js, src/data/teams.js, and src/data/leaders.js, and mirrors team
logos into public/logos/. The app therefore renders a complete season — including
every result so far — with zero requests on load. At runtime, ESPN's scoreboard is
polled only to overlay games that are live or just finished (every 30s while a game is
in progress, 2 min otherwise, never once the season ends).
That snapshot is refreshed twice daily by .github/workflows/refresh-data.yml, which
regenerates the data, runs the test suite against it, and opens a PR. Standings are
derived from the committed scores, so a bad refresh surfaces as a failing test rather
than a quietly wrong table.
NBA seasons span two calendar years, and ESPN keys them by the ending year.
--season 2026is the 2025-26 season;--season 2027is 2026-27. The committed data is the most recent complete season; swap seasons with a singlenpm run fetch:schedule -- --season <year>.
These are the difference between "looks about right" and matching the official standings exactly:
seasonType: 'cup' and excluded. (Careful: the knockout
round names "Quarterfinals"/"Semifinals" contain the substring "final", so only a
headline containing "championship" is reclassified.)media.shortName;
the scoreboard uses names[]. Both are accepted.With those handled, derived W-L, home/road splits, last-10, and streaks match ESPN's published standings exactly for all 30 teams.
The biggest structural difference from a soccer viewer. Goals are rare enough to enumerate as events — scorer, minute, penalty — and a scoring table can be derived from them. Basketball can't work that way: ~50 scoring plays per game, tens of thousands across an 82-game, 1,230-game season.
So the model inverts. Games store a final score, and player leaderboards come from pre-aggregated season stat lines rather than being summed from events. Two things fill the gap that losing the event stream would otherwise leave:
Frequency also changes the live display. A soccer app can show a goal the moment it
lands and be right for the next ten minutes; a basketball score is stale within seconds.
So the live badge shows the period (Q3, HALF, OT) rather than a running game
clock, which would imply a precision a 30-second poll can't deliver. The exact feed
status stays in the tooltip.
And it rules out goal-style alerts. One notification per basket would fire ~50 times a game. The 🔔 toggle instead surfaces the moments that change how a game feels — tipoff, a lead change, a one-possession fourth quarter, and the final — detected by diffing poll snapshots, so no play-by-play feed is needed. A close fourth quarter alerts once on entering that state, not every 30 seconds while it holds, and a buzzer-beater that both flips the lead and ends the game is reported as one moment rather than three.
The NBA is conference-based, and a few details drive most of the app's playoff logic:
seasontype=5), not the postseason, and they do not count
toward the regular-season standings.src/data/history.js holds every completed season since 2020-21. The floor is not
arbitrary: that is when the play-in reached its current 7–10 shape, so every archived
season is directly comparable with this one. (The 2020 Orlando restart ran a one-off
qualifier; before that there was none.)
Each season commits four things:
{id, rank, value} against a deduped
per-season player table (~63 players) carrying each one's full stat line.The regular season's ~1,230 games are summarised into those totals and standings rather than committed, which is why six extra seasons cost ~320KB instead of ~5MB (bundle 1.10MB → 1.40MB, 224KB → 259KB gzipped).
Nothing derived is committed. The bracket and the ladder are rebuilt at runtime by the
same buildBracket() / buildPlayIn() the current season uses; the leader boards were
built by the same leaderboard() (same tie handling, same volume qualifiers for the
percentage categories); the scoring margin re-derives from points for and against via the
same rankScoring(). An archived season renders through the live components, so it
cannot drift from the live view. Box scores aren't committed either — the detail modal
fetches them from ESPN by event id, and every archived game has one, down to the five
closest games of 2020-21.
The one live card with no historical meaning is the playoff race: magic numbers for a season that ended years ago. A finished season shows its notable games instead.
npm run fetch:history # rebuilds the archive (a season in progress is skipped)
A season joins the archive by itself the week it finishes: the script runs up to the season the app is on, and drops any season with no champion yet.
npm install
npm run dev # local dev server
npm test # unit + render tests, against frozen data
npm run test:data # the live suite: the real committed data (what a refresh must pass)
npm run build # production bundle
npm run coverage:badge # tests with coverage, writes public/coverage.json
npm run fetch:schedule # regenerate committed data from ESPN
npm run check:schedule # report drift between committed data and the live feed
npm run verify:live # check the live overlay's assumptions against a game in progress
scripts/ uses Node built-ins only — no imports from node_modules — so CI can run
the data jobs on a bare checkout with no install step. A CI job enforces this.
The suite leans on real data rather than hand-made fixtures, because real data contains the edge cases you wouldn't think to invent.
Real, but frozen. There are two suites, and they never mix:
The main suite (npm test, the 100% coverage gate) never sees the modules the
refresh rewrites. A plugin in vite.config.js resolves every import of
src/data/schedule.js, leaders.js, and teams.js to a frozen 2026-27 preseason
stand-in under test/fixtures/frozen/, whoever the importer is. Coverage therefore
cannot move when the data does: the gate passes unchanged with those three modules
made to throw on import.
The live suite (npm run test:data, test/live/) reads the real modules and has no
coverage threshold. It holds invariants (unique ids, known teams, finite stats, a
per-conference standings table that recounts from the games) and a smoke render of
every view, every game dialog, and every team panel, checked for the residue of a bad
value (NaN, Invalid Date, 1969). This is the gate a refresh has to pass. Only things
true on any day of any season belong in it; it passes on both an unplayed board and the
whole finished 2025-26 season.
Standings and tiebreakers are checked against the committed 2025-26 season — the per-conference seeds are independently verifiable against ESPN's published standings.
The bracket is tested against the committed 2025-26 postseason, reproducing the
real two-conference outcome all the way to the Finals, plus a compact synthetic
best-of-seven fixture (test/fixtures/postseason.js) for the series engine. Series are
located by their play-in-immune higher seed, so a lower seed that advanced through the
play-in (a 7-over-2 upset) still slots correctly.
The play-in is cross-checked against the bracket it feeds: the teams it sends through must be exactly the 7 and 8 seeds the committed first round actually used, so the two data sets have to agree rather than each being asserted on its own.
Format invariants that would otherwise depend on this week's results (like the tiebreaker order, or per-conference seeding) are tested with synthetic data that equalises everything above the step under test, so they don't break when standings shift.
Two things the suite structurally cannot close on committed data:
npm run verify:live, run while a game is actually in progress, is the only check that
compares those assumptions to reality.Built with base: './', so the same dist/ works at a domain root (Netlify) and under
a subpath (GitHub Pages /nba-schedule/) with no separate build.
ci.yml on every push to main, gated on
tests passing.NETLIFY_AUTH_TOKEN is set
as a repository secret (NETLIFY_SITE_ID is already stored as a repo variable). Until
then that step is skipped and Netlify can be updated with
npx netlify-cli deploy --prod --dir dist. The webcal:// calendar feed is a Netlify
function, so it only resolves on the Netlify host.Created by Chester Ismay. Source on GitHub.
Unofficial fan project. Not affiliated with, endorsed by, or sponsored by the NBA. Team names and logos are trademarks of their respective owners. Schedule, results, and player data via ESPN's public feeds.
MIT licensed.