DEV Community

Cover image for Build a Countdown Timer for Writing
Kailana Kahawaii
Kailana Kahawaii

Posted on

Build a Countdown Timer for Writing

Nanowrimo has started, but it’s easy to lose motivation. This countdown tool will put a figurative fire under our fingertips and hopefully inspire you to write a tiny draft to get started with that momentous writing project.

Finished project with timer and writing form

Getting Started

In my last article, we built a random plot generator to generate a random plot. But we didn’t actually create a place for us to write about that scenario. In order to challenge ourselves, we’re going to create a timed challenge component which will allow us to get our initial ideas on the page.

I’ll be using the Random Plot Generator component, so go ahead and read that article if you’d like to follow along.

First, create a component that will render the form we’ll use to write on.

This form will hold information in state, so we’ll make it a class component.

import React from react 

class ChallengeForm extends React.Component{
    state={}

    render(){
        return(
                   <div>form goes here</div>
        )
    }

}

export default ChallengeForm 

Enter fullscreen mode Exit fullscreen mode

In order to keep the styling consistent, I’ll use the styled components library. If you haven’t already, install the styled library.

npm install --save styled-components
Enter fullscreen mode Exit fullscreen mode

I know I want a form, so I’ll build a Form with styled.

const Form = styled.form`
    width: 100%;
`
Enter fullscreen mode Exit fullscreen mode

We’ll also need to build a text field for us to actually write something.

Here’s the styling I used. Note that styled components should be OUTSIDE of the class declaration.

const TextField = styled.textarea`
    display: block;
    border: 1px solid lightgrey;
    border-radius: 2px;
    width: 750px;
    height: 500px;
    padding: 8px;
    margin: 8px;
`

Enter fullscreen mode Exit fullscreen mode

Now, in the render method, render that form and textarea.


<Form>
    <TextField/>
</Form>

Enter fullscreen mode Exit fullscreen mode

Of course, we still can’t see the form. In our Random Plot Generator Component we need to import the Challenge Form component and render it.

Rendering the Form

We’ll work in our Random Plot Generator component for this next part. Refer to the article to get a feel for how it’s set up.

import ChallengeForm from './ChallengeForm'

[]

render(){
    return(
        []
        <ChallengeForm/>
        )
    }
Enter fullscreen mode Exit fullscreen mode

Here is our page so far.

Shows the text area underneath the randomly generated sentence

Conditionally Render the Challenge

We can start writing ideas now, but there is a reason we called it the Challenge Form. To create the challenge aspect, we'll first need to conditionally render this form.

We’ll change the render in our Random Plot generator to conditionally render the form.

First, let’s add the form flag to our state.

class RandomPlotGenerator extends React.Component{
    constructor(props){
        super(props)
        this.state = {
            []
            form: false
        }
    }
Enter fullscreen mode Exit fullscreen mode

Then, in the render, write a ternary to render the form if it is true.

{this.state.form ? <ChallengeForm/>: null}
Enter fullscreen mode Exit fullscreen mode

In order to make it true, we’ll need to write a button. Let’s add the button to the sentence that’s generated.

Create a new styled button.

const Button = styled.button`
    margin: 8px; 
    padding; 8px;

    &:hover {
        color: blue;
        cursor: pointer;
      }
`
Enter fullscreen mode Exit fullscreen mode

Then, render that button in the renderRandomPlot function.

    renderRandomPlot = () => {
        return this.state.sentences.map((sentence, idx) => 
             <Container key={idx}>
                 {sentence}
                 <Button onClick={this.onChallengeClick}>Challenge</Button>
             </Container>
        )
    }
Enter fullscreen mode Exit fullscreen mode

Finally, change the state so that the form toggles between true and false.

  this.setState(prevState => ({
            sentences: [...prevState.sentences, sentence],
        }))
        }


Enter fullscreen mode Exit fullscreen mode

Now we can show and hide the form at the click of a button.

Gif depicting the toggling of the form

Now that the form is conditionally rendered, let’s make a timer to count the time we have to write.

Building Countdown Functionality

We’ll want to make a header to tell us how much time we have left. It would also be nice if, when we run out of time, the header blinks to let us know.

Styling the Countdown Headers

To do this, we need to import keyframes from the styled library.

Do this in the Challenge Form component.

import styled, { keyframes } from 'styled-components'
Enter fullscreen mode Exit fullscreen mode

Then, make a Title h3 styled component.

const Title = styled.h3`
    padding: 8px; 
`
Enter fullscreen mode Exit fullscreen mode

We’ll also write a function for our component to blink.

function blink() {
    return keyframes`
      50% {
        opacity: 0;
      }
    `;
  }

