Skip to content

Commit 0bc3b6d

Browse files
authored
Merge pull request #14 from asherhe/dev
add timer settings
2 parents 25d5a87 + 7dfddcc commit 0bc3b6d

9 files changed

+441
-26
lines changed

src/components/app.jsx

+59-14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react";
33
import TimerDisplay from "./timer-display";
44
import TimerControls from "./timer-controls";
55
import TypeControls from "./type-controls";
6+
import { TimerConfig, defaultConfig } from "./timer-config";
67

78
import TimerWorker from "../timer-worker";
89
import buildWorker from "../worker-builder";
@@ -17,12 +18,19 @@ class TimerType {
1718
this.breakCount = 0;
1819
}
1920

20-
duration() {
21+
/**
22+
*
23+
* @param {import("./timer-config").config} config
24+
* @returns {number} the duration of the timer's current state
25+
*/
26+
duration(config) {
2127
switch (this.state) {
2228
case "work":
23-
return 1500;
29+
return config.work;
2430
case "break":
25-
return this.breakCount % 4 === 3 ? 900 : 300;
31+
return (this.breakCount + 1) % config.longfreq === 0
32+
? config.longbreak
33+
: config.break;
2634
default:
2735
return -1;
2836
}
@@ -49,28 +57,42 @@ class TimerType {
4957
* @returns {React.ReactNode}
5058
*/
5159
function App(props) {
52-
/** @type {[DOMHighResTimeStamp?, React.SetStateAction<DOMHighResTimeStamp?>]} */
60+
/**
61+
* app config
62+
* @type {[import("./timer-config").config, React.SetStateAction<import("./timer-config").config>]}
63+
*/
64+
const [config, setConfig] = useState(defaultConfig);
65+
66+
/**
67+
* timestamp of when the timer was started
68+
* @type {[DOMHighResTimeStamp?, React.SetStateAction<DOMHighResTimeStamp?>]}
69+
*/
5370
const [timerStart, setTimerStart] = useState(undefined);
5471
const [elapsed, setElapsed] = useState(0);
5572
const [timerType, setTimerType] = useState(new TimerType("work"));
56-
const [duration, setDuration] = useState(timerType.duration());
73+
const [duration, setDuration] = useState(timerType.duration(config));
5774

75+
// timer worker (runs in background)
5876
const worker = useMemo(() => {
5977
let worker = buildWorker(TimerWorker);
6078
worker.postMessage([performance.timeOrigin]); // post time origin
6179
return worker;
6280
}, []);
6381

82+
// time update from worker
6483
worker.onmessage = (e) => {
6584
let elapsedTime = e.data;
66-
// console.log(elapsedTime);
6785
if (elapsedTime !== elapsed) {
6886
if (elapsedTime >= duration) timerFinish();
6987
else setElapsed(elapsedTime);
7088
}
7189
};
7290

73-
const bell = useMemo(() => new Audio(`${process.env.PUBLIC_URL}/bell.mp3`), []);
91+
// load the bell sound
92+
const bell = useMemo(
93+
() => new Audio(`${process.env.PUBLIC_URL}/bell.mp3`),
94+
[]
95+
);
7496

7597
const isPlaying = useCallback(() => {
7698
return timerStart !== undefined;
@@ -87,22 +109,28 @@ function App(props) {
87109
};
88110

89111
const restart = () => {
90-
setDuration(timerType.duration());
112+
setDuration(timerType.duration(config));
91113
if (isPlaying()) setTimerStart(performance.now());
92114
};
93115

94116
const timerFinish = useCallback(() => {
117+
// make a new notification
95118
const notifyFinish = () => {
96119
new Notification("Timer done", {
97-
body: timerType.state === "work" ? "It's time to get back to work!" : "It's time to take a break!",
120+
body:
121+
timerType.state === "work"
122+
? "It's time to get back to work!"
123+
: "It's time to take a break!",
98124
});
99125
};
100126

127+
// reset timer
101128
setTimerStart(undefined);
102129
setElapsed(0);
103130
setTimerType(timerType.next());
104-
setDuration(timerType.duration());
131+
setDuration(timerType.duration(config));
105132

133+
// send notification
106134
if (!("Notification" in window)) {
107135
alert("Timer done!");
108136
} else if (Notification.permission === "granted") {
@@ -115,30 +143,47 @@ function App(props) {
115143
});
116144
}
117145
bell.play();
118-
}, [timerType, bell]);
119146

147+
if (config.autoStart) {
148+
play();
149+
}
150+
}, [timerType, bell, config]);
151+
152+
// update time for woker
120153
useEffect(() => {
121154
worker.postMessage(timerStart);
122155
}, [timerStart, worker]);
123156

157+
// update timer duration if config is updated and timer is not running
158+
useEffect(() => {
159+
if (!isPlaying()) setDuration(timerType.duration(config));
160+
}, [config, isPlaying, timerType]);
161+
124162
let playing = isPlaying();
125163
return (
126-
<div className={"timer-app" + (timerType.state === "break" ? " break" : "")}>
164+
<div
165+
className={"timer-app" + (timerType.state === "break" ? " break" : "")}
166+
>
127167
<div className="timer-content">
128168
<TimerDisplay time={duration - elapsed} />
129-
<TimerControls mode={playing ? "pause" : "play"} onplay={playing ? pause : play} onrestart={restart} />
169+
<TimerControls
170+
mode={playing ? "pause" : "play"}
171+
onplay={playing ? pause : play}
172+
onrestart={restart}
173+
/>
130174
<TypeControls
131175
type={timerType.state}
132176
setType={(type) => {
133177
let newType = new TimerType(type);
134178
setTimerStart(undefined);
135179
setElapsed(0);
136180
setTimerType(newType);
137-
setDuration(newType.duration());
181+
setDuration(newType.duration(config));
138182
}}
139183
active={!playing}
140184
/>
141185
</div>
186+
<TimerConfig config={config} setConfig={setConfig} />
142187
</div>
143188
);
144189
}

src/components/checkbox.jsx

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useCallback } from "react";
2+
3+
/**
4+
*
5+
* @param {{val: boolean, setVal: (val: boolean) => void}} props
6+
*/
7+
function CheckBox({ val, setVal }) {
8+
const onClick = useCallback(() => setVal(!val), [val, setVal]);
9+
10+
return (
11+
<input
12+
className="input-toggle"
13+
type="checkbox"
14+
defaultChecked={val}
15+
onClick={onClick}
16+
/>
17+
);
18+
}
19+
20+
export default CheckBox;

src/components/number-input.jsx

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useCallback } from "react";
2+
3+
/**
4+
*
5+
* @param {{val: number, setVal: (val: number) => void, min: number, max: number, step: number}} props
6+
*/
7+
function NumberInput({
8+
val,
9+
setVal,
10+
min = undefined,
11+
max = undefined,
12+
step = 1,
13+
}) {
14+
const onChange = useCallback(
15+
(e) => {
16+
let v = parseFloat(e.target.value);
17+
setVal(v);
18+
},
19+
[setVal]
20+
);
21+
22+
return (
23+
<input
24+
className="input-number"
25+
type="number"
26+
value={val}
27+
min={min}
28+
max={max}
29+
step={step}
30+
onChange={onChange}
31+
/>
32+
);
33+
}
34+
35+
export default NumberInput;

