diff options
Diffstat (limited to 'apps/web-shared/src/components/stopwatch.svelte')
| -rw-r--r-- | apps/web-shared/src/components/stopwatch.svelte | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/apps/web-shared/src/components/stopwatch.svelte b/apps/web-shared/src/components/stopwatch.svelte new file mode 100644 index 0000000..8287e31 --- /dev/null +++ b/apps/web-shared/src/components/stopwatch.svelte @@ -0,0 +1,161 @@ +<script lang="ts"> + import {writable_persistent} from "$shared/lib/persistent-store"; + import Button from "$shared/components/button.svelte"; + import {Textarea} from "$shared/components/form"; + import {StorageKeys} from "$shared/lib/configuration"; + import {Temporal} from "@js-temporal/polyfill"; + import {createEventDispatcher, onMount} from "svelte"; + + const state = writable_persistent({ + initialState: { + hours: 0, + minutes: 0, + seconds: 0, + startTime: null as Temporal.PlainTime, + isRunning: false, + intervalId: 0, + note: "", + }, + name: StorageKeys.stopwatch, + }); + + let timeString; + + $: if ($state.hours || $state.minutes || $state.seconds) { + timeString = $state.hours.toLocaleString(undefined, {minimumIntegerDigits: 2}) + + ":" + $state.minutes.toLocaleString(undefined, {minimumIntegerDigits: 2}) + + ":" + $state.seconds.toLocaleString(undefined, {minimumIntegerDigits: 2}); + } else { + timeString = "--:--:--"; + } + + onMount(() => { + if ($state.isRunning) { + clearInterval($state.intervalId); + $state.intervalId = setInterval(step, 1000); + } + }); + + const dispatch = createEventDispatcher(); + + function step() { + $state.seconds = $state.seconds + 1; + + if ($state.seconds == 60) { + $state.minutes = $state.minutes + 1; + $state.seconds = 0; + } + + if ($state.minutes == 60) { + $state.hours = $state.hours + 1; + $state.minutes = 0; + $state.seconds = 0; + } + + if (!$state.startTime) $state.startTime = Temporal.Now.plainTimeISO(); + } + + function reset() { + clearInterval($state.intervalId); + $state.isRunning = false; + $state.hours = 0; + $state.minutes = 0; + $state.seconds = 0; + $state.startTime = null; + $state.intervalId = 0; + $state.note = ""; + } + + let roundUpToNearest = 30; + let roundDownToNearest = 30; + + function on_round_up() { + const newTime = Temporal.PlainTime + .from({hour: $state.hours, minute: $state.minutes, second: $state.seconds}) + .round({ + roundingIncrement: roundUpToNearest, + smallestUnit: "minute", + roundingMode: "ceil" + }); + $state.hours = newTime.hour; + $state.minutes = newTime.minute; + $state.seconds = newTime.second; + } + + function on_round_down() { + const newTime = Temporal.PlainTime + .from({hour: $state.hours, minute: $state.minutes, second: $state.seconds,}) + .round({ + roundingIncrement: roundDownToNearest, + smallestUnit: "minute", + roundingMode: "trunc" + }); + $state.hours = newTime.hour; + $state.minutes = newTime.minute; + $state.seconds = newTime.second; + } + + function on_start_stop() { + if ($state.isRunning) { + clearInterval($state.intervalId); + $state.isRunning = false; + return; + } + step(); + $state.intervalId = setInterval(step, 1000); + $state.isRunning = true; + } + + function on_create_entry() { + if (!$state.startTime) return; + const plainStartTime = Temporal.PlainTime.from($state.startTime); + dispatch("create", { + from: plainStartTime, + to: plainStartTime.add({hours: $state.hours, minutes: $state.minutes, seconds: $state.seconds}), + description: $state.note + }); + reset(); + } +</script> + +<div class="grid"> + <div class="col-6"> + <slot name="header"></slot> + <pre class="text-xxl padding-y-sm">{timeString}</pre> + </div> + <div class="col-6 flex align-bottom flex-column text-xs"> + <Button title="{$state.isRunning ? 'Stop' : 'Start'}" + text="{$state.isRunning ? 'Stop' : 'Start'}" + variant="link" + on:click={on_start_stop}/> + + {#if $state.startTime} + <Button title="Reset" + text="Reset" + variant="link" + class="bg-error-lighter@hover color-white@hover" + on:click={reset}/> + {#if !$state.isRunning} + <Button title="Round up" + text="Round up" + variant="link" + on:click={on_round_up}/> + <Button title="Round down" + text="Round down" + variant="link" + on:click={on_round_down}/> + {#if $state.minutes > 0 || $state.hours > 0} + <Button title="Create entry" + text="Create entry" + variant="link" + on:click={on_create_entry}/> + {/if} + {/if} + {/if} + </div> +</div> +<Textarea class="width-100% margin-top-xs" + placeholder="What's your focus?" + rows="1" + bind:value={$state.note} +/> |
