Skip to content

Commit 980efeb

Browse files
committed
Add documentation
1 parent 0038f62 commit 980efeb

File tree

8 files changed

+297
-60
lines changed

8 files changed

+297
-60
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import TimerPage from "./TimerPage"
55
import "./App.css"
66

77
/**
8+
* The main app component.
89
*
10+
* @returns {Element} The main app component.
911
*/
1012
export default function App() {
1113
return (

src/Timer.tsx

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,24 @@ import {
2121
} from "./svgTools"
2222

2323
/**
24+
* Timer component.
2425
*
25-
* @param props
26-
* @param props.id
27-
* @param props.currentTime
28-
* @param props.triggerStart
29-
* @param props.triggerStop
30-
* @param props.onDelete
31-
* @param props.notifyWhenFinished
32-
* @param props.siblingRunning
26+
* This is the main component for the timer page, it represents a countdown timer
27+
* that may have children timers and enforces some rules about its children.
28+
*
29+
* @param {any} props Component props
30+
* @param {UUID} props.id
31+
* The ID of the timer to display (used to lookup the timer data from local storage)
32+
* @param {DateTime} props.currentTime The current time (used to calculate the time remaining)
33+
* @param {Function} props.triggerStart Callback to inform the parent that the timer has started
34+
* @param {Function} props.triggerStop Callback to inform the parent that the timer has paused
35+
* @param {Function} props.onDelete Callback to inform the parent that the timer has been deleted
36+
* @param {boolean} props.notifyWhenFinished Whether to notify the user when the timer finishes
37+
* @param {UUID} props.siblingRunning
38+
* The ID of the sibling timer that is currently running (may be this timer,
39+
* or a non-existent timer). Used to enforce the rule that only one timer at a level
40+
* can be running at a time.
41+
* @returns {Element} The timer component
3342
*/
3443
export default function Timer(props: {
3544
id: UUID,
@@ -48,10 +57,17 @@ export default function Timer(props: {
4857
} = props
4958

5059
/**
60+
* This is a custom hook to manage the state of the timer and synchronize it
61+
* with local storage by UUID. It is a wrapper around useLocalStorage and
62+
* saves values in the format `<uuid>-<stateName>`.
5163
*
52-
* @param stateName
53-
* @param defaultValue
54-
* @param serializer
64+
* @template T The type of the state to manage
65+
* @param {string} stateName The name of the state to manage (used as a suffix for the key)
66+
* @param {T} defaultValue The default value to use if the state is not found in local storage
67+
* @param {CustomSerializable} serializer
68+
* Serializer to correctly manage the state in local storage
69+
* @returns {[T, (newValue: T) => void, () => void]}
70+
* The state value, a function to update the state, and a function to reset the state
5571
*/
5672
function useUUIDStore<T>(
5773
stateName: string,
@@ -61,16 +77,19 @@ export default function Timer(props: {
6177
return useLocalStorage<T>(`${id}-${stateName}`, defaultValue, serializer)
6278
}
6379

80+
// TODO: setName and setTotalTime are unused, they'll be put in the edit dialog
81+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6482
const [name, setName, clearName] = useUUIDStore<string>("name", "unnamed")
83+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
6584
const [totalTime, setTotalTime, clearTotalTime] = useUUIDStore<Duration>("totalTime", Duration.fromMillis(0), durationSerializer)
66-
const [parentID, setParentID, clearParentID] = useUUIDStore<UUID|"root">("parentID", "root")
6785
const [childrenIDs, setChildrenIDs, clearChildrenIDs] = useUUIDStore<UUID[]>("childrenIDs", [])
6886

6987
const [childRunning, setChildRunning, clearChildRunning] = useUUIDStore<UUID | undefined>("childRunning", undefined)
7088
const [started, setStarted, clearStarted] = useUUIDStore<DateTime | undefined>("started", undefined, datetimeMaybeSerializer)
7189
const [finished, setFinished, clearFinished] = useUUIDStore<boolean>("finished", false)
7290
const [elapsed, setElapsed, clearElapsed] = useUUIDStore<Duration>("elapsed", Duration.fromMillis(0), durationSerializer)
7391

92+
// These are the states that are not saved to local storage
7493
const [expanded, setExpanded] = useState<boolean>(false)
7594
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false)
7695

@@ -79,10 +98,11 @@ export default function Timer(props: {
7998
setChildrenIDs([...childrenIDs, timer.id])
8099
}
81100

101+
// This function cleans up storage on deletion, though it's a bit messy and
102+
// could probably be improved to more easily support adding new states
82103
const clearSelf = () => {
83104
clearName()
84105
clearTotalTime()
85-
clearParentID()
86106
clearChildrenIDs()
87107
clearChildRunning()
88108
clearStarted()
@@ -236,14 +256,17 @@ export default function Timer(props: {
236256
}
237257

238258
/**
259+
* Custom start/pause control for timers that indecates the percent of time remaining
239260
*
240-
* @param props
241-
* @param props.running
242-
* @param props.finished
243-
* @param props.startable
244-
* @param props.percentRemaining
245-
* @param props.onStart
246-
* @param props.onStop
261+
* @param {any} props Component props
262+
* @param {boolean} props.running Whether the timer is currently running
263+
* @param {boolean} props.finished Whether the timer has finished
264+
* @param {boolean} props.startable
265+
* Whether the timer can be started (parent timers can't be started if all their time is allocated)
266+
* @param {number} props.percentRemaining The percent of time remaining (0-1)
267+
* @param {Function} props.onStart Callback to start the timer
268+
* @param {Function} props.onStop Callback to pause the timer
269+
* @returns {Element} The component
247270
*/
248271
function TimerControl(props: {
249272
running: boolean, finished: boolean, startable: boolean,

src/TimerDialogs.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ import { uuidv4 } from "./uuid"
77
import { TimerData } from "./timerUtils"
88

99
/**
10+
* Component form dialog for adding a new timer
1011
*
11-
* @param props
12-
* @param props.maxDuration
13-
* @param props.parentID
14-
* @param props.addTimer
15-
* @param props.onCancel
12+
* @param {any} props Component props
13+
* @param {Duration?} props.maxDuration
14+
* The maximum duration that can be set on this timer (by parent)
15+
* @param {UUID} props.parentID The ID of the parent timer (if any)
16+
* @param {Function} props.addTimer Callback to add a timer to the page
17+
* @param {Function} props.onCancel Callback to cancel the timer creation
18+
* @returns {Element} A JSX element for the timer creation dialog
1619
*/
1720
export function AddTimerDialog(props: {
1821
maxDuration?: Duration,
@@ -82,10 +85,12 @@ export function AddTimerDialog(props: {
8285
}
8386

8487
/**
88+
* Component with a custom hours:minutes:seconds duration input
8589
*
86-
* @param props
87-
* @param props.onChange
88-
* @param props.maxDuration
90+
* @param {any} props Component props
91+
* @param {Function} props.onChange Callback to call when the duration changes
92+
* @param {Duration} props.maxDuration The maximum duration that can be set (by parent, if any)
93+
* @returns {Element} A JSX element for the duration input
8994
*/
9095
export function DurationInput(props: {
9196
onChange: (time: {hours: number, minutes: number, seconds: number}) => void,

src/TimerPage.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,27 @@ import { AddTimerDialog } from "./TimerDialogs"
1313
import Timer from "./Timer"
1414

1515
/**
16+
* The main page of the app, which displays all the root timers in a list.
1617
*
18+
* @returns {Element} The main page of the app.
1719
*/
1820
export default function TimerPage(props: {}) {
1921
/**
22+
* This is a custom hook to manage the state of the root timers and synchronize
23+
* them with local storage. Values are saved with the key `root-<stateName>`.
2024
*
21-
* @param stateName
22-
* @param defaultValue
25+
* @template T The type of the state to manage
26+
* @param {string} stateName The name of the state to manage
27+
* @param {T} defaultValue The default value of the state
28+
* @returns {[T, (value: T) => void, () => void]}
29+
* The state value, a function to set the state value, and a function to reset it
2330
*/
2431
function usePageStore<T>(stateName: string, defaultValue: T):
2532
[T, (newValue: T) => void, () => void] {
2633
return useLocalStorage<T>(`root-${stateName}`, defaultValue)
2734
}
2835

36+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2937
const [timerIDs, setTimerIDs, _] = usePageStore<UUID[]>("timers", [])
3038
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false)
3139
const [currentTime, setCurrentTime] = useState<DateTime>(DateTime.local())

src/localStorageTools.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,76 @@
11
import { useState } from "react"
22
import { DateTime, Duration } from "luxon"
33

4+
/**
5+
* CustomSerializable is a generic type that can provide serialization/deserialization
6+
* for any type to allow it to be reliably stored in localStorage.
7+
*
8+
* @interface
9+
*/
410
export interface CustomSerializable<T> {
11+
/**
12+
* serialize is a function that takes a value of type T and returns a string
13+
*
14+
* @template T
15+
* @param {T} value
16+
* @returns {string}
17+
* @memberof CustomSerializable
18+
*/
519
stringify: (value: T) => string
20+
21+
/**
22+
* deserialize is a function that takes a string and returns a value of type T
23+
*
24+
* @template T
25+
* @param {string} value
26+
* @returns {T}
27+
* @memberof CustomSerializable
28+
*/
629
parse: (value: string) => T
730
}
831

32+
/**
33+
* The defaultSerializer is a CustomSerializable that uses JSON.stringify and JSON.parse,
34+
* but it also wraps strings in quotes to ensure that they are stored as strings in localStorage
35+
* and can be retrieved as strings.
36+
*
37+
* @implements {CustomSerializable<any>}
38+
*/
939
export const defaultSerializer: CustomSerializable<any> = {
1040
stringify: (value: any) => ((typeof value === "string") ? `"${value}"` : JSON.stringify(value)),
1141
parse: (value: string) => JSON.parse(value),
1242
}
1343

44+
/**
45+
* The datetimeMaybeSerializer is a CustomSerializable that stores Luxon DateTime objects
46+
* as strings in ISO format, but also stores undefined values as the string "undefined".
47+
*
48+
* @implements {CustomSerializable<DateTime | undefined>}
49+
*/
1450
export const datetimeMaybeSerializer: CustomSerializable<DateTime | undefined> = {
1551
stringify: (value: DateTime | undefined) => ((value === undefined) ? "\"undefined\"" : value.toISO()),
1652
parse: (value: string) => ((value === "\"undefined\"") ? undefined : DateTime.fromISO(value)),
1753
}
1854

55+
/**
56+
* The durationSerializer is a CustomSerializable that stores Luxon Duration objects
57+
* as milliseconds.
58+
*
59+
* @implements {CustomSerializable<Duration>}
60+
*/
1961
export const durationSerializer: CustomSerializable<Duration> = {
2062
stringify: (value: Duration) => value.shiftTo("milliseconds").milliseconds.toString(),
2163
parse: (value: string) => Duration.fromMillis(parseInt(value, 10)).shiftTo("hours", "minutes", "seconds"),
2264
}
2365

2466
/**
2567
*
26-
* @param storageKey
27-
* @param defaultValue
28-
* @param serialization
68+
* @template T
69+
* @param {string} storageKey The key to use for localStorage
70+
* @param {T} defaultValue The default value to use if the key is not found in localStorage
71+
* @param {CustomSerializable<T>} serialization The serialization method to use for the value
72+
* @returns {[T, (value: T) => void, () => void]}
73+
* The state value, a function to set the state value, and a function to reset it
2974
*/
3075
export function useLocalStorage<T>(
3176
storageKey: string,
@@ -41,6 +86,8 @@ export function useLocalStorage<T>(
4186
return defaultValue
4287
})
4388

89+
// Note for future self: we can't useEffect here because it will cause an infinite loop
90+
4491
const updateValue = (newValue: T) => {
4592
setValue(newValue)
4693
localStorage.setItem(storageKey, serialization.stringify(newValue))

0 commit comments

Comments
 (0)