An App Better than Pomodoro? Maintain Flow with SvelteKit.

An App Better than Pomodoro? Maintain Flow with SvelteKit.

Introduction

Hello everyone.

In this article, I will show you how to build a "Flowmodoro" app using SvelteKit.

What is Flowmodoro?

Flow + modoro (i.e. pomodoro) =Flowmodoro

And so, "Flowmodoro" is Pomodoro, but better suited to maintain the flow state.

Using Pomodoro, one must stop after intervals of around 25 minutes. But in Flowmodoro, you can stop whenever you feel your focus drifting (e.g. 50% of its peak). The break time would be calculated based on how long you worked.

Francesco Cirillo developed Pomodoro based on a countdown timer. But Zoe Read-Bivens devised Flowmodoro, based on a stopwatch.

And so we begin.

Project Setup

Pre-requisites

This blog post assumes familiarity with Svelte, a JavaScript frontend framework.

Installation

npm create svelte@latest my-app will create the project files. Ensure to select a Typescript project.

For this project, we'll need both Tailwind CSS and daisyUI to style the front end.

First, read the guide to install Tailwind CSS with SvelteKit. Now, install daisyUI as a plugin.

Finally, we will need an icons library. My preference is Feather, for its simplicity.

npm i svelte-feather-icons

Declaring the Main Page

The following provides the general order in which we will tackle each section of the application.

Let's start with the layout, in routes/+layout.svelte. Here we will set the page title, and add styling to the HTML body.

<script>
    import '../app.css';
</script>

<svelte:head>
    <title>Flowmodoro</title>
</svelte:head>

<main class="w-full h-full flex flex-col items-center justify-center">
    <slot />
</main>

<style lang="postcss">
    :global(body, html) {
        width: 100%;
        height: 100%;
        background: theme(backgroundColor.stone.800);
        color: theme(colors.white);
    }
</style>

And now, in routes/+page.svelte, we can declare the Svelte code for the entire application at the widest level.

<script>
    import InterruptionsCounter from '$lib/InterruptionsCounter.svelte';
    import EstimatedBreakTime from '$lib/EstimatedBreakTime.svelte';
    import Stopwatch from '$lib/Stopwatch.svelte';
    import KeyboardHelpText from '$lib/KeyboardHelpText.svelte';
    import TasksDisplay from '$lib/TasksDisplay.svelte';
    import CurrentTaskText from '$lib/CurrentTaskText.svelte';
    import StatsDisplay from '$lib/StatsDisplay.svelte';
    import OptionsDisplay from '$lib/OptionsDisplay.svelte';
    import InfoDisplay from '$lib/InfoDisplay.svelte';
</script>

<div class="flex-1 w-full max-w-6xl flex flex-col justify-center items-center">
    <div
        class="absolute top-0 right-0 display flex flex-row justify-center items-center p-12 space-x-4"
    >
        <InfoDisplay />
        <StatsDisplay />
        <OptionsDisplay />
    </div>
    <div class="mt-12 flex w-3/5 py-2 flex-col justify-center items-center space-y-3">
        <CurrentTaskText />
        <div class="flex w-full flex-row justify-around items-center">
            <EstimatedBreakTime />
            <InterruptionsCounter />
        </div>
    </div>
    <Stopwatch />

    <div class="text-xl text-center font-mono mt-8 space-y-4">
        <!-- Saving here for later -->
    </div>

    <TasksDisplay />
</div>

Stores

In $lib/stores.ts, we will define the states that power the entire application.

import { get, writable } from "svelte/store";
import { browser } from "$app/environment";

Running Time

The amount of time the user has been working, in milliseconds.

export const running = writable(0);

Interruptions

The number of interruptions the user experiences during a work session.

export const interruptions = writable(0);

Start Time

The time at which the user started their work session. This is useful for displaying their work stats later on.

export const startTime = writable(0);

Break Duration

This store holds the duration of the user's break, in milliseconds. This is so that it can be counted down to 0 during breaks.

