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.
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
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
I know I want a form, so I’ll build a Form with styled.
const Form = styled.form`
width: 100%;
`
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;
`
Now, in the render method, render that form and textarea.
<Form>
<TextField/>
</Form>
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/>
)
}
Here is our page so far.
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
}
}
Then, in the render, write a ternary to render the form if it is true.
{this.state.form ? <ChallengeForm/>: null}
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;
}
`
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>
)
}
Finally, change the state so that the form toggles between true and false.
this.setState(prevState => ({
sentences: [...prevState.sentences, sentence],
}))
}
Now we can show and hide the form at the click of a button.
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'
Then, make a Title h3 styled component.
const Title = styled.h3`
padding: 8px;
`
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;
`
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
}
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)
}
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>
And our countdown is complete!
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;
`
We’ll also render that button underneath our form. Let’s also attach an onClick event to it.
<Button onClick={this.analyze}>Analyze!</Button>
Analyze will conditionally render a word and character count, so we’ll need to add a flag to our state.
analyze: false
Upon clicking the analyze button, we’ll set the state to true.
analyze = () => {
this.setState({
analyze: true
})
}
In order to count the words, we’ll need to make them part of our state.
words: ''
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;
}
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}
This will tell us how many words were written in our challenge.
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.
Top comments (5)
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!
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
'''
Thanks so much! Excited to try this out.
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).
But it's a good read, great work!
Hi Cyril! Hooks would be cleaner. Thanks for the suggestion.