playball 3.2.0 → 3.4.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/README.md +49 -12
- package/dist/cli.js +1 -1
- package/dist/components/AllPlays.js +66 -40
- package/dist/components/App.js +25 -5
- package/dist/components/BoxScore.js +69 -0
- package/dist/components/FinishedGame.js +71 -34
- package/dist/components/Game.js +5 -3
- package/dist/components/GameList.js +7 -11
- package/dist/components/LiveGame.js +30 -2
- package/dist/components/PreviewGame.js +3 -4
- package/dist/components/Standings.js +43 -1
- package/dist/components/Table.js +44 -0
- package/dist/config.js +13 -1
- package/dist/features/games.js +46 -4
- package/dist/features/schedule.js +26 -3
- package/dist/features/standings.js +13 -3
- package/dist/main.js +5 -2
- package/dist/utils.js +22 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -25,18 +25,27 @@ $ playball
|
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
### Docker
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
$ docker run -it --rm
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
38
|
-
$ docker
|
|
39
|
-
```
|
|
28
|
+
Don't have Node.js installed? You can run via Docker instead.
|
|
29
|
+
```
|
|
30
|
+
$ docker run -it --rm paaatrick0/playball
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> [!TIP]
|
|
34
|
+
> When running via Docker, times will be shown by default in Eastern Time. To change this, set the `TZ` environment variable to your desired timezone.
|
|
35
|
+
>
|
|
36
|
+
> For Central Time use:
|
|
37
|
+
> ```
|
|
38
|
+
> $ docker run -it --rm -e TZ=America/Chicago paaatrick0/playball
|
|
39
|
+
> ```
|
|
40
|
+
> For Mountain Time use:
|
|
41
|
+
> ```
|
|
42
|
+
> $ docker run -it --rm -e TZ=America/Denver paaatrick0/playball
|
|
43
|
+
> ```
|
|
44
|
+
> For Pacific Time use:
|
|
45
|
+
> ```
|
|
46
|
+
> $ docker run -it --rm -e TZ=America/Los_Angeles paaatrick0/playball
|
|
47
|
+
> ```
|
|
48
|
+
> For other timezones see the list of [TZ database time zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
|
40
49
|
|
|
41
50
|
### Keys
|
|
42
51
|
#### Global
|
|
@@ -60,6 +69,32 @@ key | action
|
|
|
60
69
|
----|--------
|
|
61
70
|
<kbd>↓</kbd>/<kbd>j</kbd>, <kbd>↑</kbd>/<kbd>k</kbd> | scroll list of all plays
|
|
62
71
|
|
|
72
|
+
### World Baseball Classic
|
|
73
|
+
|
|
74
|
+
Watch World Baseball Classic games by setting the sport configuration:
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
playball config sport wbc
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Or use an environment variable for one-time viewing:
|
|
81
|
+
|
|
82
|
+
```shell
|
|
83
|
+
PLAYBALL_SPORT=wbc playball
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
To switch back to MLB:
|
|
87
|
+
|
|
88
|
+
```shell
|
|
89
|
+
playball config sport mlb
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When running via Docker:
|
|
93
|
+
|
|
94
|
+
```shell
|
|
95
|
+
docker run -it --rm -e PLAYBALL_SPORT=wbc paaatrick0/playball
|
|
96
|
+
```
|
|
97
|
+
|
|
63
98
|
### Configuration
|
|
64
99
|
|
|
65
100
|
Playball can be configured using the `config` subcommand. To list the current configuration values run the subcommand with no additional arguments:
|
|
@@ -122,6 +157,8 @@ key | description | default | allowed values
|
|
|
122
157
|
`color.walk` | Color of result where play ends on a ball (walk, hit by pitch) in list of plays in game view | green | _See above_
|
|
123
158
|
`favorites` | Teams to highlight in schedule and standings views | | Any one of the following: `ATL`, `AZ`, `BAL`, `BOS`, `CHC`, `CIN`, `CLE`, `COL`, `CWS`, `DET`, `HOU`, `KC`, `LAA`, `LAD`, `MIA`, `MIL`, `MIN`, `NYM`, `NYY`, `OAK`, `PHI`, `PIT`, `SD`, `SEA`, `SF`, `STL`, `TB`, `TEX`, `TOR`, `WSH`. Or a comma-separated list of multiple (e.g. `SEA,MIL`).<br/><br />Note: in some terminals the list must be quoted: `playball config favorites "SEA,MIL"`
|
|
124
159
|
`title` | If enabled, the terminal title will be set to the score of the current game | `false` | `false`, `true`
|
|
160
|
+
`live-delay` | Number of seconds to delay the live game stream. Useful when watching with delayed broadcast streams. | `0` (no delay) | Any positive number
|
|
161
|
+
`sport` | Which sport/league to display | `mlb` | `mlb`, `wbc`
|
|
125
162
|
|
|
126
163
|
### Development
|
|
127
164
|
```
|
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ const notifier = updateNotifier({
|
|
|
10
10
|
updateCheckInterval: 1000 * 60 * 60 * 24 * 7 // 1 week
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
program.name(pkg.name).description(pkg.description).version(pkg.version).action(main).hook('postAction', () => notifier.notify({
|
|
13
|
+
program.name(pkg.name).description(pkg.description).version(pkg.version).option('--replay <gameId>', 'Replay a game by game ID').option('--date <date>', 'Open schedule to specific date (YYYY-MM-DD)').action(main).hook('postAction', () => notifier.notify({
|
|
14
14
|
isGlobal: true
|
|
15
15
|
}));
|
|
16
16
|
program.command('config').description('Set or get configration values').argument('[key]').argument('[value]').option('--unset', 'Unset configuration value').action((key, value, options) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
3
4
|
import { selectAllPlays, selectTeams } from "../features/games.js";
|
|
4
5
|
import { get } from "../config.js";
|
|
5
6
|
import style from "../style/index.js";
|
|
@@ -19,28 +20,46 @@ function getPlayResultColor(play) {
|
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
function formatOut(out) {
|
|
22
|
-
return ` {bold}${out} out{/}`;
|
|
23
|
+
return ` {bold}${out} out${out > 1 ? 's' : ''}{/}`;
|
|
23
24
|
}
|
|
24
|
-
function AllPlays(
|
|
25
|
+
function AllPlays({
|
|
26
|
+
reverse,
|
|
27
|
+
scoringOnly
|
|
28
|
+
}) {
|
|
25
29
|
const plays = useSelector(selectAllPlays);
|
|
26
30
|
const teams = useSelector(selectTeams);
|
|
27
31
|
const formatScoreDetail = scoreObj => ` {bold}{${get('color.in-play-runs-bg')}-bg}{${get('color.in-play-runs-fg')}-fg} ` + `${teams.away.abbreviation} ${scoreObj.awayScore} - ` + `${teams.home.abbreviation} ${scoreObj.homeScore}` + ' {/}';
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let lastPlay;
|
|
32
|
-
if (playIdx < plays.length - 1) {
|
|
33
|
-
lastPlay = plays[playIdx + 1];
|
|
34
|
-
}
|
|
32
|
+
const playsByInning = {};
|
|
33
|
+
plays === null || plays === void 0 ? void 0 : plays.forEach(play => {
|
|
34
|
+
var _play$playEvents2;
|
|
35
35
|
const playInning = play.about.halfInning + ' ' + play.about.inning;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
(_play$playEvents2 = play.playEvents) === null || _play$playEvents2 === void 0 ? void 0 : _play$playEvents2.forEach(event => {
|
|
37
|
+
if (event.type === 'action') {
|
|
38
|
+
if (scoringOnly && !event.details.isScoringPlay) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (event.details.eventType === 'batter_timeout' || event.details.eventType === 'mound_visit') {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let line = '';
|
|
45
|
+
if (event.details.event) {
|
|
46
|
+
line += `{${get('color.other-event')}-fg}[${event.details.event}]{/} `;
|
|
47
|
+
}
|
|
48
|
+
line += event.details.description;
|
|
49
|
+
if (event.details.isOut) {
|
|
50
|
+
var _event$count;
|
|
51
|
+
line += formatOut((_event$count = event.count) === null || _event$count === void 0 ? void 0 : _event$count.outs);
|
|
52
|
+
}
|
|
53
|
+
if (event.isScoringPlay || event.details.isScoringPlay) {
|
|
54
|
+
line += formatScoreDetail(event.details);
|
|
55
|
+
}
|
|
56
|
+
if (!(playInning in playsByInning)) {
|
|
57
|
+
playsByInning[playInning] = [];
|
|
58
|
+
}
|
|
59
|
+
playsByInning[playInning].push(line);
|
|
40
60
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (play.about.isComplete) {
|
|
61
|
+
});
|
|
62
|
+
if (play.about.isComplete && (!scoringOnly || play.about.isScoringPlay)) {
|
|
44
63
|
const color = getPlayResultColor(play);
|
|
45
64
|
let line = `{${color}-fg}[${play.result.event}]{/} ${play.result.description}`;
|
|
46
65
|
if (play.about.hasOut) {
|
|
@@ -52,32 +71,35 @@ function AllPlays() {
|
|
|
52
71
|
if (play.about.isScoringPlay) {
|
|
53
72
|
line += formatScoreDetail(play.result);
|
|
54
73
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
play.playEvents && play.playEvents.slice().reverse().forEach((event, eventIdx, events) => {
|
|
58
|
-
if (event.type === 'action') {
|
|
59
|
-
var _event$count;
|
|
60
|
-
let line = '';
|
|
61
|
-
if (event.details.event) {
|
|
62
|
-
line += `{${get('color.other-event')}-fg}[${event.details.event}]{/} `;
|
|
63
|
-
}
|
|
64
|
-
line += event.details.description;
|
|
65
|
-
if (event.isScoringPlay || event.details.isScoringPlay) {
|
|
66
|
-
line += formatScoreDetail(event.details);
|
|
67
|
-
}
|
|
68
|
-
const currentOut = (_event$count = event.count) === null || _event$count === void 0 ? void 0 : _event$count.outs;
|
|
69
|
-
let prevOut = lastPlay ? lastPlay.count.outs : 0;
|
|
70
|
-
if (eventIdx < events.length - 1) {
|
|
71
|
-
var _events$count;
|
|
72
|
-
prevOut = (_events$count = events[eventIdx + 1].count) === null || _events$count === void 0 ? void 0 : _events$count.outs;
|
|
73
|
-
}
|
|
74
|
-
if (currentOut > prevOut) {
|
|
75
|
-
line += formatOut(currentOut);
|
|
76
|
-
}
|
|
77
|
-
lines.push(line);
|
|
74
|
+
if (!(playInning in playsByInning)) {
|
|
75
|
+
playsByInning[playInning] = [];
|
|
78
76
|
}
|
|
79
|
-
|
|
77
|
+
playsByInning[playInning].push(line);
|
|
78
|
+
}
|
|
80
79
|
});
|
|
80
|
+
const lines = [];
|
|
81
|
+
const inningKeys = Object.keys(playsByInning);
|
|
82
|
+
if (reverse) {
|
|
83
|
+
inningKeys.reverse();
|
|
84
|
+
}
|
|
85
|
+
inningKeys.forEach(inning => {
|
|
86
|
+
if (lines.length > 0) {
|
|
87
|
+
lines.push('');
|
|
88
|
+
}
|
|
89
|
+
lines.push(`{bold}[${inning.toUpperCase()}]{/}`);
|
|
90
|
+
if (reverse) {
|
|
91
|
+
lines.push(...playsByInning[inning].slice().reverse());
|
|
92
|
+
} else {
|
|
93
|
+
lines.push(...playsByInning[inning]);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (lines.length === 0) {
|
|
97
|
+
if (scoringOnly) {
|
|
98
|
+
lines.push('No scoring plays yet');
|
|
99
|
+
} else {
|
|
100
|
+
lines.push('No plays yet');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
81
103
|
return /*#__PURE__*/React.createElement("box", {
|
|
82
104
|
content: lines.join('\n'),
|
|
83
105
|
focused: true,
|
|
@@ -90,4 +112,8 @@ function AllPlays() {
|
|
|
90
112
|
tags: true
|
|
91
113
|
});
|
|
92
114
|
}
|
|
115
|
+
AllPlays.propTypes = {
|
|
116
|
+
reverse: PropTypes.bool,
|
|
117
|
+
scoringOnly: PropTypes.bool
|
|
118
|
+
};
|
|
93
119
|
export default AllPlays;
|
package/dist/components/App.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { useDispatch } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import PropTypes from 'prop-types';
|
|
4
|
+
import { parseISO } from 'date-fns';
|
|
3
5
|
import GameList from "./GameList.js";
|
|
4
6
|
import HelpBar from "./HelpBar.js";
|
|
5
|
-
import {
|
|
7
|
+
import { setLiveGame, setReplayGame } from "../features/games.js";
|
|
6
8
|
import Game from "./Game.js";
|
|
7
9
|
import useKey from "../hooks/useKey.js";
|
|
8
10
|
import Standings from "./Standings.js";
|
|
11
|
+
import { setDate } from "../features/schedule.js";
|
|
9
12
|
const SCHEDULE = 'schedule';
|
|
10
13
|
const STANDINGS = 'standings';
|
|
11
14
|
const GAME = 'game';
|
|
12
|
-
function App(
|
|
15
|
+
function App({
|
|
16
|
+
replayId,
|
|
17
|
+
defaultDate
|
|
18
|
+
}) {
|
|
13
19
|
const [view, setView] = useState(SCHEDULE);
|
|
14
20
|
const dispatch = useDispatch();
|
|
15
21
|
useKey('c', () => {
|
|
16
22
|
setView(SCHEDULE);
|
|
17
|
-
dispatch(
|
|
23
|
+
dispatch(setLiveGame(null));
|
|
18
24
|
}, {
|
|
19
25
|
key: 'C',
|
|
20
26
|
label: 'Schedule'
|
|
@@ -24,9 +30,19 @@ function App() {
|
|
|
24
30
|
label: 'Standings'
|
|
25
31
|
});
|
|
26
32
|
const handleGameSelect = game => {
|
|
27
|
-
dispatch(
|
|
33
|
+
dispatch(setLiveGame(game.gamePk));
|
|
28
34
|
setView(GAME);
|
|
29
35
|
};
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (replayId) {
|
|
38
|
+
dispatch(setReplayGame(replayId)).unwrap().then(() => setView(GAME)).catch(() => setView(SCHEDULE));
|
|
39
|
+
}
|
|
40
|
+
}, [replayId]);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (defaultDate) {
|
|
43
|
+
dispatch(setDate(parseISO(defaultDate)));
|
|
44
|
+
}
|
|
45
|
+
}, [defaultDate]);
|
|
30
46
|
return /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("element", {
|
|
31
47
|
top: 0,
|
|
32
48
|
left: 0,
|
|
@@ -39,4 +55,8 @@ function App() {
|
|
|
39
55
|
height: 1
|
|
40
56
|
}, /*#__PURE__*/React.createElement(HelpBar, null)));
|
|
41
57
|
}
|
|
58
|
+
App.propTypes = {
|
|
59
|
+
replayId: PropTypes.string,
|
|
60
|
+
defaultDate: PropTypes.string
|
|
61
|
+
};
|
|
42
62
|
export default App;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import { selectBoxscore, selectPlayers } from "../features/games.js";
|
|
4
|
+
import Table from "./Table.js";
|
|
5
|
+
function getBatterRows(boxscoreTeam, players) {
|
|
6
|
+
const batters = Object.values(boxscoreTeam.players).filter(player => player.battingOrder !== undefined).sort((a, b) => parseInt(a.battingOrder) - parseInt(b.battingOrder));
|
|
7
|
+
const batterNames = batters.map(batter => {
|
|
8
|
+
var _batter$allPositions, _batter$gameStatus;
|
|
9
|
+
const name = players[`ID${batter.person.id}`].boxscoreName;
|
|
10
|
+
const positions = (_batter$allPositions = batter.allPositions) === null || _batter$allPositions === void 0 ? void 0 : _batter$allPositions.map(pos => pos.abbreviation).join('-');
|
|
11
|
+
const prefix = (_batter$gameStatus = batter.gameStatus) !== null && _batter$gameStatus !== void 0 && _batter$gameStatus.isSubstitute ? ' ' : '';
|
|
12
|
+
return `${prefix}${name} (${positions})`;
|
|
13
|
+
});
|
|
14
|
+
return [...batters.map((batter, idx) => [batterNames[idx], batter.stats.batting.atBats.toString(), batter.stats.batting.runs.toString(), batter.stats.batting.hits.toString(), batter.stats.batting.rbi.toString(), batter.stats.batting.baseOnBalls.toString(), batter.stats.batting.strikeOuts.toString(), batter.seasonStats.batting.avg, batter.seasonStats.batting.ops]), ['Totals', boxscoreTeam.teamStats.batting.atBats.toString(), boxscoreTeam.teamStats.batting.runs.toString(), boxscoreTeam.teamStats.batting.hits.toString(), boxscoreTeam.teamStats.batting.rbi.toString(), boxscoreTeam.teamStats.batting.baseOnBalls.toString(), boxscoreTeam.teamStats.batting.strikeOuts.toString(), '', '']];
|
|
15
|
+
}
|
|
16
|
+
function getPitcherRows(boxscoreTeam, players) {
|
|
17
|
+
const pitchers = boxscoreTeam.pitchers.map(pitcherId => boxscoreTeam.players['ID' + pitcherId]);
|
|
18
|
+
const pitcherNames = pitchers.map(pitcher => {
|
|
19
|
+
const name = players[`ID${pitcher.person.id}`].boxscoreName;
|
|
20
|
+
const note = pitcher.stats.pitching.note ? ` ${pitcher.stats.pitching.note}` : '';
|
|
21
|
+
return `${name}${note}`;
|
|
22
|
+
});
|
|
23
|
+
return [...pitchers.map((pitcher, idx) => [pitcherNames[idx], pitcher.stats.pitching.inningsPitched, pitcher.stats.pitching.hits.toString(), pitcher.stats.pitching.runs.toString(), pitcher.stats.pitching.earnedRuns.toString(), pitcher.stats.pitching.baseOnBalls.toString(), pitcher.stats.pitching.strikeOuts.toString(), pitcher.stats.pitching.homeRuns.toString(), pitcher.seasonStats.pitching.era]), ['Totals', boxscoreTeam.teamStats.pitching.inningsPitched, boxscoreTeam.teamStats.pitching.hits.toString(), boxscoreTeam.teamStats.pitching.runs.toString(), boxscoreTeam.teamStats.pitching.earnedRuns.toString(), boxscoreTeam.teamStats.pitching.baseOnBalls.toString(), boxscoreTeam.teamStats.pitching.strikeOuts.toString(), boxscoreTeam.teamStats.pitching.homeRuns.toString(), '']];
|
|
24
|
+
}
|
|
25
|
+
function BoxScore({
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
const boxscore = useSelector(selectBoxscore);
|
|
29
|
+
const players = useSelector(selectPlayers);
|
|
30
|
+
const batterHeader = ['Batters', 'AB', 'R', 'H', 'RBI', 'BB', 'K', 'AVG', 'OPS'];
|
|
31
|
+
const batterWidths = ['auto', 4, 4, 4, 4, 4, 4, 6, 6];
|
|
32
|
+
const awayBatterRows = getBatterRows(boxscore.away, players);
|
|
33
|
+
const homeBatterRows = getBatterRows(boxscore.home, players);
|
|
34
|
+
const pitcherHeader = ['Pitchers', 'IP', 'H', 'R', 'ER', 'BB', 'K', 'HR', 'ERA'];
|
|
35
|
+
const pitcherWidths = ['auto', 5, 4, 4, 4, 4, 4, 4, 6];
|
|
36
|
+
const pitcherStart = Math.max(awayBatterRows.length, homeBatterRows.length) + 2;
|
|
37
|
+
const awayPitcherRows = getPitcherRows(boxscore.away, players);
|
|
38
|
+
const homePitcherRows = getPitcherRows(boxscore.home, players);
|
|
39
|
+
return /*#__PURE__*/React.createElement("element", props, /*#__PURE__*/React.createElement(Table, {
|
|
40
|
+
top: 0,
|
|
41
|
+
left: 0,
|
|
42
|
+
width: "50%-1",
|
|
43
|
+
headers: batterHeader,
|
|
44
|
+
widths: batterWidths,
|
|
45
|
+
rows: awayBatterRows
|
|
46
|
+
}), /*#__PURE__*/React.createElement(Table, {
|
|
47
|
+
top: 0,
|
|
48
|
+
left: "50%+1",
|
|
49
|
+
width: "50%-1",
|
|
50
|
+
headers: batterHeader,
|
|
51
|
+
widths: batterWidths,
|
|
52
|
+
rows: homeBatterRows
|
|
53
|
+
}), /*#__PURE__*/React.createElement(Table, {
|
|
54
|
+
top: pitcherStart,
|
|
55
|
+
left: 0,
|
|
56
|
+
width: "50%-1",
|
|
57
|
+
headers: pitcherHeader,
|
|
58
|
+
widths: pitcherWidths,
|
|
59
|
+
rows: awayPitcherRows
|
|
60
|
+
}), /*#__PURE__*/React.createElement(Table, {
|
|
61
|
+
top: pitcherStart,
|
|
62
|
+
left: "50%+1",
|
|
63
|
+
width: "50%-1",
|
|
64
|
+
headers: pitcherHeader,
|
|
65
|
+
widths: pitcherWidths,
|
|
66
|
+
rows: homePitcherRows
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
export default BoxScore;
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
+
import figlet from 'figlet';
|
|
3
4
|
import LineScore from "./LineScore.js";
|
|
4
5
|
import { get } from "../config.js";
|
|
5
|
-
import {
|
|
6
|
+
import { selectBoxscore, selectDecisions, selectGameStatus, selectLineScore, selectSelectedId, selectTeams, setReplayGame } from "../features/games.js";
|
|
6
7
|
import { resetTitle, setTitle } from "../screen.js";
|
|
8
|
+
import useKey from "../hooks/useKey.js";
|
|
9
|
+
import BoxScore from "./BoxScore.js";
|
|
10
|
+
import AllPlays from "./AllPlays.js";
|
|
11
|
+
const BOX_SCORE = 'BOX_SCORE';
|
|
12
|
+
const ALL_PLAYS = 'ALL_PLAYS';
|
|
13
|
+
const SCORING_PLAYS = 'SCORING_PLAYS';
|
|
7
14
|
const getPlayer = (id, boxscore) => {
|
|
8
15
|
var _boxscore$home, _boxscore$away;
|
|
9
16
|
const homePlayer = (_boxscore$home = boxscore.home) === null || _boxscore$home === void 0 || (_boxscore$home = _boxscore$home.players) === null || _boxscore$home === void 0 ? void 0 : _boxscore$home['ID' + id];
|
|
@@ -37,24 +44,31 @@ const formatDecisions = (decisions, boxscore) => {
|
|
|
37
44
|
}
|
|
38
45
|
return content.join('\n');
|
|
39
46
|
};
|
|
40
|
-
const formatScore = (status, linescore) => {
|
|
41
|
-
let display = '';
|
|
42
|
-
if (status.detailedState === 'Postponed') {
|
|
43
|
-
display = status.detailedState;
|
|
44
|
-
if (status.reason) {
|
|
45
|
-
display += '\n' + status.reason;
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
display = `\n${linescore.teams.away.runs} - ${linescore.teams.home.runs}`;
|
|
49
|
-
}
|
|
50
|
-
return display;
|
|
51
|
-
};
|
|
52
47
|
function FinishedGame() {
|
|
48
|
+
const dispatch = useDispatch();
|
|
49
|
+
const id = useSelector(selectSelectedId);
|
|
53
50
|
const boxscore = useSelector(selectBoxscore);
|
|
54
51
|
const decisions = useSelector(selectDecisions);
|
|
55
52
|
const linescore = useSelector(selectLineScore);
|
|
56
53
|
const status = useSelector(selectGameStatus);
|
|
57
54
|
const teams = useSelector(selectTeams);
|
|
55
|
+
const [view, setView] = React.useState(BOX_SCORE);
|
|
56
|
+
useKey('r', () => dispatch(setReplayGame(id)), {
|
|
57
|
+
key: 'R',
|
|
58
|
+
label: 'Replay'
|
|
59
|
+
});
|
|
60
|
+
useKey('b', () => setView(BOX_SCORE), {
|
|
61
|
+
key: 'B',
|
|
62
|
+
label: 'Box Score'
|
|
63
|
+
});
|
|
64
|
+
useKey('a', () => setView(ALL_PLAYS), {
|
|
65
|
+
key: 'A',
|
|
66
|
+
label: 'All Plays'
|
|
67
|
+
});
|
|
68
|
+
useKey('p', () => setView(SCORING_PLAYS), {
|
|
69
|
+
key: 'P',
|
|
70
|
+
label: 'Scoring Plays'
|
|
71
|
+
});
|
|
58
72
|
useEffect(() => {
|
|
59
73
|
if (get('title')) {
|
|
60
74
|
const homeRuns = linescore.teams['home'].runs;
|
|
@@ -67,38 +81,61 @@ function FinishedGame() {
|
|
|
67
81
|
};
|
|
68
82
|
}
|
|
69
83
|
}, [get, linescore, resetTitle, setTitle, teams]);
|
|
84
|
+
const bigTextOptions = {
|
|
85
|
+
font: 'Small Block'
|
|
86
|
+
};
|
|
70
87
|
const awayTeam = `${teams.away.teamName}\n(${teams.away.record.wins}-${teams.away.record.losses})`;
|
|
88
|
+
const awayRuns = useMemo(() => figlet.textSync(linescore.teams.away.runs, bigTextOptions), [linescore.teams.away.runs]);
|
|
71
89
|
const homeTeam = `${teams.home.teamName}\n(${teams.home.record.wins}-${teams.home.record.losses})`;
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const homeRuns = useMemo(() => figlet.textSync(linescore.teams.home.runs, bigTextOptions), [linescore.teams.home.runs]);
|
|
91
|
+
return /*#__PURE__*/React.createElement("element", null, status.detailedState === 'Postponed' ? /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("box", {
|
|
92
|
+
top: 1
|
|
93
|
+
}, "status.detailedState"), /*#__PURE__*/React.createElement("box", {
|
|
94
|
+
top: 2
|
|
95
|
+
}, status.reason)) : /*#__PURE__*/React.createElement("element", {
|
|
96
|
+
top: 1
|
|
74
97
|
}, /*#__PURE__*/React.createElement("box", {
|
|
98
|
+
top: 1,
|
|
99
|
+
left: 0,
|
|
100
|
+
width: "25%",
|
|
75
101
|
content: awayTeam,
|
|
76
|
-
|
|
77
|
-
|
|
102
|
+
align: "right"
|
|
103
|
+
}), /*#__PURE__*/React.createElement("box", {
|
|
104
|
+
top: 0,
|
|
105
|
+
left: "25%",
|
|
106
|
+
width: "25%",
|
|
107
|
+
content: awayRuns,
|
|
78
108
|
align: "center"
|
|
79
109
|
}), /*#__PURE__*/React.createElement("box", {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
top: 0,
|
|
111
|
+
left: "50%",
|
|
112
|
+
width: "25%",
|
|
113
|
+
content: homeRuns,
|
|
84
114
|
align: "center"
|
|
85
115
|
}), /*#__PURE__*/React.createElement("box", {
|
|
116
|
+
top: 1,
|
|
117
|
+
left: "75%",
|
|
118
|
+
width: "25%",
|
|
86
119
|
content: homeTeam,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
top: "60%+1",
|
|
93
|
-
height: 3
|
|
120
|
+
align: "left"
|
|
121
|
+
}), /*#__PURE__*/React.createElement("element", {
|
|
122
|
+
top: 5,
|
|
123
|
+
left: 0,
|
|
124
|
+
width: "50%-2"
|
|
94
125
|
}, /*#__PURE__*/React.createElement(LineScore, {
|
|
95
|
-
align: "
|
|
126
|
+
align: "right",
|
|
96
127
|
final: true
|
|
97
128
|
})), /*#__PURE__*/React.createElement("element", {
|
|
98
|
-
top:
|
|
99
|
-
left: "50
|
|
129
|
+
top: 5,
|
|
130
|
+
left: "50%+2",
|
|
131
|
+
width: "50%-2",
|
|
132
|
+
align: "right"
|
|
100
133
|
}, /*#__PURE__*/React.createElement("box", {
|
|
101
134
|
content: formatDecisions(decisions, boxscore)
|
|
102
|
-
}))
|
|
135
|
+
})), /*#__PURE__*/React.createElement("element", {
|
|
136
|
+
top: 9
|
|
137
|
+
}, view === BOX_SCORE && /*#__PURE__*/React.createElement(BoxScore, null), view === ALL_PLAYS && /*#__PURE__*/React.createElement(AllPlays, null), view === SCORING_PLAYS && /*#__PURE__*/React.createElement(AllPlays, {
|
|
138
|
+
scoringOnly: true
|
|
139
|
+
}))));
|
|
103
140
|
}
|
|
104
141
|
export default FinishedGame;
|
package/dist/components/Game.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
-
import { fetchGame, selectGame, selectSelectedId, selectFullUpdateRequired } from "../features/games.js";
|
|
3
|
+
import { fetchGame, selectGame, selectSelectedId, selectFullUpdateRequired, selectDelay } from "../features/games.js";
|
|
4
4
|
import PreviewGame from "./PreviewGame.js";
|
|
5
5
|
import LiveGame from "./LiveGame.js";
|
|
6
6
|
import FinishedGame from "./FinishedGame.js";
|
|
@@ -11,13 +11,15 @@ function Game() {
|
|
|
11
11
|
const game = useSelector(selectGame);
|
|
12
12
|
const fullUpdateRequired = useSelector(selectFullUpdateRequired);
|
|
13
13
|
const id = useSelector(selectSelectedId);
|
|
14
|
+
const delay = useSelector(selectDelay);
|
|
14
15
|
const timerRef = useRef(null);
|
|
15
16
|
const timestampRef = useRef();
|
|
16
17
|
timestampRef.current = fullUpdateRequired ? null : game === null || game === void 0 || (_game$metaData = game.metaData) === null || _game$metaData === void 0 ? void 0 : _game$metaData.timeStamp;
|
|
17
18
|
const updateGameData = () => {
|
|
18
19
|
dispatch(fetchGame({
|
|
19
20
|
id,
|
|
20
|
-
start: timestampRef.current
|
|
21
|
+
start: timestampRef.current,
|
|
22
|
+
delay
|
|
21
23
|
})).unwrap().then(result => {
|
|
22
24
|
var _result$metaData;
|
|
23
25
|
const wait = (result && ((_result$metaData = result.metaData) === null || _result$metaData === void 0 ? void 0 : _result$metaData.wait) || 10) * 1000;
|
|
@@ -29,7 +31,7 @@ function Game() {
|
|
|
29
31
|
return () => {
|
|
30
32
|
clearTimeout(timerRef.current);
|
|
31
33
|
};
|
|
32
|
-
}, [id]);
|
|
34
|
+
}, [id, delay]);
|
|
33
35
|
if (!game) {
|
|
34
36
|
return /*#__PURE__*/React.createElement("element", null);
|
|
35
37
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
|
|
2
|
-
import React, { useCallback, useEffect, useRef
|
|
2
|
+
import React, { useCallback, useEffect, useRef } from 'react';
|
|
3
3
|
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
4
4
|
import PropTypes from 'prop-types';
|
|
5
|
-
import {
|
|
6
|
-
import { fetchSchedule, selectData, selectLoading } from "../features/schedule.js";
|
|
5
|
+
import { format } from 'date-fns';
|
|
6
|
+
import { fetchSchedule, nextDay, prevDay, selectData, selectLoading, selectScheduleDate, setDate } from "../features/schedule.js";
|
|
7
7
|
import { teamFavoriteStar } from "../utils.js";
|
|
8
8
|
import Grid from "./Grid.js";
|
|
9
9
|
import useKey from "../hooks/useKey.js";
|
|
@@ -98,8 +98,8 @@ function GameList({
|
|
|
98
98
|
const dispatch = useDispatch();
|
|
99
99
|
const schedule = useSelector(selectData);
|
|
100
100
|
const loading = useSelector(selectLoading);
|
|
101
|
+
const date = useSelector(selectScheduleDate);
|
|
101
102
|
const timerRef = useRef(null);
|
|
102
|
-
const [date, setDate] = useState(new Date());
|
|
103
103
|
let games = [];
|
|
104
104
|
if (schedule && schedule.dates.length > 0) {
|
|
105
105
|
games = schedule.dates[0].games.slice().sort(compareGames);
|
|
@@ -109,19 +109,15 @@ function GameList({
|
|
|
109
109
|
timerRef.current = setInterval(() => dispatch(fetchSchedule(date)), 30000);
|
|
110
110
|
return () => clearInterval(timerRef.current);
|
|
111
111
|
}, [date]);
|
|
112
|
-
useKey('p', useCallback(() =>
|
|
113
|
-
days: -1
|
|
114
|
-
})), []), {
|
|
112
|
+
useKey('p', useCallback(() => dispatch(prevDay()), []), {
|
|
115
113
|
key: 'P',
|
|
116
114
|
label: 'Prev Day'
|
|
117
115
|
});
|
|
118
|
-
useKey('n', useCallback(() =>
|
|
119
|
-
days: 1
|
|
120
|
-
})), []), {
|
|
116
|
+
useKey('n', useCallback(() => dispatch(nextDay()), []), {
|
|
121
117
|
key: 'N',
|
|
122
118
|
label: 'Next Day'
|
|
123
119
|
});
|
|
124
|
-
useKey('t', useCallback(() => setDate(new Date()), []), {
|
|
120
|
+
useKey('t', useCallback(() => dispatch(setDate(new Date())), []), {
|
|
125
121
|
key: 'T',
|
|
126
122
|
label: 'Today'
|
|
127
123
|
});
|
|
@@ -2,6 +2,7 @@ import React, { useEffect } from 'react';
|
|
|
2
2
|
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
3
|
import Count from "./Count.js";
|
|
4
4
|
import Bases from "./Bases.js";
|
|
5
|
+
import BoxScore from "./BoxScore.js";
|
|
5
6
|
import LineScore from "./LineScore.js";
|
|
6
7
|
import Matchup from "./Matchup.js";
|
|
7
8
|
import AtBat from "./AtBat.js";
|
|
@@ -10,10 +11,27 @@ import InningDisplay from "./InningDisplay.js";
|
|
|
10
11
|
import { get } from "../config.js";
|
|
11
12
|
import { selectGameStatus, selectLineScore, selectTeams } from "../features/games.js";
|
|
12
13
|
import { resetTitle, setTitle } from "../screen.js";
|
|
14
|
+
import useKey from "../hooks/useKey.js";
|
|
15
|
+
const GAME_STATUS = 'GAME_STATUS';
|
|
16
|
+
const BOX_SCORE = 'BOX_SCORE';
|
|
17
|
+
const SCORING_PLAYS = 'SCORING_PLAYS';
|
|
13
18
|
function LiveGame() {
|
|
14
19
|
const gameStatus = useSelector(selectGameStatus);
|
|
15
20
|
const linescore = useSelector(selectLineScore);
|
|
16
21
|
const teams = useSelector(selectTeams);
|
|
22
|
+
const [view, setView] = React.useState(GAME_STATUS);
|
|
23
|
+
useKey('g', () => setView(GAME_STATUS), {
|
|
24
|
+
key: 'G',
|
|
25
|
+
label: 'Game Status'
|
|
26
|
+
});
|
|
27
|
+
useKey('b', () => setView(BOX_SCORE), {
|
|
28
|
+
key: 'B',
|
|
29
|
+
label: 'Box Score'
|
|
30
|
+
});
|
|
31
|
+
useKey('p', () => setView(SCORING_PLAYS), {
|
|
32
|
+
key: 'P',
|
|
33
|
+
label: 'Scoring Plays'
|
|
34
|
+
});
|
|
17
35
|
useEffect(() => {
|
|
18
36
|
if (get('title')) {
|
|
19
37
|
const homeRuns = linescore.teams['home'].runs;
|
|
@@ -62,7 +80,7 @@ function LiveGame() {
|
|
|
62
80
|
type: "line",
|
|
63
81
|
top: 3,
|
|
64
82
|
width: "100%"
|
|
65
|
-
}), /*#__PURE__*/React.createElement("element", {
|
|
83
|
+
}), view === GAME_STATUS && /*#__PURE__*/React.createElement("element", {
|
|
66
84
|
top: 4,
|
|
67
85
|
left: 1
|
|
68
86
|
}, /*#__PURE__*/React.createElement("element", {
|
|
@@ -79,6 +97,16 @@ function LiveGame() {
|
|
|
79
97
|
}), /*#__PURE__*/React.createElement("element", {
|
|
80
98
|
left: "50%+2",
|
|
81
99
|
width: "50%-2"
|
|
82
|
-
}, /*#__PURE__*/React.createElement(AllPlays,
|
|
100
|
+
}, /*#__PURE__*/React.createElement(AllPlays, {
|
|
101
|
+
reverse: true
|
|
102
|
+
}))), view === BOX_SCORE && /*#__PURE__*/React.createElement("element", {
|
|
103
|
+
top: 4,
|
|
104
|
+
left: 1
|
|
105
|
+
}, /*#__PURE__*/React.createElement(BoxScore, null)), view === SCORING_PLAYS && /*#__PURE__*/React.createElement("element", {
|
|
106
|
+
top: 4,
|
|
107
|
+
left: 1
|
|
108
|
+
}, /*#__PURE__*/React.createElement(AllPlays, {
|
|
109
|
+
scoringOnly: true
|
|
110
|
+
})));
|
|
83
111
|
}
|
|
84
112
|
export default LiveGame;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect } from 'react';
|
|
2
2
|
import { useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
3
|
-
import { format } from 'date-fns';
|
|
3
|
+
import { format, isSameDay } from 'date-fns';
|
|
4
4
|
import { get } from "../config.js";
|
|
5
5
|
import { selectTeams, selectVenue, selectStartTime, selectBoxscore, selectProbablePitchers, selectGameStatus } from "../features/games.js";
|
|
6
6
|
import { resetTitle, setTitle } from "../screen.js";
|
|
@@ -42,10 +42,9 @@ function PreviewGame() {
|
|
|
42
42
|
|
|
43
43
|
// Only show the date if it's not today.
|
|
44
44
|
const startDate = new Date(startTime);
|
|
45
|
-
const today =
|
|
46
|
-
const gameDay = format(startDate, 'yyyy-DDD');
|
|
45
|
+
const today = new Date();
|
|
47
46
|
let start = format(startDate, 'p');
|
|
48
|
-
if (today
|
|
47
|
+
if (!isSameDay(startDate, today)) {
|
|
49
48
|
start = `${format(startDate, 'MMMM d, yyy')} ${start}`;
|
|
50
49
|
}
|
|
51
50
|
setTitle(`${away} - ${home} @ ${start}`);
|
|
@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
|
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
3
|
import { useDispatch, useSelector } from "react-redux/lib/alternate-renderers.js";
|
|
4
4
|
import { fetchStandings, selectData } from "../features/standings.js";
|
|
5
|
-
import { teamFavoriteStar } from "../utils.js";
|
|
5
|
+
import { teamFavoriteStar, getSport } from "../utils.js";
|
|
6
6
|
function formatHeaderRow(record) {
|
|
7
7
|
return record.division.nameShort.padEnd(15) + ' W' + ' L' + ' PCT' + ' GB' + ' WCGB' + ' L10' + ' STRK';
|
|
8
8
|
}
|
|
@@ -49,10 +49,52 @@ Division.propTypes = {
|
|
|
49
49
|
function Standings() {
|
|
50
50
|
const dispatch = useDispatch();
|
|
51
51
|
const standings = useSelector(selectData);
|
|
52
|
+
const sport = getSport();
|
|
52
53
|
useEffect(() => dispatch(fetchStandings()), []);
|
|
53
54
|
if (!standings) {
|
|
54
55
|
return /*#__PURE__*/React.createElement("element", null);
|
|
55
56
|
}
|
|
57
|
+
if (sport === 'wbc') {
|
|
58
|
+
// WBC Pool Standings
|
|
59
|
+
if (!standings.records || standings.records.length === 0) {
|
|
60
|
+
return /*#__PURE__*/React.createElement("box", {
|
|
61
|
+
top: 0,
|
|
62
|
+
left: 0,
|
|
63
|
+
width: "100%",
|
|
64
|
+
height: "100%"
|
|
65
|
+
}, /*#__PURE__*/React.createElement("text", {
|
|
66
|
+
top: 1,
|
|
67
|
+
left: 2
|
|
68
|
+
}, "WBC Standings not available during this phase of the tournament."), /*#__PURE__*/React.createElement("text", {
|
|
69
|
+
top: 3,
|
|
70
|
+
left: 2
|
|
71
|
+
}, "Use Schedule view to see games and results."));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Render WBC pools (records likely grouped by pool/division)
|
|
75
|
+
const halfPoint = Math.ceil(standings.records.length / 2);
|
|
76
|
+
return /*#__PURE__*/React.createElement("element", null, standings.records.slice(0, halfPoint).map((pool, idx) => {
|
|
77
|
+
var _pool$division;
|
|
78
|
+
return /*#__PURE__*/React.createElement(Division, {
|
|
79
|
+
top: idx * 7,
|
|
80
|
+
left: 0,
|
|
81
|
+
width: "50%-1",
|
|
82
|
+
key: ((_pool$division = pool.division) === null || _pool$division === void 0 ? void 0 : _pool$division.id) || idx,
|
|
83
|
+
record: pool
|
|
84
|
+
});
|
|
85
|
+
}), standings.records.slice(halfPoint).map((pool, idx) => {
|
|
86
|
+
var _pool$division2;
|
|
87
|
+
return /*#__PURE__*/React.createElement(Division, {
|
|
88
|
+
top: idx * 7,
|
|
89
|
+
left: "50%+1",
|
|
90
|
+
width: "50%-1",
|
|
91
|
+
key: ((_pool$division2 = pool.division) === null || _pool$division2 === void 0 ? void 0 : _pool$division2.id) || idx + halfPoint,
|
|
92
|
+
record: pool
|
|
93
|
+
});
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MLB AL/NL Standings (existing logic)
|
|
56
98
|
return /*#__PURE__*/React.createElement("element", null, standings.records.filter(record => record.league.id === 103).map((record, idx) => /*#__PURE__*/React.createElement(Division, {
|
|
57
99
|
top: idx * 7,
|
|
58
100
|
left: 0,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
function formatRow(row, widths) {
|
|
4
|
+
return row.map((cell, idx) => idx === 0 ? cell.padEnd(widths[idx]) : cell.padStart(widths[idx])).join('');
|
|
5
|
+
}
|
|
6
|
+
function Table({
|
|
7
|
+
headers,
|
|
8
|
+
widths,
|
|
9
|
+
rows,
|
|
10
|
+
...rest
|
|
11
|
+
}) {
|
|
12
|
+
const resolvedWidths = widths.map((width, idx) => {
|
|
13
|
+
if (width !== 'auto') {
|
|
14
|
+
return width;
|
|
15
|
+
}
|
|
16
|
+
return Math.max((headers[idx] || '').length, ...rows.map(row => (row[idx] || '').length));
|
|
17
|
+
});
|
|
18
|
+
const headerRow = formatRow(headers, resolvedWidths);
|
|
19
|
+
const contentRows = rows.map(row => formatRow(row, resolvedWidths));
|
|
20
|
+
return /*#__PURE__*/React.createElement("element", rest, /*#__PURE__*/React.createElement("box", {
|
|
21
|
+
top: 0,
|
|
22
|
+
left: 0,
|
|
23
|
+
width: "100%",
|
|
24
|
+
height: 1,
|
|
25
|
+
fg: "black",
|
|
26
|
+
bg: "white",
|
|
27
|
+
wrap: false,
|
|
28
|
+
content: headerRow
|
|
29
|
+
}), /*#__PURE__*/React.createElement("box", {
|
|
30
|
+
top: 1,
|
|
31
|
+
left: 0,
|
|
32
|
+
width: "100%",
|
|
33
|
+
height: rows.length,
|
|
34
|
+
wrap: false,
|
|
35
|
+
content: contentRows.join('\n')
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
Table.propTypes = {
|
|
39
|
+
headers: PropTypes.arrayOf(PropTypes.string),
|
|
40
|
+
widths: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.oneOf(['auto'])])),
|
|
41
|
+
rows: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
|
42
|
+
top: PropTypes.number
|
|
43
|
+
};
|
|
44
|
+
export default Table;
|
package/dist/config.js
CHANGED
|
@@ -79,6 +79,16 @@ const schema = {
|
|
|
79
79
|
'title': {
|
|
80
80
|
type: 'boolean',
|
|
81
81
|
default: false
|
|
82
|
+
},
|
|
83
|
+
'live-delay': {
|
|
84
|
+
type: 'number',
|
|
85
|
+
default: 0,
|
|
86
|
+
minimum: 0
|
|
87
|
+
},
|
|
88
|
+
'sport': {
|
|
89
|
+
type: 'string',
|
|
90
|
+
enum: ['mlb', 'wbc'],
|
|
91
|
+
default: 'mlb'
|
|
82
92
|
}
|
|
83
93
|
};
|
|
84
94
|
const config = new Conf({
|
|
@@ -92,11 +102,13 @@ function serialize(value) {
|
|
|
92
102
|
return value;
|
|
93
103
|
}
|
|
94
104
|
function deserialize(key, value) {
|
|
95
|
-
var _schema$key, _schema$key2;
|
|
105
|
+
var _schema$key, _schema$key2, _schema$key3;
|
|
96
106
|
if (value && ((_schema$key = schema[key]) === null || _schema$key === void 0 ? void 0 : _schema$key.type) === 'array') {
|
|
97
107
|
return value.split(/\s*,\s*/);
|
|
98
108
|
} else if (((_schema$key2 = schema[key]) === null || _schema$key2 === void 0 ? void 0 : _schema$key2.type) === 'boolean') {
|
|
99
109
|
return value === 'true';
|
|
110
|
+
} else if (((_schema$key3 = schema[key]) === null || _schema$key3 === void 0 ? void 0 : _schema$key3.type) === 'number') {
|
|
111
|
+
return parseInt(value);
|
|
100
112
|
}
|
|
101
113
|
return value;
|
|
102
114
|
}
|
package/dist/features/games.js
CHANGED
|
@@ -6,19 +6,42 @@ const {
|
|
|
6
6
|
createSelector
|
|
7
7
|
} = reduxjsToolkit;
|
|
8
8
|
import jsonpatch from 'json-patch';
|
|
9
|
+
import { UTCDate } from '@date-fns/utc';
|
|
10
|
+
import { addSeconds, differenceInSeconds, format } from 'date-fns';
|
|
11
|
+
import logger from "../logger.js";
|
|
12
|
+
import { get } from "../config.js";
|
|
9
13
|
const initialState = {
|
|
10
14
|
loading: false,
|
|
11
15
|
fullUpdateRequired: false,
|
|
12
16
|
error: null,
|
|
13
17
|
selectedId: null,
|
|
18
|
+
delay: 0,
|
|
14
19
|
games: {}
|
|
15
20
|
};
|
|
21
|
+
function makeDiffParams(start, end) {
|
|
22
|
+
const endParam = start ? '&endTimecode=' : '?timecode=';
|
|
23
|
+
const startParam = start ? `/diffPatch?startTimecode=${start}` : '';
|
|
24
|
+
if (end) {
|
|
25
|
+
return `${startParam}${endParam}${end}`;
|
|
26
|
+
} else {
|
|
27
|
+
return startParam;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
export const fetchGame = createAsyncThunk('games/fetch', async ({
|
|
17
31
|
id,
|
|
18
|
-
start
|
|
32
|
+
start,
|
|
33
|
+
delay
|
|
19
34
|
}) => {
|
|
20
|
-
const
|
|
35
|
+
const end = delay > 0 ? format(addSeconds(new UTCDate(Date.now()), -delay), 'yyyyMMdd_HHmmss') : null;
|
|
36
|
+
const diffParams = makeDiffParams(start, end);
|
|
21
37
|
const url = `https://statsapi.mlb.com/api/v1.1/game/${id}/feed/live${diffParams}`;
|
|
38
|
+
logger.info(`GET ${url}`);
|
|
39
|
+
const response = await axios.get(url);
|
|
40
|
+
return response.data;
|
|
41
|
+
});
|
|
42
|
+
export const setReplayGame = createAsyncThunk('games/setReplay', async id => {
|
|
43
|
+
const url = `https://statsapi.mlb.com/api/v1.1/game/${id}/feed/live?fields=gamePk,gameData,datetime,gameInfo,dateTime,firstPitch`;
|
|
44
|
+
logger.info(`GET ${url}`);
|
|
22
45
|
const response = await axios.get(url);
|
|
23
46
|
return response.data;
|
|
24
47
|
});
|
|
@@ -26,11 +49,28 @@ export const gamesSlice = createSlice({
|
|
|
26
49
|
name: 'games',
|
|
27
50
|
initialState,
|
|
28
51
|
reducers: {
|
|
29
|
-
|
|
52
|
+
setLiveGame(state, action) {
|
|
30
53
|
state.selectedId = action.payload;
|
|
54
|
+
state.delay = get('live-delay') || 0;
|
|
31
55
|
}
|
|
32
56
|
},
|
|
33
57
|
extraReducers: builder => {
|
|
58
|
+
builder.addCase(setReplayGame.pending, state => {
|
|
59
|
+
state.loading = true;
|
|
60
|
+
});
|
|
61
|
+
builder.addCase(setReplayGame.fulfilled, (state, action) => {
|
|
62
|
+
state.loading = false;
|
|
63
|
+
state.error = null;
|
|
64
|
+
state.selectedId = action.payload.gamePk;
|
|
65
|
+
state.fullUpdateRequired = true;
|
|
66
|
+
const start = action.payload.gameData.gameInfo.firstPitch || action.payload.gameData.datetime.dateTime;
|
|
67
|
+
state.delay = differenceInSeconds(new Date(), start);
|
|
68
|
+
});
|
|
69
|
+
builder.addCase(setReplayGame.rejected, (state, action) => {
|
|
70
|
+
state.loading = false;
|
|
71
|
+
state.fullUpdateRequired = true;
|
|
72
|
+
state.error = action.error;
|
|
73
|
+
});
|
|
34
74
|
builder.addCase(fetchGame.pending, state => {
|
|
35
75
|
state.loading = true;
|
|
36
76
|
});
|
|
@@ -70,13 +110,14 @@ export const gamesSlice = createSlice({
|
|
|
70
110
|
}
|
|
71
111
|
});
|
|
72
112
|
export const {
|
|
73
|
-
|
|
113
|
+
setLiveGame
|
|
74
114
|
} = gamesSlice.actions;
|
|
75
115
|
const gamesRoot = state => state.games;
|
|
76
116
|
export const selectLoading = createSelector(gamesRoot, root => root.loading);
|
|
77
117
|
export const selectError = createSelector(gamesRoot, root => root.error);
|
|
78
118
|
export const selectFullUpdateRequired = createSelector(gamesRoot, root => root.fullUpdateRequired);
|
|
79
119
|
export const selectSelectedId = createSelector(gamesRoot, root => root.selectedId);
|
|
120
|
+
export const selectDelay = createSelector(gamesRoot, root => root.delay);
|
|
80
121
|
export const selectGame = createSelector([gamesRoot, selectSelectedId], (root, id) => root.games[id]);
|
|
81
122
|
const selectLiveData = createSelector(selectGame, game => game.liveData);
|
|
82
123
|
const selectPlays = createSelector(selectLiveData, data => data.plays);
|
|
@@ -90,6 +131,7 @@ export const selectLineScore = createSelector(selectLiveData, data => data.lines
|
|
|
90
131
|
export const selectDecisions = createSelector(selectLiveData, data => data.decisions);
|
|
91
132
|
const selectGameData = createSelector(selectGame, game => game.gameData);
|
|
92
133
|
export const selectGameStatus = createSelector(selectGameData, game => game.status);
|
|
134
|
+
export const selectPlayers = createSelector(selectGameData, gameData => gameData.players);
|
|
93
135
|
export const selectTeams = createSelector(selectGameData, gameData => gameData.teams);
|
|
94
136
|
export const selectVenue = createSelector(selectGameData, gameData => gameData.venue);
|
|
95
137
|
export const selectStartTime = createSelector(selectGameData, gameData => {
|
|
@@ -5,21 +5,38 @@ const {
|
|
|
5
5
|
createSlice,
|
|
6
6
|
createSelector
|
|
7
7
|
} = reduxjsToolkit;
|
|
8
|
-
import { format } from 'date-fns';
|
|
8
|
+
import { add, format } from 'date-fns';
|
|
9
|
+
import { getSportId } from "../utils.js";
|
|
9
10
|
const initialState = {
|
|
11
|
+
scheduleDate: new Date(),
|
|
10
12
|
loading: false,
|
|
11
13
|
error: null,
|
|
12
14
|
data: null
|
|
13
15
|
};
|
|
14
16
|
export const fetchSchedule = createAsyncThunk('schedule/fetch', async date => {
|
|
15
17
|
const dateStr = format(date, 'MM/dd/yyyy');
|
|
16
|
-
const
|
|
18
|
+
const sportId = getSportId();
|
|
19
|
+
const response = await axios.get(`http://statsapi.mlb.com/api/v1/schedule?sportId=${sportId}&hydrate=team,linescore&date=${dateStr}`);
|
|
17
20
|
return response.data;
|
|
18
21
|
});
|
|
19
22
|
export const scheduleSlice = createSlice({
|
|
20
23
|
name: 'schedule',
|
|
21
24
|
initialState,
|
|
22
|
-
reducers: {
|
|
25
|
+
reducers: {
|
|
26
|
+
nextDay(state) {
|
|
27
|
+
state.scheduleDate = add(state.scheduleDate, {
|
|
28
|
+
days: 1
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
prevDay(state) {
|
|
32
|
+
state.scheduleDate = add(state.scheduleDate, {
|
|
33
|
+
days: -1
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
setDate(state, action) {
|
|
37
|
+
state.scheduleDate = action.payload;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
23
40
|
extraReducers: builder => {
|
|
24
41
|
builder.addCase(fetchSchedule.pending, state => {
|
|
25
42
|
state.loading = true;
|
|
@@ -40,4 +57,10 @@ const scheduleSelector = state => state.schedule;
|
|
|
40
57
|
export const selectLoading = createSelector(scheduleSelector, schedule => schedule.loading);
|
|
41
58
|
export const selectError = createSelector(scheduleSelector, schedule => schedule.error);
|
|
42
59
|
export const selectData = createSelector(scheduleSelector, schedule => schedule.data);
|
|
60
|
+
export const selectScheduleDate = createSelector(scheduleSelector, schedule => schedule.scheduleDate);
|
|
61
|
+
export const {
|
|
62
|
+
nextDay,
|
|
63
|
+
prevDay,
|
|
64
|
+
setDate
|
|
65
|
+
} = scheduleSlice.actions;
|
|
43
66
|
export default scheduleSlice.reducer;
|
|
@@ -5,6 +5,7 @@ const {
|
|
|
5
5
|
createSlice,
|
|
6
6
|
createSelector
|
|
7
7
|
} = reduxjsToolkit;
|
|
8
|
+
import { getSport } from "../utils.js";
|
|
8
9
|
const initialState = {
|
|
9
10
|
loading: false,
|
|
10
11
|
error: null,
|
|
@@ -12,9 +13,18 @@ const initialState = {
|
|
|
12
13
|
};
|
|
13
14
|
const SEASON = new Date().getFullYear();
|
|
14
15
|
export const fetchStandings = createAsyncThunk('standings/fetch', async () => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const sport = getSport();
|
|
17
|
+
if (sport === 'wbc') {
|
|
18
|
+
// WBC standings - try sportId-based endpoint
|
|
19
|
+
// If no data, return empty structure (component will handle gracefully)
|
|
20
|
+
const response = await axios.get(`https://statsapi.mlb.com/api/v1/standings?sportId=51&season=${SEASON}`);
|
|
21
|
+
return response.data;
|
|
22
|
+
} else {
|
|
23
|
+
// MLB standings (existing logic)
|
|
24
|
+
const url = `https://statsapi.mlb.com/api/v1/standings?leagueId=103,104&season=${SEASON}&standingsTypes=regularSeason&hydrate=division,team`;
|
|
25
|
+
const response = await axios.get(url);
|
|
26
|
+
return response.data;
|
|
27
|
+
}
|
|
18
28
|
});
|
|
19
29
|
export const standingsSlice = createSlice({
|
|
20
30
|
name: 'standings',
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import screen from "./screen.js";
|
|
|
5
5
|
import store from "./store/index.js";
|
|
6
6
|
import log from "./logger.js";
|
|
7
7
|
import App from "./components/App.js";
|
|
8
|
-
export default async function startInterface() {
|
|
8
|
+
export default async function startInterface(options) {
|
|
9
9
|
raf.polyfill();
|
|
10
10
|
process.on('uncaughtException', function (error) {
|
|
11
11
|
log.error('UNCAUGHT EXCEPTION\n' + JSON.stringify(error) + '\n' + error.stack);
|
|
@@ -16,5 +16,8 @@ export default async function startInterface() {
|
|
|
16
16
|
const reactBlessed = await import('react-blessed');
|
|
17
17
|
reactBlessed.render( /*#__PURE__*/React.createElement(Provider, {
|
|
18
18
|
store: store
|
|
19
|
-
}, /*#__PURE__*/React.createElement(App,
|
|
19
|
+
}, /*#__PURE__*/React.createElement(App, {
|
|
20
|
+
replayId: options.replay,
|
|
21
|
+
defaultDate: options.date
|
|
22
|
+
})), screen());
|
|
20
23
|
}
|
package/dist/utils.js
CHANGED
|
@@ -6,4 +6,26 @@ export function teamFavoriteStar(team) {
|
|
|
6
6
|
return `{${style}}★{/${style}} `;
|
|
7
7
|
}
|
|
8
8
|
return '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get the sport ID for API calls
|
|
13
|
+
* @returns {string} '51' for WBC, '1' for MLB
|
|
14
|
+
*/
|
|
15
|
+
export function getSportId() {
|
|
16
|
+
var _process$env$PLAYBALL;
|
|
17
|
+
// ENV override takes precedence
|
|
18
|
+
const envSport = (_process$env$PLAYBALL = process.env.PLAYBALL_SPORT) === null || _process$env$PLAYBALL === void 0 ? void 0 : _process$env$PLAYBALL.toLowerCase();
|
|
19
|
+
const sport = envSport || get('sport') || 'mlb';
|
|
20
|
+
return sport === 'wbc' ? '51' : '1';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the current sport setting
|
|
25
|
+
* @returns {string} 'mlb' or 'wbc'
|
|
26
|
+
*/
|
|
27
|
+
export function getSport() {
|
|
28
|
+
var _process$env$PLAYBALL2;
|
|
29
|
+
const envSport = (_process$env$PLAYBALL2 = process.env.PLAYBALL_SPORT) === null || _process$env$PLAYBALL2 === void 0 ? void 0 : _process$env$PLAYBALL2.toLowerCase();
|
|
30
|
+
return envSport || get('sport') || 'mlb';
|
|
9
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "playball",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Watch MLB games from the comfort of your terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"MLB",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"lint": "eslint --ext .jsx,.js src",
|
|
17
17
|
"start": "babel src --out-dir dist --watch",
|
|
18
18
|
"prepublishOnly": "npm run build",
|
|
19
|
-
"react-devtools": "react-devtools"
|
|
19
|
+
"react-devtools": "react-devtools",
|
|
20
|
+
"test": "echo \"No tests yet\""
|
|
20
21
|
},
|
|
21
22
|
"bin": {
|
|
22
23
|
"playball": "./bin/playball.js"
|
|
@@ -37,12 +38,14 @@
|
|
|
37
38
|
"preferGlobal": true,
|
|
38
39
|
"license": "MIT",
|
|
39
40
|
"dependencies": {
|
|
41
|
+
"@date-fns/utc": "^2.1.1",
|
|
40
42
|
"@reduxjs/toolkit": "^1.8.0",
|
|
41
43
|
"axios": "^0.26.1",
|
|
42
44
|
"blessed": "^0.1.81",
|
|
43
45
|
"commander": "^11.0.0",
|
|
44
46
|
"conf": "^11.0.1",
|
|
45
|
-
"date-fns": "^
|
|
47
|
+
"date-fns": "^4.1.0",
|
|
48
|
+
"figlet": "^1.10.0",
|
|
46
49
|
"json-patch": "^0.7.0",
|
|
47
50
|
"prop-types": "^15.8.1",
|
|
48
51
|
"raf": "^3.4.1",
|