Building a local running tracker
Tags: react, showdev
Published: 2021-01-27 17:46:02

My local runners' club has a four-month-long challenge to run 100 miles. They provided a PDF of a 10x10 checkbox grid, instructing runners to print it and check off each mile completed. My reaction was something akin to the brat in Back to the Future 2:

!["You mean you have to use your hands?"](https://dev-to-uploads.s3.amazonaws.com/i/z3mqwclysqaomr0prv4s.jpg)

I thought to myself, *Self, we should build an interactive version of this form.* So I did. The final version is available [here](https://100-miles-in-100-days.netlify.app/). It's written in React, the source is available on [GitHub](https://github.com/peiche/100-miles-in-100-days), and deployed automatically by [Netlify](https://app.netlify.com/sites/100-miles-in-100-days/deploys).

## Building the app

I used `create-react-app` to build the scaffolding for this webapp. It's the first time I've done so, but since I've built a couple sites with Gatsby (which is itself a React project), I felt comfortable enough.

### CSS framework

It's pretty typical for modern projects to either use Bootstrap or Tailwind. The former is pretty drop-in ready, while the latter requires a little more tooling. I myself have preferred the CodyFrame library for some time. Its grid system is as easy to use as Bootstrap's, so I customized it to be 10 columns wide (instead of the default 12).

### 100 checkboxes

Sure, I could have copy-and-pasted 100 grid columns and checkboxes. But one of the advantages of rendering the whole app inside JavaScript is the ability to iterate and render dynamically (without the need for a backend server, anyway). So that's what I did:

```jsx
{Array.from(Array(100), (e, i) => {return (
  // simplified HTML 😉
  <input type="checkbox" id={`day-${i + 1}`} />
)})}
```

### Saving data

It was important to me that this app wouldn't save any data, while at the same time allowing the user to keep track of their progress. To accomplish that, data would only be saved locally -- no data would be stored outside of local storage. In JavaScript, that's as simple as:

```js
localStorage.setItem(key, value);
```

In order to minimize the number of calls to directly modify local storage, the name and checkboxes' change events modify the state. This is also done to make sure that all properties are stored in local storage as JSON. The `componentDidMount` and `componentDidUpdate` functions are then responsible both for getting from and setting to local storage, as well as parsing and stringifying (it is too a word) JSON formatting. For example, here's all the functionality for the name textbox (for simplicity's sake):

```jsx
componentDidMount() {
  const nameJson = localStorage.getItem('name');
  if (nameJson) {
    const name = JSON.parse(nameJson);
    this.setState(() => ({ name }));
  }
}

componentDidUpdate(prevProps, prevState) {
  if (prevState.name !== this.state.name) {
    const name = this.state.name;
    localStorage.setItem('name', JSON.stringify(name));
  }
}

<input onChange={event => this.setState({ name: event.target.value })} />
```

### Exporting a DOM node to an image

Something I thought would be pretty cool is the ability to generate (and download) an image of one's progress. I've dabbled a bit with generating images with Java on the server side, but never in a client-side app. My searching led me to find [dom-to-image](https://github.com/tsayen/dom-to-image), which has options for exporting to a JPEG, a PNG, or even a blob (not the killer kind; a "blob" is [raw file data](https://developer.mozilla.org/en-US/docs/Web/API/Blob), which can be read or processed as you wish). Combined with another library to [make saving files easier](https://github.com/eligrey/FileSaver.js/), exporting the image is done easily:

``` js
domtoimage.toBlob(document.getElementById('main'), {
  bgcolor: '#ffffff',
}).then(function (blob) {
  saveAs(blob, '100-miles.png');
});
```

Trial and error taught me that I had to manually set the background color of the image. I also found, per dom-to-image's documentation, that Safari is not supported. (This also means iPhones.) This particular issue might prevent a one-click image download, but it doesn't prevent the app's primary intent from being used. Being responsive, it's usable on any device -- and any user can take a screenshot, anyway.

## Deploying the app

I wrote this with the intent of it being a solely static, client-side app, so deploying to Netlify made the most sense to me. They have a wonderful, easy process for deploying any git site. Since I put the source on GitHub, the entire process couldn't have been easier.

## The final product!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/k407zvjlxzrxnuri012a.png)