Svelte Work:Rest Timer – closingtags </>
Categories
CSS HTML5 Javascript Svelte

Svelte Work:Rest Timer

I was recently approached by a friend to build an app. Now, I’ve been cornered and pitched far too many half-brained apps in my day but it is refreshing to hear a good pitch. This pitch was for a timer, but not just any timer or stopwatch app that comes preinstalled on your phone. Rather, it was a workout timer that would also time your rests. As someone who often takes too long to rest between sets, I thought this sounded like a decent idea. I was told “nothing like this exists!” but a quick search of the web proves that false; it’s definitely been done. But I could do it as a Svelte component! And it’d be a fun coding challenge!

The Setup

If you’ve read any recent posts of mine, you know I’ve been working with SvelteKit lately. To get started with SvelteKit, I ran a few commands in the project directory:

npm create [email protected] timer
cd timer
npm install
npm run dev

From there, I created the file /src/routes/+page.svelte as the entry point page for the app. I also created the file /src/lib/timer.svelte which is where all of the magic happens and included it in the aforementioned +page.svelte like so:

+page.svelte

<script>
  import Timer from '$lib/timer.svelte';
</script>

<div class='content'>
  <Timer />
</div>

<style>
  div.content {
    text-align: center;
    width: 100%;
    background-color: #130d0d;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    color: #d9d9d7;
  }
</style>

$lib is an alias to /src/lib/ that is supported by SvelteKit out of the box.

The Markup

Most timers have a display of the running time, a start button, a stop button, and reset button so I went ahead and included those right away in /src/lib/timer.svelte. I also gave it some rough styling so it wasn’t awful to look at while testing. The basic structure of the component looks something like this:

<script>...</script>

<h1>Work+Rest Timer</h1>

<div class='time'>{hr}:{min}:{s}.{ms}</div>

<div class='controls'>
  <button class='start' on:click={start}>{running ? `rest` : `start`}</button>
  <button class='stop' on:click={stop}>stop</button>
  <button class='reset' on:click={reset}>reset</button>
</div>

<style>
  h1 {
    text-align: center;
  }
  .time {
    margin: 1.5rem;
    font-size: 4rem;
  }
  .controls {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr 1fr;
    grid-template-rows: 1fr 1fr;
  }
  button {
    padding: 1.1rem 1.4rem;
    margin: 1.1rem;
    font-size: 1.5rem;
    border: none;
    background-color: #496bf0;
    color: white;
    transition: all .4s ease-in-out;
    cursor: pointer;
    grid-column: 3 / 4;
    grid-row: 2 / 3;
  }
  button:hover {
    background-color: #2047df;
  }
  button.stop {
    background-color: #df3d2c;
    grid-column: 2 / 3;
    grid-row: 2 / 3;
  }
  button.stop:hover {
    background-color: #a81303;
  }
  button.start {
    background-color: #408e2a;
    grid-column: 2 / 4;
    grid-row: 1 / 2;
  }
  button.start:hover {
    background-color: #245e14;
  }
</style>
A stop watch style timer with the title text "Work plus Rest timer" showing a timer with all zeros, a green start button, a red stop button, and a blue reset button.
Yup, that’s a timer alright.

Eagle eyed readers likely noticed that there are some variables used in the markup that were not declared in the script tag. Lets get to that part now.

The Logic

This timer needs to be able to start, stop, reset, and display the running time. It also needs a “rest” feature which counts down to zero from whenever the “rest” button was clicked. To prevent users from starting another timer, I decided to turn the start button into the rest button while the timer is running. A quick rundown of how this ought to work:

  • clicking “start” starts the timer (duh)
  • the time since start is displayed in hours, minutes, seconds, and milliseconds
  • after clicking “start,” the “start” button becomes the “rest” button
  • clicking the “rest” button will reverse the time, counting down to zero
  • when the timer reaches zero, it will start counting up again until stopped or until “rest” is clicked again
  • clicking “stop” will stop the time
  • clicking reset will set hours, minutes, seconds, and milliseconds back to zero

That all seems fairly straightforward, right? Oh, but dear reader, you don’t know this (how could you have?) but when it comes to code, I hate programming anything related to time. And I’m really bad at math.

So why did I think this was a good idea? 🤔 Who knows? Maybe I just hate myself.

Side note: When working with computers and time, there’s all sorts of problems that can arise. It’s a lot easier to do math with time when you convert time to a regular number.

About those missing variables…

<script>
  let interval = null;
  let running = false;
  let elapsed = 0;
  let oldElapsed = 0;

  $: ms = pad3(0);
  $: s = pad2(0);
  $: min = pad2(0);
  $: hr = pad2(0);

  // pad2/3 exist to format the numbers with leading zeros
  const pad2 = (number) => `00${number}`.slice(-2);
  const pad3 = (number) => `000${number}`.slice(-3);

  const start = () => {
    ...
  }
  const stop = () => {
    ...
  }
  const reset = () => {
    ...
  }
</script>

