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,37 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*.*.*'
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
release:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- name: Check out repository
|
|
17
|
+
uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Node.js
|
|
20
|
+
uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: 22
|
|
23
|
+
cache: npm
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: npm ci
|
|
27
|
+
|
|
28
|
+
- name: Typecheck
|
|
29
|
+
run: npm run check
|
|
30
|
+
|
|
31
|
+
- name: Build
|
|
32
|
+
run: npm run build
|
|
33
|
+
|
|
34
|
+
- name: Create GitHub release
|
|
35
|
+
uses: softprops/action-gh-release@v2
|
|
36
|
+
with:
|
|
37
|
+
generate_release_notes: true
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for contributing to `nhl-tui`.
|
|
4
|
+
|
|
5
|
+
## Before You Open A PR
|
|
6
|
+
|
|
7
|
+
Run:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run check
|
|
11
|
+
npm run build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
Keep the layering intact:
|
|
17
|
+
|
|
18
|
+
- `src/api`: endpoint access only
|
|
19
|
+
- `src/domain`: normalization, diffing, events, reducer logic
|
|
20
|
+
- `src/ui`: Ink rendering and input dispatch only
|
|
21
|
+
|
|
22
|
+
## Trademark And Content Guidance
|
|
23
|
+
|
|
24
|
+
Please do not add:
|
|
25
|
+
|
|
26
|
+
- NHL logos
|
|
27
|
+
- team logos
|
|
28
|
+
- league or club branding assets
|
|
29
|
+
- broadcast audio or video
|
|
30
|
+
- copyrighted media or artwork sourced from NHL properties
|
|
31
|
+
|
|
32
|
+
The UI should use team abbreviations and text-based presentation only.
|
|
33
|
+
|
|
34
|
+
## Design Constraints
|
|
35
|
+
|
|
36
|
+
- keep the UI terminal-native and keyboard-first
|
|
37
|
+
- prefer dense, stable layouts over web-style components
|
|
38
|
+
- avoid leaking raw upstream payloads into the UI layer
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sean McCann
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# nhl-tui
|
|
2
|
+
|
|
3
|
+
A terminal UI for following NHL games, built with TypeScript, Node.js, React, and Ink.
|
|
4
|
+
|
|
5
|
+
`nhl-tui` is designed to feel like a real TUI, not a web dashboard squeezed into a terminal. It opens on a compact scoreboard, supports keyboard-first navigation, drills into live game detail, and keeps NHL-specific logic out of the rendering layer.
|
|
6
|
+
|
|
7
|
+
## Status
|
|
8
|
+
|
|
9
|
+
This is an early but usable v1.
|
|
10
|
+
|
|
11
|
+
Current capabilities:
|
|
12
|
+
|
|
13
|
+
- date-based scoreboard browsing
|
|
14
|
+
- stable row selection
|
|
15
|
+
- game detail view with summary, play-by-play, and box score tabs
|
|
16
|
+
- adaptive polling for scoreboard and game detail
|
|
17
|
+
- standings screen with conference playoff-cut grouping
|
|
18
|
+
- leaders screen with top-10 skater and goalie stat tables
|
|
19
|
+
- diff-based domain events for goals, game starts, period changes, and finals
|
|
20
|
+
- queued goal banners
|
|
21
|
+
- reducer-based state management with separate API, domain, and UI layers
|
|
22
|
+
|
|
23
|
+
## Screens
|
|
24
|
+
|
|
25
|
+
Scoreboard:
|
|
26
|
+
|
|
27
|
+
- grouped by `LIVE`, `UPCOMING`, and `FINAL`
|
|
28
|
+
- away and home team abbreviations
|
|
29
|
+
- score and game state
|
|
30
|
+
- live period and clock
|
|
31
|
+
- scheduled puck drop time for upcoming games
|
|
32
|
+
|
|
33
|
+
Game detail:
|
|
34
|
+
|
|
35
|
+
- summary
|
|
36
|
+
- play-by-play
|
|
37
|
+
- box score
|
|
38
|
+
|
|
39
|
+
Standings:
|
|
40
|
+
|
|
41
|
+
- Eastern and Western conference sections
|
|
42
|
+
- top three teams in each division
|
|
43
|
+
- two conference wild card teams
|
|
44
|
+
- teams below the wild card line
|
|
45
|
+
|
|
46
|
+
Leaders:
|
|
47
|
+
|
|
48
|
+
- skater leaders for points, goals, and assists
|
|
49
|
+
- goalie leaders for goals against average, save percentage, and shutouts
|
|
50
|
+
- top 10 rows per table
|
|
51
|
+
|
|
52
|
+
The summary view includes scoring, penalties, and three stars. Where the upstream payload exposes enough information, player labels are rendered as sweater number plus short name, for example `97 C. McDavid`.
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### Requirements
|
|
57
|
+
|
|
58
|
+
- Node.js 22+
|
|
59
|
+
- network access to `https://api-web.nhle.com`
|
|
60
|
+
|
|
61
|
+
### Run locally
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
npm start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Build the CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm run build
|
|
72
|
+
node dist/index.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The package is configured with a `nhl-tui` bin entry for future npm publishing.
|
|
76
|
+
|
|
77
|
+
## Releases
|
|
78
|
+
|
|
79
|
+
GitHub Releases are automated with Actions on semantic-version tags. Pushing a tag such as `v0.1.0` runs typecheck and build, then creates a GitHub Release with generated notes.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git checkout main
|
|
85
|
+
git pull
|
|
86
|
+
git tag v0.1.0
|
|
87
|
+
git push origin main --tags
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Version selection is still manual. The workflow only builds and publishes the GitHub Release after the tag exists.
|
|
91
|
+
|
|
92
|
+
## Controls
|
|
93
|
+
|
|
94
|
+
### Scoreboard
|
|
95
|
+
|
|
96
|
+
- `up` / `down`: move selection
|
|
97
|
+
- `left` / `right`: previous or next date
|
|
98
|
+
- `g`: jump to top
|
|
99
|
+
- `G`: jump to bottom
|
|
100
|
+
- `enter`: open selected game
|
|
101
|
+
- `s`: open standings
|
|
102
|
+
- `l`: open leaders
|
|
103
|
+
- `r`: manual refresh
|
|
104
|
+
- `esc` or `q`: quit
|
|
105
|
+
|
|
106
|
+
### Standings view
|
|
107
|
+
|
|
108
|
+
- `esc`: back to scoreboard
|
|
109
|
+
|
|
110
|
+
### Leaders view
|
|
111
|
+
|
|
112
|
+
- `esc`: back to scoreboard
|
|
113
|
+
|
|
114
|
+
### Game view
|
|
115
|
+
|
|
116
|
+
- `left` / `right`: cycle tabs
|
|
117
|
+
- `1`: summary
|
|
118
|
+
- `2`: play-by-play
|
|
119
|
+
- `3`: box score
|
|
120
|
+
- `r`: manual refresh
|
|
121
|
+
- `esc`: back to scoreboard
|
|
122
|
+
- `q`: quit
|
|
123
|
+
|
|
124
|
+
## Architecture
|
|
125
|
+
|
|
126
|
+
The app is split into three layers:
|
|
127
|
+
|
|
128
|
+
- `src/api`
|
|
129
|
+
Public endpoint access only.
|
|
130
|
+
- `src/domain`
|
|
131
|
+
Stable types, normalization, diffing, event emission, and reducer logic.
|
|
132
|
+
- `src/ui`
|
|
133
|
+
Ink components that render already-processed state and dispatch actions.
|
|
134
|
+
|
|
135
|
+
Runtime flow:
|
|
136
|
+
|
|
137
|
+
`fetch -> normalize -> diff -> emit events -> reducer -> Ink render`
|
|
138
|
+
|
|
139
|
+
Ink is only the renderer. Components do not interpret raw upstream payloads.
|
|
140
|
+
|
|
141
|
+
The data-fetching layer is intentionally isolated in `src/api` so endpoints can be swapped or replaced if upstream services change.
|
|
142
|
+
|
|
143
|
+
## Data Sources And API Usage
|
|
144
|
+
|
|
145
|
+
Game data is retrieved from publicly accessible endpoints used by NHL web applications.
|
|
146
|
+
|
|
147
|
+
Current endpoint usage is isolated in `src/api/nhl.ts`:
|
|
148
|
+
|
|
149
|
+
- `/v1/score/YYYY-MM-DD`
|
|
150
|
+
- `/v1/standings/YYYY-MM-DD`
|
|
151
|
+
- `/v1/skater-stats-leaders/current?categories=points,goals,assists&limit=10`
|
|
152
|
+
- `/v1/goalie-stats-leaders/current?categories=goalsAgainstAverage,savePctg,shutouts&limit=10`
|
|
153
|
+
- `/v1/gamecenter/{gameId}/landing`
|
|
154
|
+
- `/v1/gamecenter/{gameId}/play-by-play`
|
|
155
|
+
- `/v1/gamecenter/{gameId}/boxscore`
|
|
156
|
+
|
|
157
|
+
No API key is currently required for these endpoints, but their availability and permitted use can change over time.
|
|
158
|
+
|
|
159
|
+
The application uses adaptive polling to minimize load on upstream services. It limits requests to the current scoreboard date or the currently selected game, slows down aggressively when possible, and is intended for personal and educational use. The standings screen is fetched once per selected date and then served from in-memory state. The leaders screen is also fetched once and then served from in-memory state.
|
|
160
|
+
|
|
161
|
+
## Legal And Trademark Considerations
|
|
162
|
+
|
|
163
|
+
This project is not affiliated with, endorsed by, or sponsored by the National Hockey League.
|
|
164
|
+
|
|
165
|
+
Additional safeguards and project conventions:
|
|
166
|
+
|
|
167
|
+
- The UI uses team abbreviations only, not team logos or NHL branding assets.
|
|
168
|
+
- The repository does not bundle NHL logos, team logos, broadcast assets, or other league branding materials.
|
|
169
|
+
- This project does not stream NHL video, bundle broadcast media, or distribute copyrighted game footage or other copyrighted media.
|
|
170
|
+
- `NHL`, team names, club names, logos, shields, and other league or team branding remain the property of their respective owners.
|
|
171
|
+
- References to the league, clubs, players, and game data are for identification and informational use only.
|
|
172
|
+
- This project is intended for personal and educational use. The MIT license applies to this source code, but it does not grant any rights to NHL trademarks, logos, or media.
|
|
173
|
+
- Public NHL web endpoints and related terms may change or be restricted at any time, and this project should not be presented as an official NHL product or service.
|
|
174
|
+
|
|
175
|
+
## Events
|
|
176
|
+
|
|
177
|
+
Meaningful state transitions are detected in the domain layer by diffing normalized snapshots.
|
|
178
|
+
|
|
179
|
+
Current event types:
|
|
180
|
+
|
|
181
|
+
- `goal_scored`
|
|
182
|
+
- `game_started`
|
|
183
|
+
- `period_changed`
|
|
184
|
+
- `game_ended`
|
|
185
|
+
|
|
186
|
+
Goal events are surfaced through a queued banner system so multiple notifications do not overlap.
|
|
187
|
+
|
|
188
|
+
## Development
|
|
189
|
+
|
|
190
|
+
Useful commands:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
npm start
|
|
194
|
+
npm run check
|
|
195
|
+
npm run build
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
Issues and pull requests are welcome.
|
|
201
|
+
|
|
202
|
+
Before opening a PR, run:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
npm run check
|
|
206
|
+
npm run build
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for contributor expectations, including trademark and branding restrictions.
|
|
210
|
+
|
|
211
|
+
## Roadmap
|
|
212
|
+
|
|
213
|
+
Likely next improvements:
|
|
214
|
+
|
|
215
|
+
- play-by-play-id-based event detection instead of score-only goal diffs
|
|
216
|
+
- additional domain events such as penalties, lead changes, and goalie pulls
|
|
217
|
+
- optional sound hooks driven from emitted domain events
|
|
218
|
+
- richer box score and play-by-play layouts
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
This project is licensed under the MIT License. See [LICENSE](./LICENSE).
|
package/dist/api/nhl.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const BASE_URL = "https://api-web.nhle.com/v1";
|
|
2
|
+
async function requestJson(path) {
|
|
3
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
4
|
+
headers: {
|
|
5
|
+
"user-agent": "nhl-tui/0.1.0",
|
|
6
|
+
accept: "application/json",
|
|
7
|
+
},
|
|
8
|
+
});
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
const text = await response.text();
|
|
11
|
+
throw new Error(`NHL API request failed: ${response.status} ${response.statusText} ${text}`.trim());
|
|
12
|
+
}
|
|
13
|
+
return response.json();
|
|
14
|
+
}
|
|
15
|
+
export class NhlApi {
|
|
16
|
+
async fetchScoreboard(scoreboardDate) {
|
|
17
|
+
return requestJson(`/score/${scoreboardDate}`);
|
|
18
|
+
}
|
|
19
|
+
async fetchStandings(scoreboardDate) {
|
|
20
|
+
return requestJson(`/standings/${scoreboardDate}`);
|
|
21
|
+
}
|
|
22
|
+
async fetchSkaterLeaders(limit = 10) {
|
|
23
|
+
return requestJson(`/skater-stats-leaders/current?categories=points,goals,assists&limit=${limit}`);
|
|
24
|
+
}
|
|
25
|
+
async fetchGoalieLeaders(limit = 10) {
|
|
26
|
+
return requestJson(`/goalie-stats-leaders/current?categories=goalsAgainstAverage,savePctg,shutouts&limit=${limit}`);
|
|
27
|
+
}
|
|
28
|
+
async fetchSummary(gameId) {
|
|
29
|
+
return requestJson(`/gamecenter/${gameId}/landing`);
|
|
30
|
+
}
|
|
31
|
+
async fetchPlayByPlay(gameId) {
|
|
32
|
+
return requestJson(`/gamecenter/${gameId}/play-by-play`);
|
|
33
|
+
}
|
|
34
|
+
async fetchBoxScore(gameId) {
|
|
35
|
+
return requestJson(`/gamecenter/${gameId}/boxscore`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function pad(value) {
|
|
2
|
+
return String(value).padStart(2, "0");
|
|
3
|
+
}
|
|
4
|
+
function parseScoreboardDate(scoreboardDate) {
|
|
5
|
+
const [year, month, day] = scoreboardDate.split("-").map(Number);
|
|
6
|
+
return new Date(year, (month ?? 1) - 1, day ?? 1);
|
|
7
|
+
}
|
|
8
|
+
export function todayScoreboardDate(now = new Date()) {
|
|
9
|
+
return [now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate())].join("-");
|
|
10
|
+
}
|
|
11
|
+
export function shiftScoreboardDate(scoreboardDate, deltaDays) {
|
|
12
|
+
const nextDate = parseScoreboardDate(scoreboardDate);
|
|
13
|
+
nextDate.setDate(nextDate.getDate() + deltaDays);
|
|
14
|
+
return todayScoreboardDate(nextDate);
|
|
15
|
+
}
|
|
16
|
+
export function compareScoreboardDateToToday(scoreboardDate, now = new Date()) {
|
|
17
|
+
const today = todayScoreboardDate(now);
|
|
18
|
+
if (scoreboardDate < today) {
|
|
19
|
+
return -1;
|
|
20
|
+
}
|
|
21
|
+
if (scoreboardDate > today) {
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
export function formatScoreboardDateLabel(scoreboardDate, now = new Date()) {
|
|
27
|
+
const date = parseScoreboardDate(scoreboardDate);
|
|
28
|
+
const isToday = compareScoreboardDateToToday(scoreboardDate, now) === 0;
|
|
29
|
+
const parts = new Intl.DateTimeFormat(undefined, {
|
|
30
|
+
weekday: "short",
|
|
31
|
+
month: "short",
|
|
32
|
+
day: "numeric",
|
|
33
|
+
year: date.getFullYear() === now.getFullYear() ? undefined : "numeric",
|
|
34
|
+
}).format(date);
|
|
35
|
+
return isToday ? `Today ${parts}` : parts;
|
|
36
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const gameTabs = ["summary", "pbp", "box"];
|
|
2
|
+
function cycleGameTab(currentTab, direction) {
|
|
3
|
+
const currentIndex = gameTabs.indexOf(currentTab);
|
|
4
|
+
const nextIndex = (currentIndex + direction + gameTabs.length) % gameTabs.length;
|
|
5
|
+
return gameTabs[nextIndex] ?? currentTab;
|
|
6
|
+
}
|
|
7
|
+
export function handleAppInput(input, key, state, dispatch, quit) {
|
|
8
|
+
if (state.screen.type === "standings" || state.screen.type === "leaders") {
|
|
9
|
+
if (key.escape) {
|
|
10
|
+
dispatch({ type: "go_back" });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
if (input === "q") {
|
|
15
|
+
quit();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (state.screen.type === "scoreboard" && key.escape) {
|
|
19
|
+
quit();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (input === "r") {
|
|
23
|
+
if (state.screen.type !== "standings" && state.screen.type !== "leaders") {
|
|
24
|
+
dispatch({ type: "manual_refresh_requested" });
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (input === "g") {
|
|
29
|
+
dispatch({ type: "jump_selection", target: "top" });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (input === "G") {
|
|
33
|
+
dispatch({ type: "jump_selection", target: "bottom" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (state.screen.type === "scoreboard" && input === "s") {
|
|
37
|
+
dispatch({ type: "open_standings" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (state.screen.type === "scoreboard" && input === "l") {
|
|
41
|
+
dispatch({ type: "open_leaders" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (state.screen.type === "scoreboard") {
|
|
45
|
+
if (key.leftArrow) {
|
|
46
|
+
dispatch({ type: "change_scoreboard_date", delta: -1 });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (key.rightArrow) {
|
|
50
|
+
dispatch({ type: "change_scoreboard_date", delta: 1 });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (state.screen.type === "scoreboard" && key.upArrow) {
|
|
55
|
+
dispatch({ type: "move_selection", delta: -1 });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (state.screen.type === "scoreboard" && key.downArrow) {
|
|
59
|
+
dispatch({ type: "move_selection", delta: 1 });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.return && state.screen.type === "scoreboard") {
|
|
63
|
+
dispatch({ type: "open_selected_game" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (state.screen.type === "game") {
|
|
67
|
+
if (key.escape) {
|
|
68
|
+
dispatch({ type: "go_back" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (key.leftArrow) {
|
|
72
|
+
dispatch({
|
|
73
|
+
type: "set_tab",
|
|
74
|
+
tab: cycleGameTab(state.screen.tab, -1),
|
|
75
|
+
});
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.rightArrow) {
|
|
79
|
+
dispatch({
|
|
80
|
+
type: "set_tab",
|
|
81
|
+
tab: cycleGameTab(state.screen.tab, 1),
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (input === "1") {
|
|
86
|
+
dispatch({ type: "set_tab", tab: "summary" });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (input === "2") {
|
|
90
|
+
dispatch({ type: "set_tab", tab: "pbp" });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (input === "3") {
|
|
94
|
+
dispatch({ type: "set_tab", tab: "box" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|