NBA Feed
팀 뉴스와 경기 정보를 제공, 점수는 직접 확인할 때까지 숨겨져 있습니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기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.