playball 2.2.0 → 3.0.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/.eslintrc.json +2 -2
- package/README.md +18 -4
- package/demo.cast +95 -0
- package/demo.gif +0 -0
- package/dist/components/AllPlays.js +94 -93
- package/dist/components/App.js +45 -117
- package/dist/components/AtBat.js +44 -43
- package/dist/components/Bases.js +17 -26
- package/dist/components/Count.js +15 -26
- package/dist/components/FinishedGame.js +51 -66
- package/dist/components/Game.js +51 -114
- package/dist/components/GameList.js +161 -134
- package/dist/components/Grid.js +115 -0
- package/dist/components/HelpBar.js +13 -6
- package/dist/components/InningDisplay.js +26 -0
- package/dist/components/LineScore.js +32 -90
- package/dist/components/LiveGame.js +24 -19
- package/dist/components/LoadingSpinner.js +34 -125
- package/dist/components/Matchup.js +38 -59
- package/dist/components/PreviewGame.js +34 -52
- package/dist/components/Standings.js +91 -0
- package/dist/features/games.js +135 -0
- package/dist/features/keys.js +63 -0
- package/dist/features/schedule.js +58 -0
- package/dist/features/standings.js +57 -0
- package/dist/hooks/useKey.js +23 -0
- package/dist/logger.js +8 -10
- package/dist/main.js +13 -23
- package/dist/screen.js +22 -0
- package/dist/store/index.js +21 -7
- package/dist/style/index.js +3 -3
- package/package.json +44 -26
- package/src/components/AllPlays.jsx +95 -63
- package/src/components/App.jsx +38 -66
- package/src/components/AtBat.jsx +34 -36
- package/src/components/Bases.jsx +8 -13
- package/src/components/Count.jsx +10 -15
- package/src/components/FinishedGame.jsx +29 -40
- package/src/components/Game.jsx +48 -65
- package/src/components/GameList.jsx +128 -82
- package/src/components/Grid.jsx +91 -0
- package/src/components/HelpBar.jsx +14 -9
- package/src/components/InningDisplay.jsx +19 -0
- package/src/components/LineScore.jsx +27 -33
- package/src/components/LiveGame.jsx +7 -3
- package/src/components/LoadingSpinner.jsx +26 -60
- package/src/components/Matchup.jsx +26 -39
- package/src/components/PreviewGame.jsx +22 -33
- package/src/components/Standings.jsx +78 -0
- package/src/features/games.js +165 -0
- package/src/features/keys.js +38 -0
- package/src/features/schedule.js +59 -0
- package/src/features/standings.js +60 -0
- package/src/hooks/useKey.js +13 -0
- package/src/main.js +7 -14
- package/src/screen.js +14 -0
- package/src/store/index.js +16 -7
- package/src/style/index.js +1 -1
- package/dist/actions/game.js +0 -36
- package/dist/actions/schedule.js +0 -31
- package/dist/actions/types.js +0 -16
- package/dist/reducers/game.js +0 -70
- package/dist/reducers/index.js +0 -21
- package/dist/reducers/schedule.js +0 -35
- package/dist/selectors/game.js +0 -82
- package/dist/selectors/schedule.js +0 -25
- package/src/actions/game.js +0 -25
- package/src/actions/schedule.js +0 -19
- package/src/actions/types.js +0 -5
- package/src/reducers/game.js +0 -56
- package/src/reducers/index.js +0 -9
- package/src/reducers/schedule.js +0 -28
- package/src/selectors/game.js +0 -93
- package/src/selectors/schedule.js +0 -18
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
import {
|
|
4
|
-
import { selectLineScore, selectTeams } from '../
|
|
3
|
+
import { useSelector } from 'react-redux';
|
|
4
|
+
import { selectLineScore, selectTeams } from '../features/games';
|
|
5
5
|
|
|
6
6
|
const getRuns = (inning, homeAway, isFinal) => {
|
|
7
|
-
const runs = inning
|
|
7
|
+
const runs = inning[homeAway].runs;
|
|
8
8
|
if (runs !== undefined) {
|
|
9
9
|
return runs;
|
|
10
10
|
}
|
|
@@ -12,47 +12,41 @@ const getRuns = (inning, homeAway, isFinal) => {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
const getTeamLine = (linescore, totalInnings, homeAway, final) => (
|
|
15
|
-
linescore.
|
|
15
|
+
linescore.innings
|
|
16
16
|
.map(inning => getRuns(inning, homeAway, final))
|
|
17
17
|
.map(r => r.toString().padStart(2))
|
|
18
18
|
.join(' ')
|
|
19
19
|
.padEnd(totalInnings * 3) +
|
|
20
20
|
'{bold}' +
|
|
21
|
-
linescore.
|
|
22
|
-
linescore.
|
|
23
|
-
linescore.
|
|
21
|
+
linescore.teams[homeAway].runs.toString().padStart(3) + '{/bold}' +
|
|
22
|
+
linescore.teams[homeAway].hits.toString().padStart(3) +
|
|
23
|
+
linescore.teams[homeAway].errors.toString().padStart(3)
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const totalInnings = Math.max(currentInning, 9);
|
|
34
|
-
const home = teams.getIn(['home', 'abbreviation']);
|
|
35
|
-
const away = teams.getIn(['away', 'abbreviation']);
|
|
36
|
-
const teamNameLength = 3;
|
|
37
|
-
let str = ''.padEnd(teamNameLength) + Array.from(Array(totalInnings).keys()).map(i => (i + 1).toString().padStart(2)).join(' ') + ' {bold}R{/bold} H E\n' +
|
|
38
|
-
away.padEnd(teamNameLength) + getTeamLine(linescore, totalInnings, 'away', final) + '\n' +
|
|
39
|
-
home.padEnd(teamNameLength) + getTeamLine(linescore, totalInnings, 'home', final);
|
|
40
|
-
return (
|
|
41
|
-
<box align={align} content={str} tags wrap={false} />
|
|
42
|
-
);
|
|
26
|
+
function LineScore({ align, final }) {
|
|
27
|
+
const linescore = useSelector(selectLineScore);
|
|
28
|
+
const teams = useSelector(selectTeams);
|
|
29
|
+
|
|
30
|
+
const currentInning = linescore.currentInning;
|
|
31
|
+
if (!currentInning) {
|
|
32
|
+
return '';
|
|
43
33
|
}
|
|
34
|
+
|
|
35
|
+
const totalInnings = Math.max(currentInning, 9);
|
|
36
|
+
const home = teams.home.abbreviation;
|
|
37
|
+
const away = teams.away.abbreviation;
|
|
38
|
+
const teamNameLength = 3;
|
|
39
|
+
let str = ''.padEnd(teamNameLength) + Array.from(Array(totalInnings).keys()).map(i => (i + 1).toString().padStart(2)).join(' ') + ' {bold}R{/bold} H E\n' +
|
|
40
|
+
away.padEnd(teamNameLength) + getTeamLine(linescore, totalInnings, 'away', final) + '\n' +
|
|
41
|
+
home.padEnd(teamNameLength) + getTeamLine(linescore, totalInnings, 'home', final);
|
|
42
|
+
return (
|
|
43
|
+
<box align={align} content={str} tags wrap={false} />
|
|
44
|
+
);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
LineScore.propTypes = {
|
|
47
|
-
align: PropTypes.
|
|
48
|
+
align: PropTypes.oneOf(['left', 'center', 'right']),
|
|
48
49
|
final: PropTypes.bool,
|
|
49
|
-
linescore: PropTypes.object,
|
|
50
|
-
teams: PropTypes.object,
|
|
51
50
|
};
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
linescore: selectLineScore(state),
|
|
55
|
-
teams: selectTeams(state),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export default connect(mapStateToProps)(LineScore);
|
|
52
|
+
export default LineScore;
|
|
@@ -6,12 +6,16 @@ import LineScore from './LineScore';
|
|
|
6
6
|
import Matchup from './Matchup';
|
|
7
7
|
import AtBat from './AtBat';
|
|
8
8
|
import AllPlays from './AllPlays';
|
|
9
|
+
import InningDisplay from './InningDisplay';
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
function LiveGame() {
|
|
11
12
|
return (
|
|
12
13
|
<element>
|
|
13
14
|
<element top={0} left={1} width='100%-1' height={3}>
|
|
14
|
-
<element width=
|
|
15
|
+
<element left={0} width={2}>
|
|
16
|
+
<InningDisplay />
|
|
17
|
+
</element>
|
|
18
|
+
<element left={5} width='25%-5'>
|
|
15
19
|
<Count />
|
|
16
20
|
</element>
|
|
17
21
|
<element left='25%+1' width='25%'>
|
|
@@ -38,6 +42,6 @@ export const LiveGame = () => {
|
|
|
38
42
|
</element>
|
|
39
43
|
</element>
|
|
40
44
|
);
|
|
41
|
-
}
|
|
45
|
+
}
|
|
42
46
|
|
|
43
47
|
export default LiveGame;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { selectLoading as
|
|
5
|
-
import { selectLoading as selectScheduleLoading } from '../selectors/schedule';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import { selectLoading as selectScheduleLoading } from '../features/schedule';
|
|
4
|
+
import { selectLoading as gamesLoading } from '../features/games';
|
|
6
5
|
|
|
7
6
|
const frames = [
|
|
8
7
|
'⠋',
|
|
@@ -17,67 +16,34 @@ const frames = [
|
|
|
17
16
|
'⠏'
|
|
18
17
|
];
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
};
|
|
27
|
-
}
|
|
19
|
+
function LoadingSpinner() {
|
|
20
|
+
const [frame, setFrame] = useState(0);
|
|
21
|
+
const [animating, setAnimating] = useState(false);
|
|
22
|
+
const timerRef = useRef(null);
|
|
23
|
+
const scheduleLoading = useSelector(selectScheduleLoading);
|
|
24
|
+
const gameLoading = useSelector(gamesLoading);
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
26
|
+
const increment = () => {
|
|
27
|
+
setFrame(prevFrame => (prevFrame + 1) % frames.length);
|
|
28
|
+
};
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
const { gameLoading, scheduleLoading } = this.props;
|
|
35
|
-
const { frame } = this.state;
|
|
36
|
-
if (gameLoading !== prevProps.gameLoading ||
|
|
37
|
-
scheduleLoading !== prevProps.scheduleLoading ||
|
|
38
|
-
frame !== prevState.frame) {
|
|
39
|
-
this.doUpdate();
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
increment() {
|
|
44
|
-
this.setState(state => ({
|
|
45
|
-
frame: (state.frame + 1) % frames.length
|
|
46
|
-
}));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
doUpdate() {
|
|
50
|
-
const { gameLoading, scheduleLoading } = this.props;
|
|
51
|
-
const { animating, frame } = this.state;
|
|
30
|
+
const doUpdate = () => {
|
|
52
31
|
if (!animating && (gameLoading || scheduleLoading)) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.increment();
|
|
57
|
-
this.timer = setInterval(() => this.increment(), 50);
|
|
32
|
+
setAnimating(true);
|
|
33
|
+
increment();
|
|
34
|
+
timerRef.current = setInterval(increment, 50);
|
|
58
35
|
}
|
|
59
36
|
if (!gameLoading && !scheduleLoading && frame === 0) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
clearInterval(this.timer);
|
|
37
|
+
setAnimating(false);
|
|
38
|
+
clearInterval(timerRef.current);
|
|
64
39
|
}
|
|
65
|
-
}
|
|
40
|
+
};
|
|
66
41
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
LoadingSpinner.propTypes = {
|
|
74
|
-
gameLoading: PropTypes.bool,
|
|
75
|
-
scheduleLoading: PropTypes.bool
|
|
76
|
-
};
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
doUpdate();
|
|
44
|
+
}, [gameLoading, scheduleLoading, frame]);
|
|
77
45
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
scheduleLoading: selectScheduleLoading(state),
|
|
81
|
-
});
|
|
46
|
+
return <box content={animating ? frames[frame] : ' '} />;
|
|
47
|
+
}
|
|
82
48
|
|
|
83
|
-
export default
|
|
49
|
+
export default LoadingSpinner;
|
|
@@ -1,54 +1,41 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { selectCurrentPlay, selectBoxscore, selectTeams } from '../selectors/game';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import { selectCurrentPlay, selectBoxscore, selectTeams } from '../features/games';
|
|
5
4
|
|
|
6
5
|
const getPlayerStats = (boxscore, teams, id) => {
|
|
7
6
|
const key = 'ID' + id;
|
|
8
|
-
const homePlayers = boxscore.
|
|
9
|
-
if (homePlayers
|
|
7
|
+
const homePlayers = boxscore.home.players;
|
|
8
|
+
if (homePlayers[key]) {
|
|
10
9
|
return {
|
|
11
|
-
team: teams.
|
|
12
|
-
player: homePlayers
|
|
10
|
+
team: teams.home,
|
|
11
|
+
player: homePlayers[key],
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
14
|
return {
|
|
16
|
-
team: teams.
|
|
17
|
-
player: boxscore.
|
|
15
|
+
team: teams.away,
|
|
16
|
+
player: boxscore.away.players[key],
|
|
18
17
|
};
|
|
19
18
|
};
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
function Matchup() {
|
|
21
|
+
const boxscore = useSelector(selectBoxscore);
|
|
22
|
+
const currentPlay = useSelector(selectCurrentPlay);
|
|
23
|
+
const teams = useSelector(selectTeams);
|
|
24
|
+
|
|
25
|
+
const pitcherId = currentPlay.matchup?.pitcher?.id;
|
|
26
|
+
const batterId = currentPlay.matchup?.batter?.id;
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const {team: pitchTeam, player: pitcher} = getPlayerStats(boxscore, teams, pitcherId);
|
|
29
|
+
const {team: batTeam, player: batter} = getPlayerStats(boxscore, teams, batterId);
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const display = `${pitchTeam.abbreviation} Pitching: ` +
|
|
32
|
+
`{bold}${pitcher.person.fullName}{/bold} ${pitcher.stats.pitching.inningsPitched} IP, ${pitcher.stats.pitching.pitchesThrown || 0} P, ${pitcher.seasonStats.pitching.era} ERA\n` +
|
|
33
|
+
`${batTeam.abbreviation} At Bat: ` +
|
|
34
|
+
`{bold}${batter.person.fullName}{/bold} ${batter.stats.batting.hits}-${batter.stats.batting.atBats}, ${batter.seasonStats.batting.avg} AVG, ${batter.seasonStats.batting.homeRuns} HR`;
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
Matchup.propTypes = {
|
|
43
|
-
boxscore: PropTypes.object,
|
|
44
|
-
currentPlay: PropTypes.object,
|
|
45
|
-
teams: PropTypes.object,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const mapStateToProps = state => ({
|
|
49
|
-
boxscore: selectBoxscore(state),
|
|
50
|
-
currentPlay: selectCurrentPlay(state),
|
|
51
|
-
teams: selectTeams(state),
|
|
52
|
-
});
|
|
36
|
+
return (
|
|
37
|
+
<box tags content={display} wrap={false} />
|
|
38
|
+
);
|
|
39
|
+
}
|
|
53
40
|
|
|
54
|
-
export default
|
|
41
|
+
export default Matchup;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import { selectTeams, selectVenue, selectStartTime, selectBoxscore, selectProbablePitchers } from '../selectors/game';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import { format } from 'date-fns';
|
|
4
|
+
import { selectTeams, selectVenue, selectStartTime, selectBoxscore, selectProbablePitchers, selectGameStatus } from '../features/games';
|
|
6
5
|
|
|
7
6
|
const formatPitcherName = (pitcher) => {
|
|
8
|
-
let display = pitcher.
|
|
9
|
-
const number = pitcher.
|
|
7
|
+
let display = pitcher.person.fullName;
|
|
8
|
+
const number = pitcher.jerseyNumber;
|
|
10
9
|
if (number) {
|
|
11
10
|
display += `, #${number}`;
|
|
12
11
|
}
|
|
@@ -14,52 +13,42 @@ const formatPitcherName = (pitcher) => {
|
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
const formatTeam = (teams, probables, boxscore, homeAway) => {
|
|
17
|
-
const pitcherId = probables
|
|
18
|
-
const pitcher = boxscore
|
|
16
|
+
const pitcherId = probables[homeAway].id;
|
|
17
|
+
const pitcher = boxscore[homeAway].players['ID' + pitcherId];
|
|
19
18
|
let lines = [
|
|
20
|
-
teams
|
|
21
|
-
`(${teams
|
|
19
|
+
teams[homeAway].teamName,
|
|
20
|
+
`(${teams[homeAway].record.wins}-${teams[homeAway].record.losses})`,
|
|
22
21
|
];
|
|
23
22
|
if (pitcher) {
|
|
24
23
|
lines = lines.concat([
|
|
25
24
|
'',
|
|
26
25
|
formatPitcherName(pitcher),
|
|
27
|
-
`${pitcher.
|
|
28
|
-
`${pitcher.
|
|
26
|
+
`${pitcher.seasonStats?.pitching?.wins}-${pitcher.seasonStats?.pitching?.losses}`,
|
|
27
|
+
`${pitcher.seasonStats?.pitching?.era} ERA ${pitcher.seasonStats?.pitching?.strikeOuts} K`,
|
|
29
28
|
]);
|
|
30
29
|
}
|
|
31
30
|
return lines;
|
|
32
31
|
};
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
function PreviewGame() {
|
|
34
|
+
const boxscore = useSelector(selectBoxscore);
|
|
35
|
+
const probables = useSelector(selectProbablePitchers);
|
|
36
|
+
const startTime = useSelector(selectStartTime);
|
|
37
|
+
const status = useSelector(selectGameStatus);
|
|
38
|
+
const teams = useSelector(selectTeams);
|
|
39
|
+
const venue = useSelector(selectVenue);
|
|
35
40
|
const away = formatTeam(teams, probables, boxscore, 'away');
|
|
36
41
|
const home = formatTeam(teams, probables, boxscore, 'home');
|
|
37
|
-
const formattedStart =
|
|
42
|
+
const formattedStart = status.startTimeTBD ? 'Start time TBD' : format(new Date(startTime), 'MMMM d, yyy p');
|
|
38
43
|
return (
|
|
39
44
|
<element>
|
|
40
45
|
<element height='60%'>
|
|
41
46
|
<box content={away.join('\n')} width='33%-1' top='50%' align='center' />
|
|
42
|
-
<box content={`\nvs.\n\n${formattedStart}\n${venue.
|
|
47
|
+
<box content={`\nvs.\n\n${formattedStart}\n${venue.name}\n${venue.location.city}, ${venue.location.stateAbbrev}`} width='33%-1' left='33%' top='50%' align='center' />
|
|
43
48
|
<box content={home.join('\n')} width='34%' top='50%' left='66%' align='center' />
|
|
44
49
|
</element>
|
|
45
50
|
</element>
|
|
46
51
|
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
PreviewGame.propTypes = {
|
|
50
|
-
boxscore: PropTypes.object,
|
|
51
|
-
probables: PropTypes.object,
|
|
52
|
-
startTime: PropTypes.string,
|
|
53
|
-
teams: PropTypes.object,
|
|
54
|
-
venue: PropTypes.object,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const mapStateToProps = state => ({
|
|
58
|
-
boxscore: selectBoxscore(state),
|
|
59
|
-
probables: selectProbablePitchers(state),
|
|
60
|
-
startTime: selectStartTime(state),
|
|
61
|
-
teams: selectTeams(state),
|
|
62
|
-
venue: selectVenue(state),
|
|
63
|
-
});
|
|
52
|
+
}
|
|
64
53
|
|
|
65
|
-
export default
|
|
54
|
+
export default PreviewGame;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useDispatch, useSelector } from 'react-redux';
|
|
4
|
+
import { fetchStandings, selectData } from '../features/standings';
|
|
5
|
+
|
|
6
|
+
function formatHeaderRow(record) {
|
|
7
|
+
return record.division.nameShort.padEnd(15) +
|
|
8
|
+
' W' +
|
|
9
|
+
' L' +
|
|
10
|
+
' PCT' +
|
|
11
|
+
' GB' +
|
|
12
|
+
' WCGB' +
|
|
13
|
+
' L10' +
|
|
14
|
+
' STRK';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatTeamRow(record) {
|
|
18
|
+
const lastTen = record.records.splitRecords.find(o => o.type === 'lastTen');
|
|
19
|
+
return record.team.teamName.padEnd(15) +
|
|
20
|
+
record.wins.toString().padStart(5) +
|
|
21
|
+
record.losses.toString().padStart(5) +
|
|
22
|
+
record.winningPercentage.padStart(7) +
|
|
23
|
+
record.gamesBack.padStart(6) +
|
|
24
|
+
record.wildCardGamesBack.padStart(6) +
|
|
25
|
+
`${lastTen.wins}-${lastTen.losses}`.padStart(6) +
|
|
26
|
+
record.streak.streakCode.padStart(5);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Division({record, top, left, width}) {
|
|
30
|
+
return (
|
|
31
|
+
<box top={top} left={left} height={6} width={width}>
|
|
32
|
+
<box top={0} left={0} height={1} fg='black' bg='white' content={formatHeaderRow(record)} wrap={false} />
|
|
33
|
+
<box top={1} left={0} height={5} content={record.teamRecords.map(formatTeamRow).join('\n')} wrap={false} />
|
|
34
|
+
</box>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
Division.propTypes = {
|
|
38
|
+
record: PropTypes.object,
|
|
39
|
+
top: PropTypes.number,
|
|
40
|
+
left: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
41
|
+
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function Standings() {
|
|
45
|
+
const dispatch = useDispatch();
|
|
46
|
+
const standings = useSelector(selectData);
|
|
47
|
+
|
|
48
|
+
useEffect(() => dispatch(fetchStandings()), []);
|
|
49
|
+
|
|
50
|
+
if (!standings) {
|
|
51
|
+
return <element />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<element>
|
|
56
|
+
{standings.records.filter(record => record.league.id === 103).map((record, idx) => (
|
|
57
|
+
<Division
|
|
58
|
+
top={idx * 7}
|
|
59
|
+
left={0}
|
|
60
|
+
width='50%-1'
|
|
61
|
+
key={record.division.id}
|
|
62
|
+
record={record}
|
|
63
|
+
/>
|
|
64
|
+
))}
|
|
65
|
+
{standings.records.filter(record => record.league.id === 104).map((record, idx) => (
|
|
66
|
+
<Division
|
|
67
|
+
top={idx * 7}
|
|
68
|
+
left='50%+1'
|
|
69
|
+
width='50%-1'
|
|
70
|
+
key={record.division.id}
|
|
71
|
+
record={record}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</element>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default Standings;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { createAsyncThunk, createSlice, createSelector } from '@reduxjs/toolkit';
|
|
3
|
+
import jsonpatch from 'json-patch';
|
|
4
|
+
|
|
5
|
+
const initialState = {
|
|
6
|
+
loading: false,
|
|
7
|
+
fullUpdateRequired: false,
|
|
8
|
+
error: null,
|
|
9
|
+
selectedId: null,
|
|
10
|
+
games: {},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const fetchGame = createAsyncThunk(
|
|
14
|
+
'games/fetch',
|
|
15
|
+
async ({id, start}) => {
|
|
16
|
+
const diffParams = start ? `/diffPatch?startTimecode=${start}` : '';
|
|
17
|
+
const url = `https://statsapi.mlb.com/api/v1.1/game/${id}/feed/live${diffParams}`;
|
|
18
|
+
const response = await axios.get(url);
|
|
19
|
+
return response.data;
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
export const gamesSlice = createSlice({
|
|
24
|
+
name: 'games',
|
|
25
|
+
initialState,
|
|
26
|
+
reducers: {
|
|
27
|
+
setSelectedId(state, action) {
|
|
28
|
+
state.selectedId = action.payload;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
extraReducers: (builder) => {
|
|
32
|
+
builder.addCase(fetchGame.pending, (state) => {
|
|
33
|
+
state.loading = true;
|
|
34
|
+
});
|
|
35
|
+
builder.addCase(fetchGame.fulfilled, (state, action) => {
|
|
36
|
+
const id = state.selectedId;
|
|
37
|
+
let game = state.games[id];
|
|
38
|
+
let patchError = false;
|
|
39
|
+
if (Array.isArray(action.payload)) {
|
|
40
|
+
action.payload.forEach(obj => {
|
|
41
|
+
if (patchError) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
jsonpatch.apply(game || {}, obj.diff);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
patchError = true;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
game = action.payload;
|
|
53
|
+
}
|
|
54
|
+
if (patchError) {
|
|
55
|
+
state.fullUpdateRequired = true;
|
|
56
|
+
} else {
|
|
57
|
+
state.fullUpdateRequired = false;
|
|
58
|
+
state.error = null;
|
|
59
|
+
state.games[id] = game;
|
|
60
|
+
}
|
|
61
|
+
state.loading = false;
|
|
62
|
+
});
|
|
63
|
+
builder.addCase(fetchGame.rejected, (state, action) => {
|
|
64
|
+
state.fullUpdateRequired = true;
|
|
65
|
+
state.loading = false;
|
|
66
|
+
state.error = action.error;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const { setSelectedId } = gamesSlice.actions;
|
|
72
|
+
|
|
73
|
+
const gamesRoot = state => state.games;
|
|
74
|
+
|
|
75
|
+
export const selectLoading = createSelector(
|
|
76
|
+
gamesRoot,
|
|
77
|
+
root => root.loading
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
export const selectError = createSelector(
|
|
81
|
+
gamesRoot,
|
|
82
|
+
root => root.error
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
export const selectFullUpdateRequired = createSelector(
|
|
86
|
+
gamesRoot,
|
|
87
|
+
root => root.fullUpdateRequired
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const selectSelectedId = createSelector(
|
|
91
|
+
gamesRoot,
|
|
92
|
+
root => root.selectedId
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
export const selectGame = createSelector(
|
|
96
|
+
[gamesRoot, selectSelectedId],
|
|
97
|
+
(root, id) => root.games[id]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const selectLiveData = createSelector(
|
|
101
|
+
selectGame,
|
|
102
|
+
game => game.liveData
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const selectPlays = createSelector(
|
|
106
|
+
selectLiveData,
|
|
107
|
+
data => data.plays
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
export const selectCurrentPlay = createSelector(
|
|
111
|
+
selectPlays,
|
|
112
|
+
plays => plays.currentPlay
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
export const selectAllPlays = createSelector(
|
|
116
|
+
selectPlays,
|
|
117
|
+
plays => plays.allPlays
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
export const selectBoxscore = createSelector(
|
|
121
|
+
selectLiveData,
|
|
122
|
+
data => data.boxscore?.teams
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export const selectLineScore = createSelector(
|
|
126
|
+
selectLiveData,
|
|
127
|
+
data => data.linescore
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
export const selectDecisions = createSelector(
|
|
131
|
+
selectLiveData,
|
|
132
|
+
data => data.decisions
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const selectGameData = createSelector(
|
|
136
|
+
selectGame,
|
|
137
|
+
game => game.gameData
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
export const selectGameStatus = createSelector(
|
|
141
|
+
selectGameData,
|
|
142
|
+
game => game.status
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export const selectTeams = createSelector(
|
|
146
|
+
selectGameData,
|
|
147
|
+
gameData => gameData.teams
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
export const selectVenue = createSelector(
|
|
151
|
+
selectGameData,
|
|
152
|
+
gameData => gameData.venue
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
export const selectStartTime = createSelector(
|
|
156
|
+
selectGameData,
|
|
157
|
+
gameData => gameData.datetime?.dateTime
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
export const selectProbablePitchers = createSelector(
|
|
161
|
+
selectGameData,
|
|
162
|
+
gameData => gameData.probablePitchers
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
export default gamesSlice.reducer;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
+
import screen from '../screen';
|
|
3
|
+
|
|
4
|
+
export const keysSlice = createSlice({
|
|
5
|
+
name: 'keys',
|
|
6
|
+
initialState: [{ key: 'Q', label: 'Quit' }],
|
|
7
|
+
reducers: {
|
|
8
|
+
addKeyListener: {
|
|
9
|
+
reducer: (state, action) => {
|
|
10
|
+
if (action.payload) {
|
|
11
|
+
state.push(action.payload);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
prepare: (key, listener, help) => {
|
|
15
|
+
screen.key(key, listener);
|
|
16
|
+
return { payload: help };
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
removeKeyListener: {
|
|
20
|
+
reducer: (state, action) => {
|
|
21
|
+
if (action.payload) {
|
|
22
|
+
const idx = state.findIndex(item => item.key === action.payload.key);
|
|
23
|
+
if (idx >= 0) {
|
|
24
|
+
state.splice(idx, 1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
prepare: (key, listener, help) => {
|
|
29
|
+
screen.unkey(key, listener);
|
|
30
|
+
return { payload: help };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const { addKeyListener, removeKeyListener } = keysSlice.actions;
|
|
37
|
+
|
|
38
|
+
export default keysSlice.reducer;
|