Paul
Posted on January 27, 2021
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:
I thought to myself, Self, we should build an interactive version of this form. So I did. The final version is available here. It's written in React, the source is available on GitHub, and deployed automatically by Netlify.
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:
{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:
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):
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, which has options for exporting to a JPEG, a PNG, or even a blob (not the killer kind; a "blob" is raw file data, which can be read or processed as you wish). Combined with another library to make saving files easier, exporting the image is done easily:
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!
Posted on January 27, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.