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.
- package/.claude/settings.local.json +11 -0
- package/.github/workflows/release.yml +37 -0
- package/CONTRIBUTING.md +38 -0
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/api/nhl.js +37 -0
- package/dist/app/dates.js +36 -0
- package/dist/app/input.js +97 -0
- package/dist/app/polling.js +241 -0
- package/dist/app/store.js +39 -0
- package/dist/app/timers.js +15 -0
- package/dist/domain/diff.js +57 -0
- package/dist/domain/events.js +22 -0
- package/dist/domain/normalize.js +677 -0
- package/dist/domain/reducer.js +313 -0
- package/dist/domain/types.js +1 -0
- package/dist/index.js +14 -0
- package/dist/ui/App.js +77 -0
- package/dist/ui/components/Banner.js +8 -0
- package/dist/ui/components/Footer.js +10 -0
- package/dist/ui/components/GameList.js +20 -0
- package/dist/ui/components/GameRow.js +25 -0
- package/dist/ui/components/StatusLine.js +49 -0
- package/dist/ui/screens/GameDetailScreen.js +37 -0
- package/dist/ui/screens/LeadersScreen.js +36 -0
- package/dist/ui/screens/ScoreboardScreen.js +6 -0
- package/dist/ui/screens/StandingsScreen.js +27 -0
- package/package.json +28 -0
- package/src/api/nhl.ts +53 -0
- package/src/app/dates.ts +54 -0
- package/src/app/input.ts +130 -0
- package/src/app/polling.ts +333 -0
- package/src/app/store.ts +55 -0
- package/src/app/timers.ts +23 -0
- package/src/domain/diff.ts +107 -0
- package/src/domain/events.ts +31 -0
- package/src/domain/normalize.ts +966 -0
- package/src/domain/reducer.ts +458 -0
- package/src/domain/types.ts +270 -0
- package/src/index.tsx +15 -0
- package/src/ui/App.tsx +151 -0
- package/src/ui/components/Banner.tsx +23 -0
- package/src/ui/components/Footer.tsx +17 -0
- package/src/ui/components/GameList.tsx +45 -0
- package/src/ui/components/GameRow.tsx +60 -0
- package/src/ui/components/StatusLine.tsx +83 -0
- package/src/ui/screens/GameDetailScreen.tsx +199 -0
- package/src/ui/screens/LeadersScreen.tsx +92 -0
- package/src/ui/screens/ScoreboardScreen.tsx +36 -0
- package/src/ui/screens/StandingsScreen.tsx +95 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import type { NormalizedGame } from "../../domain/types.js";
|
|
3
|
+
import { GameList } from "../components/GameList.js";
|
|
4
|
+
|
|
5
|
+
type ScoreboardScreenProps = {
|
|
6
|
+
dateLabel: string;
|
|
7
|
+
games: NormalizedGame[];
|
|
8
|
+
selectedGameId?: number;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
errorMessage?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function ScoreboardScreen({
|
|
14
|
+
dateLabel,
|
|
15
|
+
games,
|
|
16
|
+
selectedGameId,
|
|
17
|
+
loading,
|
|
18
|
+
errorMessage,
|
|
19
|
+
}: ScoreboardScreenProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Box flexDirection="column">
|
|
22
|
+
<Box marginBottom={1}>
|
|
23
|
+
<Text dimColor>{`<- ${dateLabel} ->`}</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
{loading ? (
|
|
26
|
+
<Text dimColor>{`Loading ${dateLabel}...`}</Text>
|
|
27
|
+
) : games.length ? (
|
|
28
|
+
<GameList games={games} selectedGameId={selectedGameId} />
|
|
29
|
+
) : errorMessage ? (
|
|
30
|
+
<Text color="redBright">Unable to load scoreboard: {errorMessage}</Text>
|
|
31
|
+
) : (
|
|
32
|
+
<Text dimColor>{`No games on ${dateLabel}.`}</Text>
|
|
33
|
+
)}
|
|
34
|
+
</Box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Box, Text, useStdout } from "ink";
|
|
2
|
+
import type {
|
|
3
|
+
ConferenceStandings,
|
|
4
|
+
NormalizedStandings,
|
|
5
|
+
StandingsEntry,
|
|
6
|
+
StandingsSection,
|
|
7
|
+
} from "../../domain/types.js";
|
|
8
|
+
|
|
9
|
+
type StandingsScreenProps = {
|
|
10
|
+
dateLabel: string;
|
|
11
|
+
standings?: NormalizedStandings;
|
|
12
|
+
loading: boolean;
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SIDE_BY_SIDE_MIN_WIDTH = 100;
|
|
17
|
+
|
|
18
|
+
function formatRecord(entry: StandingsEntry): string {
|
|
19
|
+
return `${entry.wins}-${entry.losses}-${entry.otLosses}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rankForSection(section: StandingsSection, entry: StandingsEntry): number {
|
|
23
|
+
if (section.title === "WILD CARD" || section.title === "UNDER WILD CARD") {
|
|
24
|
+
return entry.wildcardRank;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return entry.divisionRank;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderRow(section: StandingsSection, entry: StandingsEntry) {
|
|
31
|
+
const rank = rankForSection(section, entry);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Text key={`${section.title}-${entry.teamAbbrev}`}>
|
|
35
|
+
{String(rank).padStart(2)} {entry.teamAbbrev.padEnd(3)} {String(entry.points).padStart(3)} {String(
|
|
36
|
+
entry.gamesPlayed,
|
|
37
|
+
).padStart(2)} {formatRecord(entry).padEnd(8)} {String(entry.row).padStart(3)} {(
|
|
38
|
+
entry.streak || "-"
|
|
39
|
+
).padStart(3)}
|
|
40
|
+
{entry.clinchIndicator ? ` ${entry.clinchIndicator}` : ""}
|
|
41
|
+
</Text>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function renderSection(section: StandingsSection) {
|
|
46
|
+
return (
|
|
47
|
+
<Box key={section.title} flexDirection="column" marginBottom={1}>
|
|
48
|
+
<Text bold>{section.title}</Text>
|
|
49
|
+
<Text dimColor> # TM PTS GP REC ROW STK</Text>
|
|
50
|
+
{section.entries.length ? (
|
|
51
|
+
section.entries.map((entry) => renderRow(section, entry))
|
|
52
|
+
) : (
|
|
53
|
+
<Text dimColor>No teams</Text>
|
|
54
|
+
)}
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderConference(conference: ConferenceStandings) {
|
|
60
|
+
return (
|
|
61
|
+
<Box key={conference.conferenceAbbrev} flexDirection="column" flexGrow={1} paddingRight={2}>
|
|
62
|
+
<Text bold color="cyanBright">{conference.conferenceName.toUpperCase()}</Text>
|
|
63
|
+
{conference.sections.map(renderSection)}
|
|
64
|
+
</Box>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function StandingsScreen({
|
|
69
|
+
dateLabel,
|
|
70
|
+
standings,
|
|
71
|
+
loading,
|
|
72
|
+
errorMessage,
|
|
73
|
+
}: StandingsScreenProps) {
|
|
74
|
+
const { stdout } = useStdout();
|
|
75
|
+
const shouldUseColumns = (stdout.columns ?? 0) >= SIDE_BY_SIDE_MIN_WIDTH;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Box flexDirection="column">
|
|
79
|
+
<Box marginBottom={1}>
|
|
80
|
+
<Text dimColor>{`Standings ${dateLabel}`}</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
{loading ? (
|
|
83
|
+
<Text dimColor>{`Loading standings for ${dateLabel}...`}</Text>
|
|
84
|
+
) : standings?.conferences.length ? (
|
|
85
|
+
<Box flexDirection={shouldUseColumns ? "row" : "column"}>
|
|
86
|
+
{standings.conferences.map(renderConference)}
|
|
87
|
+
</Box>
|
|
88
|
+
) : errorMessage ? (
|
|
89
|
+
<Text color="redBright">Unable to load standings: {errorMessage}</Text>
|
|
90
|
+
) : (
|
|
91
|
+
<Text dimColor>{`No standings available for ${dateLabel}.`}</Text>
|
|
92
|
+
)}
|
|
93
|
+
</Box>
|
|
94
|
+
);
|
|
95
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2023",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"lib": ["ES2023"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": "src",
|
|
15
|
+
"types": ["node"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*.ts", "src/**/*.tsx"]
|
|
18
|
+
}
|