Skip to content

Commit 1ae2485

Browse files
committed
xState and separate logic from Counter
1 parent b4f2364 commit 1ae2485

File tree

8 files changed

+96
-43
lines changed

8 files changed

+96
-43
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
"homepage": "https://workout-interval-timer.netlify.com",
66
"dependencies": {
77
"@testing-library/react": "^9.1.4",
8+
"@xstate/react": "^0.8.1",
89
"date-fns": "^2.1.0",
910
"react": "^16.9.0",
1011
"react-dom": "^16.9.0",
11-
"react-scripts": "3.1.1"
12+
"react-scripts": "3.1.1",
13+
"xstate": "^4.11.0"
1214
},
1315
"scripts": {
1416
"start": "react-scripts start",

src/App.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useWorkoutTimer } from './hooks/useWorkoutTimer';
55
import { Status } from './model/Status';
66
import { FormFields } from './components/FormFields/FormFields';
77
import { Counter } from './components/Counter/Counter';
8+
import { evaluateStatus } from './utils/evaluateStatus';
89

910
function App() {
1011
const {
@@ -22,7 +23,7 @@ function App() {
2223
return (
2324
<div className={styles.content}>
2425
<div className={styles.centerArea}>
25-
{status === Status.stopped ? (
26+
{status === Status.STOPPED ? (
2627
<FormFields
2728
rounds={rounds}
2829
handleRoundsChange={handleRoundsChange}
@@ -34,14 +35,14 @@ function App() {
3435
) : (
3536
<Counter
3637
timeLeft={timeLeft}
37-
status={status}
38+
text={evaluateStatus(status)}
3839
roundsLeft={roundsLeft}
3940
rounds={rounds}
4041
/>
4142
)}
4243
</div>
4344
<Button onClick={start} data-testid={'start-button'}>
44-
{status === Status.stopped ? 'Start' : 'Stop'}
45+
{status === Status.STOPPED ? 'Start' : 'Stop'}
4546
</Button>
4647
</div>
4748
);

src/components/Counter/Counter.js

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,9 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import styles from './Counter.module.css';
44
import { DurationInput } from '../DurationInput/DurationInput';
5-
import { Status } from '../../model/Status';
6-
const evaluateStatus = status => {
7-
if (status === Status.prework) {
8-
return 'PREP';
9-
}
10-
if (status === Status.break) {
11-
return 'REST';
12-
}
13-
return 'WORK';
14-
};
15-
export function Counter({ timeLeft, status, roundsLeft, rounds }) {
5+
import { StatusDisplay } from '../../model/StatusDisplay';
6+
7+
export function Counter({ text, timeLeft, roundsLeft, rounds }) {
168
return (
179
<div className={styles.container}>
1810
<div className={styles.round}>
@@ -32,16 +24,20 @@ export function Counter({ timeLeft, status, roundsLeft, rounds }) {
3224
<div className={styles.statusContainer}>
3325
<div className={`${styles.progress} ${styles.progressBottom}`}></div>
3426
<span className={`${styles.status}`} data-testid={'status'}>
35-
{evaluateStatus(status)}
27+
{text}
3628
</span>
3729
</div>
3830
</div>
3931
);
4032
}
4133
Counter.propTypes = {
34+
text: PropTypes.oneOf([
35+
StatusDisplay.BREAK,
36+
StatusDisplay.PREWORK,
37+
StatusDisplay.WORK,
38+
]),
4239
timeLeft: PropTypes.instanceOf(Date),
4340
dataTestId: PropTypes.string,
44-
status: PropTypes.oneOf([Status.work, Status.prework, Status.break]),
4541
roundsLeft: PropTypes.number,
4642
rounds: PropTypes.number,
4743
};

src/hooks/useWorkoutTimer.js

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,61 @@
11
import React from 'react';
2+
import { useMachine } from '@xstate/react';
23
import beepBreakFile from '../BeepBreak.mp3';
34
import beepBreakLongFile from '../BeepBreakLong.mp3';
45
import beepWorkFile from '../BeepWork.mp3';
56
import beepWorkLongFile from '../BeepWorkLong.mp3';
67
import { subSeconds, getSeconds, addSeconds, getMinutes } from 'date-fns';
78
import { useAudio } from './useAudio';
9+
import { Status } from '../model/Status';
810
import { hasOneSecondElapsed } from '../utils/hasOneSecondElapsed';
11+
import { Machine } from 'xstate';
912
const SECONDS_PER_MINUTE = 60;
1013
const PREP_TIME_SECONDS = 5;
11-
const Status = {
12-
stopped: 'stopped',
13-
prework: 'prework',
14-
work: 'work',
15-
break: 'break',
16-
};
1714

15+
const statusEvents = {
16+
START: 'START',
17+
WORK: 'WORK',
18+
BREAK: 'BREAK',
19+
STOP: 'STOP',
20+
};
21+
const statusMachine = Machine({
22+
id: 'status',
23+
initial: Status.STOPPED,
24+
states: {
25+
[Status.STOPPED]: {
26+
on: { [statusEvents.START]: Status.PREWORK },
27+
},
28+
[Status.PREWORK]: {
29+
on: { [statusEvents.WORK]: Status.WORK, STOP: Status.STOPPED },
30+
},
31+
[Status.WORK]: {
32+
on: {
33+
[statusEvents.STOP]: Status.STOPPED,
34+
[statusEvents.BREAK]: Status.BREAK,
35+
},
36+
},
37+
[Status.BREAK]: {
38+
on: {
39+
[statusEvents.WORK]: Status.WORK,
40+
[statusEvents.STOP]: Status.STOPPED,
41+
},
42+
},
43+
},
44+
});
1845
export function useWorkoutTimer() {
1946
const { audio: beepBreak } = useAudio(beepBreakFile);
2047
const { audio: beepWork } = useAudio(beepWorkFile);
2148
const { audio: beepBreakLong } = useAudio(beepBreakLongFile);
2249
const { audio: beepWorkLong } = useAudio(beepWorkLongFile);
23-
const [status, setStatus] = React.useState(Status.stopped);
50+
const [status, send] = useMachine(statusMachine);
2451
const [timeLeft, setTimeLeft] = React.useState(new Date(0));
2552
const [rounds, setRounds] = React.useState(1);
2653
const [roundsLeft, setRoundsLeft] = React.useState(rounds);
2754
const [workInterval, setWorkInterval] = React.useState(new Date(0));
2855
const [breakInterval, setBreakInterval] = React.useState(new Date(0));
2956
const timestamp = React.useRef(Date.now());
30-
let statusRef = React.useRef(status);
31-
statusRef.current = status;
57+
let statusRef = React.useRef(status.value);
58+
statusRef.current = status.value;
3259
let roundsLeftRef = React.useRef(roundsLeft);
3360
roundsLeftRef.current = roundsLeft;
3461
let timeLeftRef = React.useRef(timeLeft);
@@ -37,19 +64,20 @@ export function useWorkoutTimer() {
3764
setRounds(Number(value));
3865
};
3966
const start = React.useCallback(() => {
40-
const shouldStart = status === Status.stopped;
67+
const shouldStart = status.value === Status.STOPPED;
4168
if (shouldStart) {
4269
timestamp.current = Date.now();
43-
setStatus(Status.prework);
70+
send(statusEvents.START);
4471
setTimeLeft(addSeconds(new Date(0), PREP_TIME_SECONDS));
4572
setRoundsLeft(rounds);
4673
} else {
47-
setStatus(Status.stopped);
74+
send(statusEvents.STOP);
4875
}
49-
}, [rounds, status]);
76+
}, [rounds, status, send]);
5077
React.useEffect(() => {
5178
let interval = null;
52-
const startWasInitiated = status !== Status.stopped && interval === null;
79+
const startWasInitiated =
80+
status.value !== Status.STOPPED && interval === null;
5381
if (startWasInitiated) {
5482
interval = setInterval(function countDown() {
5583
if (hasOneSecondElapsed(timestamp.current)) {
@@ -62,7 +90,8 @@ export function useWorkoutTimer() {
6290
const shouldBeepFromTwoDown = secondsLeft <= 4;
6391
if (shouldBeepFromTwoDown) {
6492
const shouldGetReady =
65-
status === Status.prework || status === Status.break;
93+
status.value === Status.PREWORK ||
94+
status.value === Status.BREAK;
6695
if (shouldGetReady) {
6796
beepBreak.play();
6897
} else {
@@ -72,24 +101,24 @@ export function useWorkoutTimer() {
72101
setTimeLeft(previousTimeLeft => subSeconds(previousTimeLeft, 1));
73102
} else {
74103
const shouldSwitchToWork =
75-
(statusRef.current === Status.prework ||
76-
statusRef.current === Status.break) &&
104+
(statusRef.current === Status.PREWORK ||
105+
statusRef.current === Status.BREAK) &&
77106
roundsLeftRef.current > 0 &&
78107
workInterval.valueOf() !== 0;
79108
const shouldSwitchToRest =
80-
statusRef.current === Status.work && roundsLeftRef.current > 0;
109+
statusRef.current === Status.WORK && roundsLeftRef.current > 0;
81110

82111
if (shouldSwitchToWork) {
83112
beepBreakLong.play();
84-
setStatus(Status.work);
113+
send(statusEvents.WORK);
85114
setTimeLeft(workInterval);
86115
setRoundsLeft(prevRoundsLeft => prevRoundsLeft - 1);
87116
} else if (shouldSwitchToRest) {
88-
setStatus(Status.break);
117+
send(statusEvents.BREAK);
89118
setTimeLeft(breakInterval);
90119
beepWorkLong.play();
91120
} else {
92-
setStatus(Status.stopped);
121+
send(statusEvents.STOP);
93122
clearInterval(interval);
94123
}
95124
}
@@ -108,6 +137,7 @@ export function useWorkoutTimer() {
108137
beepBreak,
109138
beepBreakLong,
110139
beepWorkLong,
140+
send,
111141
]);
112142
return {
113143
rounds,
@@ -118,7 +148,7 @@ export function useWorkoutTimer() {
118148
setBreakInterval,
119149
start,
120150
timeLeft,
121-
status,
151+
status: status.value,
122152
roundsLeft,
123153
};
124154
}

src/model/Status.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export const Status = {
2-
stopped: 'stopped',
3-
prework: 'prework',
4-
work: 'work',
5-
break: 'break',
2+
STOPPED: 'STOPPED',
3+
PREWORK: 'PREWORK',
4+
WORK: 'WORK',
5+
BREAK: 'BREAK',
66
};

src/model/StatusDisplay.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const StatusDisplay = {
2+
PREWORK: 'PREP',
3+
BREAK: 'REST',
4+
WORK: 'WORK',
5+
};

src/utils/evaluateStatus.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Status } from '../model/Status';
2+
import { StatusDisplay } from '../model/StatusDisplay';
3+
4+
export const evaluateStatus = status => {
5+
if (status === Status.PREWORK || status === Status.BREAK) {
6+
return StatusDisplay[status];
7+
}
8+
return StatusDisplay.WORK;
9+
};

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,6 +1660,11 @@
16601660
"@webassemblyjs/wast-parser" "1.8.5"
16611661
"@xtuc/long" "4.2.2"
16621662

1663+
"@xstate/react@^0.8.1":
1664+
version "0.8.1"
1665+
resolved "https://registry.yarnpkg.com/@xstate/react/-/react-0.8.1.tgz#df200547cca24c909572b7aef1ddab8bca3a0603"
1666+
integrity sha512-8voZm4GX3x70lNQVvoGedoObPYapkQIbgMhE+xOQEsm8Ait4Zto6R01SZ6WJD4qvLl8JPV6uq96OcFRdEsVESg==
1667+
16631668
"@xtuc/ieee754@^1.2.0":
16641669
version "1.2.0"
16651670
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -11091,6 +11096,11 @@ xregexp@4.0.0:
1109111096
resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-4.0.0.tgz#e698189de49dd2a18cc5687b05e17c8e43943020"
1109211097
integrity sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==
1109311098

11099+
xstate@^4.11.0:
11100+
version "4.11.0"
11101+
resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.11.0.tgz#dc0bd31079fe22918c2c27c118d6310bef3dcd9e"
11102+
integrity sha512-v+S3jF2YrM2tFOit8o7+4N3FuFd9IIGcIKHyfHeeNjMlmNmwuiv/IbY9uw7ECifx7H/A9aGLcxPSr0jdjTGDww==
11103+
1109411104
xtend@^4.0.0, xtend@~4.0.1:
1109511105
version "4.0.2"
1109611106
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"

0 commit comments

Comments
 (0)