summaryrefslogtreecommitdiffstats
path: root/apps/web-shared/src/components/stopwatch.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web-shared/src/components/stopwatch.svelte')
-rw-r--r--apps/web-shared/src/components/stopwatch.svelte161
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}
+/>