const TimesUp = styled.text`
    color: red;
    animation: ${blink} 1s linear infinite;
`
Enter fullscreen mode Exit fullscreen mode

Both this styled component and the function are outside of our Challenge Form class.

Keep Track of Time in State

Before rendering the title, we’ll add minutes and seconds to our state.

state = {
    minutes: 5, 
    seconds: 0
    }
Enter fullscreen mode Exit fullscreen mode

We’ll use Set Interval to count down the seconds.

I used Charlie Russo’s Building a Simple Countdown Timer With React to build out this functionality. Check it out!

In the Component Did Mount lifecycle method, use this code to create the timer.

    componentDidMount() {
        this.myInterval = setInterval(() => {
          const { seconds, minutes } = this.state    
          if (seconds > 0) {
            this.setState(({ seconds }) => ({
              seconds: seconds - 1
            }))
          }
          if (seconds === 0) {
            if (minutes === 0) {
              clearInterval(this.myInterval)
            } else {
              this.setState(({ minutes }) => ({
                minutes: minutes - 1,
                seconds: 59
              }))
            }
          }
        }, 1000)
      }

    componentWillUnmount() {
        clearInterval(this.myInterval)
    }
Enter fullscreen mode Exit fullscreen mode

Conditionally Render the Countdown

Finally, render the countdown timer component. When the timer hits zero, our timer will blink to let us know time’s up.

   <Title>   
                    { minutes === 0 && seconds === 0
                        ? <TimesUp>Time's Up!</TimesUp>
                        : <h1>Time Remaining: {minutes}:{seconds < 10 ? `0${seconds}` : seconds}</h1>
                    }</Title>
Enter fullscreen mode Exit fullscreen mode

And our countdown is complete!

When the countdown hits zero, the title flashes

We can be mean and make it impossible to update the form after that, but that probably isn’t going to fly with many writers. Instead, let’s add an analysis tool that tells us how many words and characters we typed in that time period.

Building an Analysis Button

We’ll create a simple button style for our analysis button.

const Button = styled.button`
    padding: 8px; 
    margin: 8px;
`
Enter fullscreen mode Exit fullscreen mode

We’ll also render that button underneath our form. Let’s also attach an onClick event to it.

<Button onClick={this.analyze}>Analyze!</Button>
Enter fullscreen mode Exit fullscreen mode

Analyze will conditionally render a word and character count, so we’ll need to add a flag to our state.

 analyze: false
Enter fullscreen mode Exit fullscreen mode

Upon clicking the analyze button, we’ll set the state to true.

 analyze = () => {
        this.setState({
            analyze: true
        })
    }
Enter fullscreen mode Exit fullscreen mode

In order to count the words, we’ll need to make them part of our state.

words: ''
Enter fullscreen mode Exit fullscreen mode

Our count words function is a simple regex expression that counts words and returns only alpha-numeric characters.

  countWords = () => {
        let str = this.state.words
        const matches = str.match(/[\w\d\’\'-]+/gi);
        return matches ? matches.length : 0;
    }

Enter fullscreen mode Exit fullscreen mode

Finally, we’ll tie the analyze function to the button and conditionally render the word and character count.

<Button onClick={this.analyze}>Analyze!</Button>
                    {this.state.analyze ? <p>{`You wrote ${this.countWords()} words and ${this.state.words.length} characters.`}</p> : null}
Enter fullscreen mode Exit fullscreen mode

This will tell us how many words were written in our challenge.

Shows a word and character count

Summary

Great! We created a timer and text area to challenge ourselves to write a short story. We also created the ability to analyze the amount of words written in that time period.

There are many ways this challenge can be expanded upon. What about creating multiple plot ideas and multiple challenge forms? Or creating a backend for saving our writing projects so we can come back to them later.

Discussion (5)

Collapse
indoor_keith profile image
Keith Charles

I LOVE this! I'm not participating in NaNoWriMo this year, but man I'm going to build one of these for myself to use for next year. Thank you for the idea and explaining your process so well!

Collapse
indoor_keith profile image
Keith Charles • Edited

Quick note about syntax highlighting! If you add "js" just after your first set of backticks, on the same line, you'll get some nice syntax highlighting cooresponding to the language you specify. It would look like this (replacing the single quotes with backticks):

'''js
// your code here
'''

Collapse
kahawaiikailana profile image
Kailana Kahawaii Author

Thanks so much! Excited to try this out.

Collapse
cyrilcabo profile image
Cyril Cabo • Edited

I think setTimeout is a better way of implementing the countdown. Such is the reason that you don't need to have a clean-up function, and a much simpler code (see example in the photo).

Example code

But it's a good read, great work!

Collapse
kahawaiikailana profile image
Kailana Kahawaii Author

Hi Cyril! Hooks would be cleaner. Thanks for the suggestion.