Skip to content

Commit 2f82b6d

Browse files
committed
Separate out components into different files
1 parent d186162 commit 2f82b6d

File tree

8 files changed

+586
-556
lines changed

8 files changed

+586
-556
lines changed

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react'
22

3-
import { TimerPage } from './timer'
3+
import TimerPage from './TimerPage'
44

55
import './App.css'
66

src/Timer.tsx

Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
import React, { useState } from "react"
2+
3+
import AccountTreeTwoToneIcon from "@mui/icons-material/AccountTreeTwoTone"
4+
import AccountTreeIcon from "@mui/icons-material/AccountTree"
5+
import ReplayIcon from "@mui/icons-material/Replay"
6+
import { Add, Delete } from "@mui/icons-material"
7+
8+
import { DateTime, Duration } from "luxon"
9+
10+
import type { UUID } from "./uuid"
11+
import {
12+
useLocalStorage, CustomSerializable,
13+
defaultSerializer, durationSerializer,
14+
datetimeMaybeSerializer,
15+
} from "./localStorageTools"
16+
import { TimerData, saveTimer, getTimerDuration } from "./timerUtils"
17+
import { AddTimerDialog } from "./TimerDialogs"
18+
import { PauseCircle, PlayButton, SemiCircle, FinishedBox } from "./svgTools"
19+
20+
export default function Timer(props: {
21+
id: UUID,
22+
currentTime: DateTime,
23+
triggerStart?: () => void,
24+
triggerStop?: () => void,
25+
onDelete?: () => void,
26+
notifyWhenFinished?: boolean,
27+
siblingRunning?: UUID,
28+
}) {
29+
30+
function useUUIDStore<T>(stateName: string, defaultValue: T, serializer: CustomSerializable<T> = defaultSerializer): [T, (newValue: T) => void, () => void] {
31+
return useLocalStorage<T>(`${props.id}-${stateName}`, defaultValue, serializer)
32+
}
33+
34+
const [name, setName, clearName] = useUUIDStore<string>("name", "unnamed")
35+
const [totalTime, setTotalTime, clearTotalTime] = useUUIDStore<Duration>("totalTime", Duration.fromMillis(0), durationSerializer)
36+
const [parentID, setParentID, clearParentID] = useUUIDStore<UUID|"root">("parentID", "root")
37+
const [childrenIDs, setChildrenIDs, clearChildrenIDs] = useUUIDStore<UUID[]>("childrenIDs", [])
38+
39+
const [childRunning, setChildRunning, clearChildRunning] = useUUIDStore<UUID | undefined>("childRunning", undefined)
40+
const [started, setStarted, clearStarted] = useUUIDStore<DateTime | undefined>("started", undefined, datetimeMaybeSerializer)
41+
const [finished, setFinished, clearFinished] = useUUIDStore<boolean>("finished", false)
42+
const [elapsed, setElapsed, clearElapsed] = useUUIDStore<Duration>("elapsed", Duration.fromMillis(0), durationSerializer)
43+
44+
const [expanded, setExpanded] = useState<boolean>(false)
45+
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false)
46+
47+
const addTimer = (timer: TimerData) => {
48+
saveTimer(timer)
49+
setChildrenIDs([...childrenIDs, timer.id])
50+
}
51+
52+
const clearSelf = () => {
53+
clearName()
54+
clearTotalTime()
55+
clearParentID()
56+
clearChildrenIDs()
57+
clearChildRunning()
58+
clearStarted()
59+
clearFinished()
60+
clearElapsed()
61+
}
62+
63+
const currentSegment = (started !== undefined) ? props.currentTime.diff(started) : Duration.fromMillis(0)
64+
const timeRemaining = totalTime.minus(currentSegment.plus(elapsed))
65+
const childrenTime: Duration = childrenIDs.reduce(
66+
(acc, cid) => acc.plus(getTimerDuration(cid)), Duration.fromMillis(0))
67+
.shiftTo("milliseconds")
68+
const unallocatedTime = totalTime.minus(childrenTime)
69+
70+
if (timeRemaining.shiftTo("milliseconds").milliseconds < 10 && started) {
71+
if (props.notifyWhenFinished) {
72+
new Notification("Timer finished", { body: name })
73+
}
74+
setStarted(undefined)
75+
setFinished(true)
76+
props.triggerStop && props.triggerStop()
77+
}
78+
79+
const toggleExpanded = () => { setExpanded(!expanded) }
80+
81+
const startTimer = () => {
82+
setStarted(props.currentTime)
83+
props.triggerStart && props.triggerStart()
84+
}
85+
86+
const stopTimer = () => {
87+
setElapsed(elapsed.plus(props.currentTime.diff(started || DateTime.local())))
88+
setStarted(undefined)
89+
if (childRunning !== undefined) {
90+
setChildRunning("__NONE__" as UUID)
91+
}
92+
props.triggerStop && props.triggerStop()
93+
}
94+
95+
if (props.siblingRunning && props.siblingRunning !== props.id && started) {
96+
setElapsed(elapsed.plus(props.currentTime.diff(started || DateTime.local())))
97+
setStarted(undefined)
98+
}
99+
100+
return (
101+
<li className="Timer">
102+
<h2>
103+
<TimerControl
104+
running={started !== undefined}
105+
finished={finished}
106+
startable={totalTime.minus(childrenTime).shiftTo("milliseconds").milliseconds > 0}
107+
percentRemaining={timeRemaining.shiftTo("milliseconds").milliseconds / totalTime.shiftTo("milliseconds").milliseconds}
108+
onStart={startTimer}
109+
onStop={stopTimer}
110+
/>
111+
{` ${name} `}
112+
{expanded ?
113+
<AccountTreeIcon
114+
className="IconButton"
115+
onClick={toggleExpanded}
116+
/>
117+
:
118+
<AccountTreeTwoToneIcon
119+
className="IconButton"
120+
onClick={toggleExpanded}
121+
/>
122+
}
123+
{` ${(finished) ? "00:00:00" : timeRemaining.toFormat("hh:mm:ss")}`}
124+
{totalTime.minus(timeRemaining).shiftTo("milliseconds").milliseconds >= 0 &&
125+
<ReplayIcon
126+
className="IconButton"
127+
onClick={() => {
128+
setElapsed(Duration.fromMillis(0))
129+
setFinished(false)
130+
if (started) {
131+
setStarted(props.currentTime)
132+
}
133+
}}
134+
/>
135+
}
136+
{props.onDelete &&
137+
<Delete
138+
className="IconButton"
139+
onClick={() => {
140+
clearSelf()
141+
props.onDelete && props.onDelete()
142+
}}
143+
/>
144+
}
145+
</h2>
146+
{expanded && <ul className="TimerList">
147+
{childrenIDs.map(cid => (
148+
<Timer
149+
key={cid}
150+
id={cid}
151+
currentTime={props.currentTime}
152+
153+
triggerStart={() => {
154+
setChildRunning(cid)
155+
if (started === undefined) {
156+
startTimer()
157+
}
158+
}}
159+
triggerStop={() => {
160+
setChildRunning(undefined)
161+
if (started !== undefined) {
162+
stopTimer()
163+
}
164+
}}
165+
onDelete={() => {
166+
setChildrenIDs(childrenIDs.filter(id => id !== cid))
167+
}}
168+
siblingRunning={childRunning}
169+
notifyWhenFinished={props.notifyWhenFinished || false}
170+
/>)
171+
)}
172+
{addDialogOpen || (unallocatedTime.shiftTo("milliseconds").milliseconds > 0 &&
173+
<div>
174+
<Add
175+
className="IconButton"
176+
onClick={() => setAddDialogOpen(true)}
177+
/>
178+
{` Add Timer (unallocated: ${unallocatedTime.toFormat("hh:mm:ss")})`}
179+
</div>
180+
)}
181+
{addDialogOpen &&
182+
<AddTimerDialog
183+
addTimer={addTimer}
184+
maxDuration={unallocatedTime}
185+
parentID={props.id}
186+
onCancel={() => setAddDialogOpen(false)}
187+
/>
188+
}
189+
</ul>
190+
}
191+
</li>
192+
)
193+
}
194+
195+
function TimerControl(props: {
196+
running: boolean, finished: boolean, startable: boolean,
197+
percentRemaining: number,
198+
onStart: () => void, onStop: () => void}
199+
) {
200+
const { running, finished, startable, percentRemaining, onStart, onStop } = props
201+
const [hovering, setHovering] = useState<boolean>(false)
202+
203+
const radius = 8
204+
205+
if (finished) {
206+
return (
207+
<span className="TimerControl"
208+
style={{ cursor: "not-allowed" }}
209+
>
210+
<FinishedBox radius={radius} fill="#48A0B8" />
211+
</span>
212+
)
213+
}
214+
215+
if (!startable && !running) {
216+
return (
217+
<span className="TimerControl"
218+
style={{ cursor: "not-allowed" }}
219+
>
220+
<FinishedBox radius={radius} fill="#61DAFB" />
221+
</span>
222+
)
223+
}
224+
225+
return (
226+
<span className="TimerControl"
227+
style={{ cursor: "pointer" }}
228+
onMouseEnter={() => setHovering(true)}
229+
onMouseLeave={() => setHovering(false)}
230+
231+
onClick={running ? onStop : onStart}
232+
>
233+
{(running) ?
234+
(hovering) ?
235+
<PauseCircle radius={radius} fill="#C9EFFB" percent={percentRemaining} /> :
236+
<SemiCircle radius={radius} fill="#61dafb" percent={percentRemaining} />
237+
:
238+
<PlayButton radius={radius} fill={(hovering) ? "#C9EFFB" : "#61dafb"} />
239+
}
240+
</span>
241+
)
242+
}

0 commit comments

Comments
 (0)