// Set to 1 instead of 0 to avoid playing the timer sound on startup,
// since the timer sound will play whenever breakDuration reaches 0
export const breakDuration = writable(1);

Tasks

In this application, users can create tasks like they would in any other to-do list app. But we will also track the user's work sessions for each task.

export interface Task {
    id: string,
    name: string,
    checked: boolean,
    // Use an array.
    // The user may not always complete the task in 1 session.
    stats: StatsRow[],
}

export interface StatsRow {
    interruptions: number,
    workTime: number,
    breakTime: number,
    timeStartedAt: number,
}

But these tasks also need to be saved after the user closes the application. Or else, how can they track their progress over time? This is where we will use LocalStorage.

// If the browser is available, use the value in LocalStorage
// as the initial value. If not, use a default empty array
const defaultTasks: Tas[] = [];
const initialTasks: Task[] = browser ?
    JSON.parse(window.localStorage.getItem("tasks")) ?? defaultTasks
: defaultTasks;

export const tasks = writable<Task[]>(initialTasks);

// The ID of the selected task
export const selectedTask = writable("");

Then, once the value of tasks changes, we can update the LocalStorage copy.

tasks.subscribe((value) => {
    if (browser) {
        window.localStorage.setItem("tasks", JSON.stringify(value))
    }
})

Settings

The options the user has to configure the application. Similar to tasks, we will persist settings in LocalStorage.

export interface Settings {
    breakRatio: number,
    timerSound: string,
}

const defaultSettings: Settings = {
    breakRatio: 5,
    timerSound: "bell",
}
const initialSettings: Settings = browser ?
    JSON.parse(window.localStorage.getItem("settings")) ?? defaultSettings
: defaultSettings;

export const settings = writable<Settings>(initialSettings);

settings.subscribe((value) => {
    if (browser) {
        window.localStorage.setItem("settings", JSON.stringify(value))
    }
})

State

There are 3 application states:

export enum States {
    // The user hasn't yet done anything
    UNSET,
    // The user is working and time is counting up
    RUNNING,
    // The user is taking a break and time is counting down
    BREAK
}

Initialise the writable store, with the default as UNSET:

export const state = writable(States.UNSET)

We can now subscribe to the state store to run code whenever the state changes.

state.subscribe((value) => {
    // ...
});

To start, we can record the current time once the stopwatch begins:

if (value === States.RUNNING) {
    startTime.set(Date.now());
}

If the user is on break, or they have reset the application, we can reset their current data:

if (value === States.RUNNING) {
    startTime.set(Date.now());
} else {
    // ...

    interruptions.set(0);
    running.set(0);
    startTime.set(0);
}

Finally, we can handle the specific case of taking a break. We must calculate the break duration, and then update the user's stats for their selected task.

if (value === States.BREAK) {
    // Calculate the break duration
    const breakTime = get(running) / get(settings).breakRatio;
    breakDuration.set(breakTime);

    // Update the stats for the task
    tasks.update((value) => {
        const currentTaskId = get(selectedTask);
        // Get the task object for the selected task by its ID
        const currentTask = value.find((item) => item.id === currentTaskId);

        if (!currentTask) return value;

        currentTask.stats.push({
            interruptions: get(interruptions),
            workTime: Math.round(get(running) / 1000),
            breakTime: Math.round(breakTime / 1000),
            timeStartedAt: get(startTime)
        })

        // Add the updated task object to the tasks store array
        return [...value.filter((item) => item.id !== currentTaskId),
            currentTask
        ]
    });
}

Before we leave the stores file, we can subscribe to breakDuration so that the state changes to UNSET once the break is over.

breakDuration.subscribe((value) => {
    if (value <= 0) {
        state.set(States.UNSET);

        // Play the timer sound
        playSound(get(settings).timerSound);
    }
})

The Stopwatch

The code for this component lives in $lib/Stopwatch.svelte . There are 2 parts: the digits on the screen, and an overlay with buttons. For example, when the state is UNSET:

