nhl-tui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.github/workflows/release.yml +37 -0
  3. package/CONTRIBUTING.md +38 -0
  4. package/LICENSE +21 -0
  5. package/README.md +222 -0
  6. package/dist/api/nhl.js +37 -0
  7. package/dist/app/dates.js +36 -0
  8. package/dist/app/input.js +97 -0
  9. package/dist/app/polling.js +241 -0
  10. package/dist/app/store.js +39 -0
  11. package/dist/app/timers.js +15 -0
  12. package/dist/domain/diff.js +57 -0
  13. package/dist/domain/events.js +22 -0
  14. package/dist/domain/normalize.js +677 -0
  15. package/dist/domain/reducer.js +313 -0
  16. package/dist/domain/types.js +1 -0
  17. package/dist/index.js +14 -0
  18. package/dist/ui/App.js +77 -0
  19. package/dist/ui/components/Banner.js +8 -0
  20. package/dist/ui/components/Footer.js +10 -0
  21. package/dist/ui/components/GameList.js +20 -0
  22. package/dist/ui/components/GameRow.js +25 -0
  23. package/dist/ui/components/StatusLine.js +49 -0
  24. package/dist/ui/screens/GameDetailScreen.js +37 -0
  25. package/dist/ui/screens/LeadersScreen.js +36 -0
  26. package/dist/ui/screens/ScoreboardScreen.js +6 -0
  27. package/dist/ui/screens/StandingsScreen.js +27 -0
  28. package/package.json +28 -0
  29. package/src/api/nhl.ts +53 -0
  30. package/src/app/dates.ts +54 -0
  31. package/src/app/input.ts +130 -0
  32. package/src/app/polling.ts +333 -0
  33. package/src/app/store.ts +55 -0
  34. package/src/app/timers.ts +23 -0
  35. package/src/domain/diff.ts +107 -0
  36. package/src/domain/events.ts +31 -0
  37. package/src/domain/normalize.ts +966 -0
  38. package/src/domain/reducer.ts +458 -0
  39. package/src/domain/types.ts +270 -0
  40. package/src/index.tsx +15 -0
  41. package/src/ui/App.tsx +151 -0
  42. package/src/ui/components/Banner.tsx +23 -0
  43. package/src/ui/components/Footer.tsx +17 -0
  44. package/src/ui/components/GameList.tsx +45 -0
  45. package/src/ui/components/GameRow.tsx +60 -0
  46. package/src/ui/components/StatusLine.tsx +83 -0
  47. package/src/ui/screens/GameDetailScreen.tsx +199 -0
  48. package/src/ui/screens/LeadersScreen.tsx +92 -0
  49. package/src/ui/screens/ScoreboardScreen.tsx +36 -0
  50. package/src/ui/screens/StandingsScreen.tsx +95 -0
  51. package/tsconfig.json +18 -0
@@ -0,0 +1,107 @@
1
+ import type { AppEvent, NormalizedGame } from "./types.js";
2
+
3
+ function buildGoalEvents(
4
+ gameId: number,
5
+ team: string,
6
+ delta: number,
7
+ timestamp: number,
8
+ ): AppEvent[] {
9
+ if (delta <= 0) {
10
+ return [];
11
+ }
12
+
13
+ return Array.from({ length: delta }, () => ({
14
+ type: "goal_scored" as const,
15
+ gameId,
16
+ team,
17
+ timestamp,
18
+ }));
19
+ }
20
+
21
+ function diffSingleGame(
22
+ previousGame: NormalizedGame | undefined,
23
+ nextGame: NormalizedGame,
24
+ timestamp: number,
25
+ ): AppEvent[] {
26
+ if (!previousGame) {
27
+ return [];
28
+ }
29
+
30
+ const events: AppEvent[] = [];
31
+
32
+ if (previousGame.phase !== "live" && nextGame.phase === "live") {
33
+ events.push({
34
+ type: "game_started",
35
+ gameId: nextGame.id,
36
+ timestamp,
37
+ });
38
+ }
39
+
40
+ if (previousGame.phase === "live" && nextGame.phase === "final") {
41
+ events.push({
42
+ type: "game_ended",
43
+ gameId: nextGame.id,
44
+ timestamp,
45
+ });
46
+ }
47
+
48
+ if (
49
+ previousGame.phase === "live" &&
50
+ nextGame.phase === "live" &&
51
+ previousGame.periodLabel &&
52
+ nextGame.periodLabel &&
53
+ previousGame.periodLabel !== nextGame.periodLabel
54
+ ) {
55
+ events.push({
56
+ type: "period_changed",
57
+ gameId: nextGame.id,
58
+ period: nextGame.periodLabel,
59
+ timestamp,
60
+ });
61
+ }
62
+
63
+ events.push(
64
+ ...buildGoalEvents(
65
+ nextGame.id,
66
+ nextGame.away.abbrev,
67
+ nextGame.away.score - previousGame.away.score,
68
+ timestamp,
69
+ ),
70
+ );
71
+
72
+ events.push(
73
+ ...buildGoalEvents(
74
+ nextGame.id,
75
+ nextGame.home.abbrev,
76
+ nextGame.home.score - previousGame.home.score,
77
+ timestamp,
78
+ ),
79
+ );
80
+
81
+ return events;
82
+ }
83
+
84
+ export function diffGames(
85
+ previousGames: NormalizedGame[],
86
+ nextGames: NormalizedGame[],
87
+ timestamp: number,
88
+ ): AppEvent[] {
89
+ const previousById = new Map(previousGames.map((game) => [game.id, game]));
90
+ const events: AppEvent[] = [];
91
+
92
+ for (const nextGame of nextGames) {
93
+ events.push(
94
+ ...diffSingleGame(previousById.get(nextGame.id), nextGame, timestamp),
95
+ );
96
+ }
97
+
98
+ return events;
99
+ }
100
+
101
+ export function diffGame(
102
+ previousGame: NormalizedGame | undefined,
103
+ nextGame: NormalizedGame,
104
+ timestamp: number,
105
+ ): AppEvent[] {
106
+ return diffSingleGame(previousGame, nextGame, timestamp);
107
+ }
@@ -0,0 +1,31 @@
1
+ import type { AppEvent, Banner, NormalizedGame } from "./types.js";
2
+
3
+ export function createGoalBanners(
4
+ events: AppEvent[],
5
+ games: NormalizedGame[],
6
+ createdAt: number,
7
+ ): Banner[] {
8
+ const gamesById = new Map(games.map((game) => [game.id, game]));
9
+
10
+ return events.flatMap((event, index) => {
11
+ if (event.type !== "goal_scored") {
12
+ return [];
13
+ }
14
+
15
+ const game = gamesById.get(event.gameId);
16
+
17
+ return [
18
+ {
19
+ id: `${event.gameId}:${event.timestamp}:${index}`,
20
+ type: "goal",
21
+ gameId: event.gameId,
22
+ team: event.team,
23
+ title: `${event.team} GOAL`,
24
+ subtitle: game
25
+ ? `${game.away.abbrev} ${game.away.score} - ${game.home.score} ${game.home.abbrev}`
26
+ : undefined,
27
+ createdAt,
28
+ },
29
+ ];
30
+ });
31
+ }