You’ll notice some that weren’t mentioned the markup; you can ignore those for now. The times (hr, min, s, ms) are all declared as reactive because I want the timer display to update as the those values change. Now all of this keeps the component from throwing errors but it doesn’t do anything. To make it do the things, let’s look at start() first.

  const start = () => {
    if(!running) {
      countUp();
    }
    else {
      countDown();
    }
  }

There! That was easy. Lets move on to the stop() and reset() functions:

  const stop = () => {
    clearInterval(interval);
    oldElapsed = elapsed;
  }
  const reset = () => {
    s = min = hr = pad2(0);
    ms = pad3(0);
    elapsed = oldElapsed = 0;
    running = false;
    clearInterval(interval);
  }

Wait a minute, this doesn’t make any sense without looking at what’s actually going on in start(). Let’s back up. Here’s countUp() which is called by start() when running is false.

  const countUp = () => {
    let startTime = Date.now();
    running = true;

    interval = setInterval(() => {
      elapsed = Date.now() - startTime + oldElapsed;
      
      ms = pad3(elapsed);
      s = pad2(Math.floor(elapsed / 1000) % 60);
      min = pad2(Math.floor(elapsed / 60000) % 60);
      hr = pad2(Math.floor(elapsed / 3600000) % 60);
    });
  }

The secret sauce here is setInterval(). Since it has not been provided the optional second argument, it loops on the function passed to it every millisecond. In that loop, the time since the counter began counting upwards is calculated by subtracting the start time from the current time. Then, each “section” of the timer display is calculated based on that elapsed time (and padded with extra leading zeros). Since the interval is assigned to the variable interval, I can clear it later on in stop() and reset() to stop the code.

Now for the part that I spent way too long on; countDown().

  const countDown = () => {
    stop();
    const end = Date.now() + elapsed;
    
    interval = setInterval(() => {
      elapsed = end - Date.now();
      
      ms = pad3(elapsed);
      s = pad2(Math.floor(elapsed / 1000) % 60);
      min = pad2(Math.floor(elapsed / 60000) % 60);
      hr = pad2(Math.floor(elapsed / 3600000) % 60);

      if(elapsed <= 0) {
        clearInterval(interval);
        reset();
        countUp();
      }
    });
  }

The first thing I want to do here is stop the timer where it’s at. That way, I can clear the value inside the interval variable. Once that’s done, the end time of the new timer needs to be calculated. That’s done by taking the amount of time elapsed and adding it to the current time. Once the end time has been calculated, the loop that runs every millisecond begins, where the amount of time elapsed is recalculated by subtracting the current time from the end time. It is then all shown in the timer display, and when the amount of elapsed time reaches zero, the interval is cleared, values are all reset to zero, and the count back up begins again.

If you were confused during any of that, that’s fine. You’re probably a sane, well adjusted human being that gets along perfectly well in social interactions. Congratulations 🥳!

All Together Now!

<script>
  let interval = null;
  let running = false;
  let elapsed = 0;
  let oldElapsed = 0;

  $: ms = pad3(0);
  $: s = pad2(0);
  $: min = pad2(0);
  $: hr = pad2(0);

  const pad2 = (number) => `00${number}`.slice(-2);
  const pad3 = (number) => `000${number}`.slice(-3);

  const countUp = () => {
    let startTime = Date.now();
    running = true;

    interval = setInterval(() => {
      elapsed = Date.now() - startTime + oldElapsed;
      
      ms = pad3(elapsed);
      s = pad2(Math.floor(elapsed / 1000) % 60);
      min = pad2(Math.floor(elapsed / 60000) % 60);
      hr = pad2(Math.floor(elapsed / 3600000) % 24);
    });
  }

  const countDown = () => {
    stop();
    const end = Date.now() + elapsed;
    
    interval = setInterval(() => {
      elapsed = end - Date.now();
      
      ms = pad3(elapsed);
      s = pad2(Math.floor(elapsed / 1000) % 60);
      min = pad2(Math.floor(elapsed / 60000) % 60);
      hr = pad2(Math.floor(elapsed / 3600000) % 24);

      if(elapsed <= 0) {
        clearInterval(interval);
        reset();
        countUp();
      }
    });
  }

  const start = () => {
    if(!running) {
      countUp();
    }
    else {
      countDown();
    }
  }
  const stop = () => {
    clearInterval(interval);
    oldElapsed = elapsed;
  }
  const reset = () => {
    s = min = hr = pad2(0);
    ms = pad3(0);
    elapsed = oldElapsed = 0;
    running = false;
    clearInterval(interval);
  }
</script>

I put together a repository of this code so you can read it in all of its glory there. If you follow the development, you’ll see new features like “beeps”, fun colors, and buttons that allow you to multiply the rest time by a factor of 1.5, 2, or even 3! If you like tracking calories as well as working out, then keep a watchful eye out for this new feature coming to your favorite health tracking application.

By Dylan Hildenbrand

Full stack web developer experienced with #PHP, #SvelteKit, #JS, #NodeJS, #Linux, and #Ansible. I love a good challenge.

profile for Dylan Hildenbrand on Stack Exchange, a network of free, community-driven Q&A sites  

Do you like these posts? Consider sponsoring me or making a donation!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.