Create a new file $lib/TimeDisplay.svelte. Here is where we decide whether to count up (during work), or count down (when taking a break).

Updating Running Time

First, define a function updateRunningTime that will carry out the logic outlined above. We use requestAnimationFrame to repeatedly call the function, which passes a timestamp for us to calculate the delta time.

<script lang="ts">
    import { onMount } from 'svelte';
    import { running, state, States, breakDuration } from './stores';

    let previousTimestamp: number;

    const updateRunningTime = (timestamp: number) => {
        if (previousTimestamp === undefined) {
            previousTimestamp = timestamp;
        }

        const deltaTime = timestamp - previousTimestamp;

        switch ($state) {
            case States.UNSET:
                break;
            case States.RUNNING:
                $running += deltaTime;
                break;
            case States.BREAK:
                $breakDuration = Math.max($breakDuration - deltaTime, 0);
                break;
            default:
                break;
        }

        previousTimestamp = timestamp;

        window.requestAnimationFrame(updateRunningTime);
    };

    onMount(() => {
        const request = window.requestAnimationFrame(updateRunningTime);

        return () => {
            window.cancelAnimationFrame(request);
        };
    });
</script>

Formatting Running Time

Since we update either the running store or the breakDuration store, we must decide which of the 2 to display.

// ...

let displayTime: number;

$: switch ($state) {
    case States.UNSET:
        displayTime = 0;
        break;
    case States.RUNNING:
        displayTime = $running;
        break;
    case States.BREAK:
        displayTime = $breakDuration;
        break;
    default:
        break;
}

// ...

Now, we can work on how to output the time (which is in milliseconds).

First, we can calculate the total equivalent number of split seconds, seconds, minutes and hours.

// There are 100 'splitSeconds' in 1 second
$: splitSeconds = Math.floor(displayTime / 10);
$: seconds = Math.floor(splitSeconds / 100);
$: minutes = Math.floor(seconds / 60);
$: hours = Math.floor(minutes / 60);

Then, we can place these values into an array and display them in the markup:

<script lang="ts">
    // ...

    $: time = [hours, minutes % 60, seconds % 60, splitSeconds % 100];
</script>