src/components/timer-config.jsx

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useCallback, useState } from "react";
2+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
3+
import { faGear } from "@fortawesome/free-solid-svg-icons";
4+
import NumberInput from "./number-input";
5+
import CheckBox from "./checkbox";
6+
7+
/**
8+
* @typedef {{work: number, break: number, longbreak: number, longfreq: number, autoStart: boolean }} config
9+
* @type {config}
10+
*/
11+
export const defaultConfig = {
12+
work: 1500,
13+
break: 300,
14+
longbreak: 900,
15+
longfreq: 4,
16+
autoStart: false,
17+
};
18+
19+
/**
20+
* @param {{config: config, setConfig: (v: config) => void}} props
21+
*/
22+
export function TimerConfig({ config, setConfig }) {
23+
const [show, setShow] = useState(false);
24+
25+
/**
26+
* sets some config property `prop` to `val`
27+
* @param {string} prop
28+
* @param {*} val
29+
*/
30+
const setProp = useCallback(
31+
(prop, val) => {
32+
let c = structuredClone(config);
33+
c[prop] = val;
34+
setConfig(c);
35+
},
36+
[config, setConfig]
37+
);
38+
39+
return (
40+
<div className={"timer-config" + (show ? " show" : "")}>
41+
<div className="timer-config-button" onClick={() => setShow(!show)}>
42+
<FontAwesomeIcon icon={faGear} />
43+
</div>
44+
<div className="timer-config-menu">
45+
<div className="timer-config-title">Settings</div>
46+
<div className="timer-config-list">
47+
<div>
48+
<span className="timer-config-list-title">Work duration:</span>
49+
<span>
50+
<NumberInput
51+
val={config.work / 60}
52+
setVal={(v) => setProp("work", v * 60)}
53+
min="1"
54+
/>
55+
&nbsp;minutes
56+
</span>
57+
</div>
58+
<div>
59+
<span className="timer-config-list-title">Break duration:</span>
60+
<span>
61+
<NumberInput
62+
val={config.break / 60}
63+
setVal={(v) => setProp("break", v * 60)}
64+
min="1"
65+
/>
66+
&nbsp;minutes
67+
</span>
68+
</div>
69+
<div>
70+
<span className="timer-config-list-title">
71+
Long break duration:
72+
</span>
73+
<span>
74+
<NumberInput
75+
val={config.longbreak / 60}
76+
setVal={(v) => setProp("longbreak", v * 60)}
77+
min="1"
78+
/>
79+
&nbsp;minutes
80+
</span>
81+
</div>
82+
<div>
83+
<span className="timer-config-list-title">
84+
Long break frequency:
85+
</span>
86+
<span>
87+
<NumberInput
88+
val={config.longfreq}
89+
setVal={(v) => setProp("longfreq", v)}
90+
min="0"
91+
/>
92+
&nbsp;breaks
93+
</span>
94+
</div>
95+
<div>
96+
<span className="timer-config-list-title">
97+
Auto start next timer?
98+
</span>
99+
<CheckBox
100+
val={config.autoStart}
101+
setVal={(v) => setProp("autoStart", v)}
102+
/>
103+
</div>
104+
</div>
105+
</div>
106+
</div>
107+
);
108+
}

