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 CHANGED
@@ -25,18 +25,27 @@ $ playball
25
25
  ```
26
26
 
27
27
  ### Docker
28
- ```
29
- $ docker build -t playball .
30
- $ docker run -it --rm --name playball playball:latest
31
- ```
32
-
33
- #### Build options
34
-
35
- Update the language encoding of by adding `--build-args`
36
-
37
- ```
38
- $ docker build --build-arg LANG=en_US.UTF-8 -t playball .
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>&darr;</kbd>/<kbd>j</kbd>, <kbd>&uarr;</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
- let inning = '';
29
- const lines = [];
30
- plays && plays.slice().reverse().forEach((play, playIdx, plays) => {
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
- if (playInning !== inning) {
37
- inning = playInning;
38
- if (lines.length > 0) {
39
- lines.push('');
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
- lines.push(`{bold}[${inning.toUpperCase()}]{/}`);
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
- lines.push(line);
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;
@@ -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 { setSelectedId } from "../features/games.js";
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(setSelectedId(null));
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(setSelectedId(game.gamePk));
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 { selectLineScore, selectTeams, selectDecisions, selectBoxscore, selectGameStatus } from "../features/games.js";
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
- return /*#__PURE__*/React.createElement("element", null, /*#__PURE__*/React.createElement("element", {
73
- height: "60%"
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
- width: "33%-1",
77
- top: "50%",
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
- content: formatScore(status, linescore),
81
- width: "33%-1",
82
- left: "33%",
83
- top: "50%",
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
- width: "34%",
88
- top: "50%",
89
- left: "66%",
90
- align: "center"
91
- })), /*#__PURE__*/React.createElement("element", {
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: "center",
126
+ align: "right",
96
127
  final: true
97
128
  })), /*#__PURE__*/React.createElement("element", {
98
- top: "60%+5",
99
- left: "50%-20"
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;
@@ -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, useState } from 'react';
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 { add, format } from 'date-fns';
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(() => setDate(prev => add(prev, {
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(() => setDate(prev => add(prev, {
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, null))));
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 = format(new Date(), 'yyyy-DDD');
46
- const gameDay = format(startDate, 'yyyy-DDD');
45
+ const today = new Date();
47
46
  let start = format(startDate, 'p');
48
- if (today !== gameDay) {
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
  }
@@ -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 diffParams = start ? `/diffPatch?startTimecode=${start}` : '';
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
- setSelectedId(state, action) {
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
- setSelectedId
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 response = await axios.get(`http://statsapi.mlb.com/api/v1/schedule?sportId=1&hydrate=team,linescore&date=${dateStr}`);
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 url = `https://statsapi.mlb.com/api/v1/standings?leagueId=103,104&season=${SEASON}&standingsTypes=regularSeason&hydrate=division,team`;
16
- const response = await axios.get(url);
17
- return response.data;
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, null)), screen());
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.2.0",
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": "^2.28.0",
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",