<p class="text-8xl group-hover:cursor-none font-bold font-mono">
    {#each time as item, i}
        <!-- If no whole hours have passed (i = 0),
            don't display the number of hours -->
        {#if !(item === 0 && i === 0)}
            <span>
                {item.toString().padStart(2, '0')}
                {#if i !== time.length - 1}
                    :
                {/if}
            </span>
        {/if}
    {/each}
</p>

Overlay with Buttons

In $lib/StopwatchOverlay.svelte, we will include buttons that change the application's state.

<script lang="ts">
    import { state, States } from './stores';
    import { PlayCircleIcon, StopCircleIcon, RefreshCwIcon } from 'svelte-feather-icons';
    import StateButton from './StateButton.svelte';
</script>

<div
    class="w-full h-full absolute top-0 left-0 space-x-24 flex-row items-center justify-center group-hover:bg-[#00000088] group-hover:cursor-pointer hidden group-hover:flex"
>
    {#if $state === States.UNSET}
        <StateButton title="Start" newState={States.RUNNING} activateOnSpace Icon={PlayCircleIcon} />
    {:else}
        {#if $state !== States.BREAK}
            <StateButton title="Stop" newState={States.BREAK} activateOnSpace Icon={StopCircleIcon} />
        {/if}
        <StateButton title="Reset" newState={States.UNSET} Icon={RefreshCwIcon} />
    {/if}
</div>

Note that the "Start" and "Stop" buttons can be activated with the Space bar.

Defining State Buttons

Add the following code to $lib/StateButton.svelte:

<script lang="ts">
    import { state, States } from './stores';
    import { onMount, SvelteComponent } from 'svelte';

    export let title: string;
    export let newState: States;
    export let activateOnSpace: boolean = false;
    export let Icon: typeof SvelteComponent;

    const onClick = () => {
        $state = newState;
    };

    onMount(() => {
        if (!activateOnSpace) return;

        const keyHandler = (event: KeyboardEvent) => {
            if (event.key === ' ') {
                onClick();
            }
        };

        window.addEventListener('keypress', keyHandler);

        return () => {
            window.removeEventListener('keypress', keyHandler);
        };
    });
</script>

<button class="btn btn-circle w-24 h-24 hover:bg-[#00000099]" {title} on:click={onClick}>
    <svelte:component this={Icon} size="100%" />
</button>

Displaying Break Time

Create the file $lib/EstimatedBreakTime.svelte and insert the following:

<script lang="ts">
    import { running, settings } from './stores';
    import { WatchIcon } from 'svelte-feather-icons';

    $: breakTime = Math.round($running / $settings.breakRatio / 1000 / 60);
</script>

<span title="Est. Break Time" class="text-4xl font-bold text-center">
    <WatchIcon class="mx-auto my-1" size="50" />
    {breakTime} min
</span>

Counting Interruptions

Now, in InterruptionsCounter.svelte, we will work with our interruptions store.

<script lang="ts">
    import { interruptions } from './stores';
    import { HashIcon, PlusCircleIcon, MinusCircleIcon } from 'svelte-feather-icons';

    const btnClassName = 'btn btn-circle btn-ghost hidden group-hover:block absolute';
</script>

<div class="group relative flex flex-row items-center justify-between px-20 py-12">
    <button class={`${btnClassName} left-3`} on:click={() => ($interruptions += 1)}
        ><PlusCircleIcon size="50" /></button
    >
    <span title="Interruptions" class="text-4xl font-bold text-center">
        <HashIcon class="mx-auto my-1" size="50" />
        {$interruptions}
    </span>
    <button
        class={`${btnClassName} right-3`}
        on:click={() => ($interruptions = Math.max($interruptions - 1, 0))}
        ><MinusCircleIcon size="50" /></button
    >
</div>

Re-usable Modal Component

Remember sections 4, 6, and 7 of the application? The tasks display, the stats display, and the options display, respectively?

Well, they will all display modal dialogues. So, we can create a component to reuse across the app.

Create a new file $lib/DisplayModal.svelte and add the following code:

<script lang="ts" context="module">
    let locked = false;
</script>

<script lang="ts">
    import { onMount } from 'svelte';

    // When `triggerKey` is pressed, `isOpen` will be set to true
    export let triggerKey: string;
    export let isOpen: boolean;
    export let title: string;

    const showModal = (show: boolean) => {
        isOpen = show;
        locked = show;
    };

    onMount(() => {
        const keyHandler = (event: KeyboardEvent) => {
            if (!locked && event.key === triggerKey) {
                showModal(!isOpen);
            }
        };

        window.addEventListener('keypress', keyHandler);

        return () => {
            window.removeEventListener('keypress', keyHandler);
        };
    });
</script>

<dialog class="modal" class:modal-open={isOpen}>
    <div class="modal-box w-5/6 max-w-4xl">
        <header class="px-8 w-full flex flex-row justify-between items-center">
            <h2 class="font-bold text-4xl">{title}</h2>
            <slot name="header" />
        </header>
        <div class="flex flex-col w-full px-12 my-8 space-y-8">
            <slot name="body" />
        </div>
        <div class="modal-action">
            <button class="btn" on:click={() => showModal(false)}>Close</button>
        </div>
    </div>
</dialog>

In Svelte, a script element with context="module" will only run once. No matter how many component instances there are. So, that makes it the best place to put our locked variable. This is to stop other modal instances from opening if one is already open.

Managing Tasks

Now in a new $lib/TasksDisplay.svelte, we can use our modal component, bound to the "t" key. We also include a button to create a new task, which updates the tasks store.

<script lang="ts">
    import { PlusCircleIcon } from 'svelte-feather-icons';
    import { tasks, state, States } from './stores';
    import TaskDisplay from './TaskDisplay.svelte';
    import DisplayModal from './DisplayModal.svelte';

    let isOpen = false;

    const createTask = () => {
        $tasks = [
            ...$tasks,
            {
                id: crypto.randomUUID(),
                checked: false,
                name: '',
                stats: []
            }
        ];
    };
</script>

<DisplayModal triggerKey="t" bind:isOpen title="Tasks">
    <button
        slot="header"
        class="btn btn-wide h-12 py-1"
        disabled={$state !== States.UNSET}
        on:click={createTask}><PlusCircleIcon size="100%" /></button
    >
    <svelte:fragment slot="body">
        {#each $tasks as task, index (task.id)}
            <TaskDisplay {task} {index} />
        {:else}
            <p class="text-lg">No tasks created yet. Add one!</p>
        {/each}
    </svelte:fragment>
</DisplayModal>

Checking Off a Task

Now we can create the individual TaskDisplay components, in $lib/TaskDisplay.svelte.

Let's start with the script:

<script lang="ts">
    import { tasks, selectedTask, type Task, state, States } from './stores';
    import { TrashIcon } from 'svelte-feather-icons';
    import { onMount } from 'svelte';

    export let task: Task;
    export let index: number;

    let isEditing = false;

    // ...

    $: isSelected = $selectedTask === task.id;
</script>

And for the markup, we can begin with the checkbox.

<div
    class:outline={isSelected}
    class="outline-4 flex flex-row justify-start items-center bg-stone-800 px-5 py-8 rounded-xl text-xl"
>
    <input
        type="checkbox"
        bind:checked={task.checked}
        on:change={onUpdate}
        class="checkbox checkbox-lg"
    />

    <!-- More Svelte here soon -->
</div>

In onUpdate, we update the tasks store with the new version of the task.

const onUpdate = () => {
    const taskIndex = $tasks.findIndex((item) => item.id === task.id);
    if (taskIndex === -1) return;

    $tasks[taskIndex] = task;
    $tasks = $tasks;
    isEditing = false;
};

Editing Tasks

In this component, we can display the task's name either through a text input or a regular text display. We use isEditing to dictate that.

<div class="ml-6 flex-1">
    {#if isEditing}
        <input
            type="text"
            placeholder="Enter task name"
            class="input input-bordered w-full text-lg"
            bind:value={task.name}
            disabled={$state !== States.UNSET}
            on:keypress|capture={(event) => {
                if (event.key === 'Enter') {
                    onUpdate();
                }
                event.stopPropagation();
            }}
        />
    {:else}
        <button
            class="h-max btn btn-ghost w-full normal-case text-lg"
            class:line-through={task.checked}
            on:click={() => {
                if ($state === States.UNSET) isEditing = true;
            }}>&nbsp;{task.name}&nbsp;</button
        >
    {/if}
</div>

Notice the event.stopPropagation. This stops the Space bar from being handled by our Stopwatch component. That is also why we use a capturing event.

Note that we are waiting for the user to press "Enter" to update the tasks. But it doesn't have to stop there. We can also update once the input element loses focus aka "click outside".

For this, we will need a reference to the input element. We get this by adding bind:this={inputElement} to the input element. This also means we must add let inputElement: HTMLInputelement; to the component's script.

So, if the user clicks anywhere except the input element, we can call onUpdate:

onMount(() => {
    const handleClick = (event: MouseEvent) => {
        // On Click Outside
        if (inputElement && !inputElement.contains(event.target) && !event.defaultPrevented) {
            onUpdate();
        }
    };

    window.addEventListener('click', handleClick, true);

    return () => {
        window.removeEventListener('click', handleClick, true);
    };
});

Selecting a Task

<button disabled={$state !== States.UNSET} on:click={onSelect} class="btn btn-ghost mx-4"
    >{!isSelected ? 'Select' : 'Deselect'}</button
>

Deleting a Task

Define a function named onDelete that will filter out the task from the tasks store:

const onDelete = () => {
    $tasks = $tasks.filter((item) => item.id !== task.id);
};

Now, add the corresponding button, using the code below:

<button
    disabled={$state !== States.UNSET}
    on:click={onDelete}
    class="btn btn-circle w-14 h-14 p-3"><TrashIcon size="100%" /></button
>

Displaying the Current Task

Add the following code to $lib/CurrentTaskText.svelte:

<script lang="ts">
    import { ClipboardIcon } from 'svelte-feather-icons';
    import { tasks, selectedTask } from './stores';

    $: currentTask = $tasks.find((item) => item.id === $selectedTask);
</script>

<div class="flex flex-row justify-center items-center">
    <ClipboardIcon size="64" />
    <p class="ml-4 text-4xl font-mono">
        {currentTask ? currentTask.name : 'No task selected'}
    </p>
</div>

Displaying Stats

Now, in a new $lib/StatsDisplay.svelte, we can create another modal instance, bound to the "s" key:

<script lang="ts">
    import { BarChartIcon } from 'svelte-feather-icons';
    import { tasks } from './stores';
    import DisplayModal from './DisplayModal.svelte';
    import StatDisplay from './StatDisplay.svelte';

    let isOpen = false;
</script>

<button on:click={() => (isOpen = true)} class="w-14 h-14 btn btn-circle btn-ghost p-2"
    ><BarChartIcon size="100%" /></button
>
<DisplayModal triggerKey="s" bind:isOpen title="Stats">
    <svelte:fragment slot="body">
        {#each $tasks as task (task.id)}
            <StatDisplay {task} />
        {:else}
            <p class="text-lg">No data. Do some work!</p>
        {/each}
    </svelte:fragment>
</DisplayModal>

Formatting Seconds

Then, in the individual StatDisplay.svelte components, we can output the stats for a certain task:

<script lang="ts">
    import type Task from './stores';
    import { HashIcon, WatchIcon, BriefcaseIcon, CalendarIcon } from 'svelte-feather-icons';

    export let task: Task;

    const formatDuration = (num: number) => {
        const seconds = num;
        const minutes = Math.floor(seconds / 60);
        const hours = Math.floor(minutes / 60);
        return `${hours} hr, ${minutes % 60} min, ${seconds % 60} sec`;
    };
</script>

<div class="collapse collapse-arrow border border-base-200">
    <input type="checkbox" />
    <div class:line-through={task.checked} class="font-mono text-xl collapse-title">
        &nbsp;{task.name}&nbsp;
    </div>
    <div class="collapse-content space-y-4">
        {#each task.stats as stat}
            <div class="flex flex-row justify-around items-center">
                <div title="Work Time" class="flex flex-col justify-center items-center">
                    <BriefcaseIcon size="30" />
                    {formatDuration(stat.workTime)}
                </div>
                <div title="Break Time" class="flex flex-col justify-center items-center">
                    <WatchIcon size="30" />
                    {formatDuration(stat.breakTime)}
                </div>

                <div title="Interruptions" class="flex flex-col justify-center items-center">
                    <HashIcon size="30" />
                    {stat.interruptions}
                </div>
                <div title="Started at" class="flex flex-col justify-center items-center">
                    <CalendarIcon size="30" />
                    {new Date(stat.timeStartedAt).toLocaleString()}
                </div>
            </div>
        {/each}
    </div>
</div>

Managing Options

Add the following code to $lib/OptionsDisplay.svelte :

<script lang="ts">
    import DisplayModal from './DisplayModal.svelte';
    import { SlidersIcon, PlayCircleIcon } from 'svelte-feather-icons';
    import { settings } from './stores';
    import { sounds, playSound } from './sound';

    let isOpen = false;
</script>

<button on:click={() => (isOpen = true)} class="w-14 h-14 btn btn-circle btn-ghost p-2"
    ><SlidersIcon size="100%" /></button
>
<DisplayModal triggerKey="o" bind:isOpen title="Options">
    <svelte:fragment slot="body">
        <div class="flex flex-row justify-around items-center">
            <div class="flex flex-col items-center">
                <p class="font-mono text-2xl">Break Ratio</p>
                <p class="text-sm">For every <i>n</i> minutes of work, take a 1 minute break</p>
            </div>
            <div class="flex flex-col items-center space-y-2">
                <p class="font-bold text-2xl">{$settings.breakRatio}</p>
                <input type="range" min={3} max={5} bind:value={$settings.breakRatio} class="range" />
            </div>
        </div>
        <div class="flex flex-row justify-around items-center">
            <p class="font-mono text-2xl">Timer Sound</p>
            <select bind:value={$settings.timerSound} class="select select-lg">
                {#each Object.keys(sounds) as sound}
                    <option value={sound} class="text-xl">{sound}</option>
                {/each}
            </select>
        </div>
    </svelte:fragment>
</DisplayModal>

Adding Sound

Let's make some noise. You can find some audio files here, which I downloaded from freesound.org.

In $lib/sound.ts, we can first create an object for the sounds that we import, like so:

import bell from "./assets/bell.mp3";
import ring from "./assets/ring.wav";
import scifiBeep from "./assets/scifi-beep.wav";

export const sounds: { [key: string]: string } = {
    bell,
    ring,
    scifiBeep
};

Then, we can create a function named playSound that will play a specified sound, using HTML5 Audio.

But to avoid triggering the same sound repeatedly, instead of creating a new Audio instance each time, we can reuse a top-level <audio> element.

export const playSound = (sound: string) => {
    if (!Object.keys(sounds).includes(sound)) return;

    const audio: HTMLAudioElement | null = document.getElementById("audio-player") as HTMLAudioElement;
    if (!audio) return;

    audio.src = sounds[sound];

    audio.addEventListener("canplaythrough", () => {
        audio.play();
    });
}

So, our #audio-player can be added to routes/+page.svelte:

<audio id="audio-player" />

Updating the Main Page

Listing Keyboard Shortcuts

Add the following code to $lib/KeyboardHelpText.svelte:

<script lang="ts">
    import { state, States } from './stores';
</script>

<p>
    Press <kbd class="kbd bg-stone-600">Space</kbd> to {$state === States.UNSET
        ? 'start'
        : 'take a break'}
</p>

<p>
    Press <kbd class="kbd bg-stone-600">t</kbd> to view tasks
</p>
<p>
    Press <kbd class="kbd bg-stone-600">s</kbd> to view stats
</p>
<p>
    Press <kbd class="kbd bg-stone-600">o</kbd> to view options
</p>
<p>Press <kbd class="kbd bg-stone-600">i</kbd> to view info</p>

Displaying a Break Message

<p>
    Relax! Go for a walk! Stretch! Close your eyes! <br /> But DON'T scroll social media! And DON'T
    play video games!
</p>

Now we can tie these both together in routes/+page.svelte. And make sure to import { state, States } from '$lib/stores'; once you're there:

<div class="text-xl text-center font-mono mt-8 space-y-4">
    {#if $state !== States.BREAK}
        <KeyboardHelpText />
    {:else}
        <p>
            Relax! Go for a walk! Stretch! Close your eyes! <br /> But DON'T scroll social media! And DON'T
            play video games!
        </p>
    {/if}
</div>

Conclusion

And that's all!

With SvelteKit by our side, we built the ultimate solution for focus/time management.

The code for this article is on GitHub.

If you liked this post, stay tuned for more.

References

SvelteKit | Frameworks | Vite PWA (vite-pwa-org.netlify.app)

SvelteKit • Web development, streamlined

The Technique Better than Pomodoro - Flowmodoro - YouTube

Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.

dylanblokhuis/svelte-feather-icons (github.com)

Using Local Storage with Svelte Stores in SvelteKit | Rodney Lab

How to Take Effective Breaks (And Maximize Your Productivity) (knowadays.com)

daisyUI — Tailwind CSS Components

https://feathericons.com/

Did you find this article valuable?

Support Wool Doughnut by becoming a sponsor. Any amount is appreciated!