src/components/timer-controls.jsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
2-
import { faPlay, faPause, faArrowRotateLeft } from "@fortawesome/free-solid-svg-icons";
2+
import {
3+
faPlay,
4+
faPause,
5+
faArrowRotateLeft,
6+
} from "@fortawesome/free-solid-svg-icons";
37

48
/**
59
* @param {{mode: "play" | "pause", onplay: () => void, onrestart: () => void}} props
6-
* @returns
710
*/
811
function TimerControls({ mode, onplay, onrestart }) {
912
return (

src/components/timer-display.jsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
import React from "react";
2-
31
/**
42
* @param {{time: number}} props
5-
* @returns {React.ReactNode}
63
*/
74
function TimerDisplay({ time }) {
8-
let timeStr = `${Math.floor(time / 60)
9-
.toString()
10-
.padStart(2, "0")}:${(time % 60).toString().padStart(2, "0")}`;
5+
let secs = time % 60,
6+
mins = Math.floor(time / 60) % 60,
7+
hrs = Math.floor(time / 3600);
8+
secs = secs.toString().padStart(2, "0");
9+
mins = mins.toString().padStart(2, "0");
10+
hrs = hrs ? hrs.toString().padStart(2, "0") + ":" : "";
11+
let timeStr = `${hrs}${mins}:${secs}`;
1112
document.title = `${timeStr} - timeato`;
1213
return <span className="timer-display">{timeStr}</span>;
1314
}

src/components/type-controls.jsx

-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
/**
2-
*
32
* @param {{type: "work" | "break", setType: (value: "work" | "break") => void, active: boolean}} props
4-
* @returns
53
*/
64
function TypeControls({ type, setType, active }) {
75
return (

0 commit comments

Comments
 (0)