Create your own Exercise Planner using React.js [Complete Code]

Hey everyone 👋🏻,

In this article, let us see how we can build an Exercise Planner Application using React.js.

Demo for the Project & Initial Setup

So the first thing you need to do is to create a new project.
Make sure to install Node.js onto your system because we will be needing that for our project.

We will be using this tool for creating a new React Project :

Create React App :

avdjjay3se286tfkeaw8-5035059

Install Node.js :xg6c9yjpuxclt8nscyjc-2271002

Create a new React Project

// To create a new React Project 
npx create-react-app <project_name>
cd <project_name>
// To run the project 
npm start

Install React Router DOM package

npm install react-router-dom 

Install JSON-server package globally

We will be using JSON-server for our project.

npm install json-server -g

Create a new file – exercises.json

This is the file that will contain all the exercises that we will be needing in this application. This file will serve as a local JSON store for our application.

store/data.json

{
  "exercises": [
    {
      "title": "Pushups",
      "details": "Pushups are beneficial for building upper body strength. They work the triceps, pectoral muscles, and shoulders. When done with proper form, they can also strengthen the lower back and core by engaging (pulling in) the abdominal muscles",
      "complete": false,
      "id": 119
    }
  ]
}

Inside a new terminal, run the json-server by running the command

json-server -g ./src/store/data.json --watch --port=3111

Complete Code 👨🏻‍💻

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';

ReactDOM.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

App.jsx

import {Switch, Route } from "react-router-dom"
import HomePage from './pages/HomePage';
import './App.css';
import CreateExercise from "./pages/CreateExercise";
import Navbar from "./components/Navbar";
import EditExercise from "./pages/EditExercise";

function App() {
  return (
    <div className="App">
      <Navbar />
      <Switch>
        <Route path="/home" exact>
          <HomePage />
        </Route>
        <Route path="/create-exercise" exact>
          <CreateExercise />
        </Route>
        <Route path="/exercises/:id/edit">
          <EditExercise />
        </Route>
      </Switch>
    </div>
  );
}

export default App;

App.css

.App {
  text-align: center;
}

index.css

html {
  --pink: #D60087;
  --golden: goldenrod;
  --green: rgba(216, 235, 48, 0.83);
  --text-shadow: 2px 2px 0 rgba(0,0,0,0.2);
  font-size: 62.5%;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  font-size: 2rem;
  line-height: 1.5;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.com/svgjs' width='1440' height='560' preserveAspectRatio='none' viewBox='0 0 1440 560'%3e%3cg mask='url(%26quot%3b%23SvgjsMask1000%26quot%3b)' fill='none'%3e%3crect width='1440' height='560' x='0' y='0' fill='%230e2a47'%3e%3c/rect%3e%3cpath d='M136.72 348.61a22.94 22.94 0 1 0 43.22-15.38z' stroke='%23e73635'%3e%3c/path%3e%3cpath d='M511.55 165.45L536.61 165.45L536.61 190.51L511.55 190.51z' stroke='%23037b0b'%3e%3c/path%3e%3cpath d='M483.33 455.66 a20.42 20.42 0 1 0 40.84 0 a20.42 20.42 0 1 0 -40.84 0z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M1228-20.28a48.24 48.24 0 1 0-63.63 72.52z' stroke='%23037b0b'%3e%3c/path%3e%3cpath d='M1392.69 128.01 a29.96 29.96 0 1 0 59.92 0 a29.96 29.96 0 1 0 -59.92 0z' stroke='%23d3b714'%3e%3c/path%3e%3cpath d='M1076.84 554.49L1113.49 554.49L1113.49 591.14L1076.84 591.14z' stroke='%23d3b714'%3e%3c/path%3e%3cpath d='M756.26 7.46L795.56 7.46L795.56 35.82L756.26 35.82z' fill='%23d3b714'%3e%3c/path%3e%3cpath d='M25.99 370.04L26.54 370.04L26.54 423.32L25.99 423.32z' fill='%23d3b714'%3e%3c/path%3e%3cpath d='M905.99 522.28 a27.93 27.93 0 1 0 55.86 0 a27.93 27.93 0 1 0 -55.86 0z' stroke='%23d3b714'%3e%3c/path%3e%3cpath d='M878.82 241.19L928.56 241.19L928.56 290.93L878.82 290.93z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M620.67 145.35L648.93 145.35L648.93 173.61L620.67 173.61z' stroke='%23037b0b'%3e%3c/path%3e%3cpath d='M941.34 228.22a0.43 0.43 0 1 0-0.84 0.22z' stroke='%23e73635'%3e%3c/path%3e%3cpath d='M142.87 85.55 a19.16 19.16 0 1 0 38.32 0 a19.16 19.16 0 1 0 -38.32 0z' stroke='%23037b0b'%3e%3c/path%3e%3cpath d='M329.59 523.81L381.54 523.81L381.54 575.76L329.59 575.76z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M794.97 148.48 a17.04 17.04 0 1 0 34.08 0 a17.04 17.04 0 1 0 -34.08 0z' stroke='%23d3b714'%3e%3c/path%3e%3cpath d='M1144.93 501.44a3.73 3.73 0 1 0-6.24 4.09z' stroke='%23e73635'%3e%3c/path%3e%3cpath d='M1.85 326.79L6.52 326.79L6.52 331.46L1.85 331.46z' fill='%23d3b714'%3e%3c/path%3e%3cpath d='M1161.24 414.19L1165.88 414.19L1165.88 445.98L1161.24 445.98z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M1208.54 452.49a10.52 10.52 0 1 0 10.48 18.25z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M1356.2 405.97a49.06 49.06 0 1 0 59.45-78.06z' stroke='%23037b0b'%3e%3c/path%3e%3cpath d='M569.26 204.09 a47.56 47.56 0 1 0 95.12 0 a47.56 47.56 0 1 0 -95.12 0z' stroke='%23d3b714'%3e%3c/path%3e%3cpath d='M955.59 150.05a43.84 43.84 0 1 0-65.58-58.2z' stroke='%23e73635'%3e%3c/path%3e%3cpath d='M827.49 284.25L864.82 284.25L864.82 321.58L827.49 321.58z' stroke='%23d3b714'%3e%3c/path%3e%3cpath d='M718.92 539.64 a45.27 45.27 0 1 0 90.54 0 a45.27 45.27 0 1 0 -90.54 0z' fill='%23d3b714'%3e%3c/path%3e%3cpath d='M1253.87 200.13L1273.78 200.13L1273.78 220.04L1253.87 220.04z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M365.3 8.63L376.7 8.63L376.7 16.04L365.3 16.04z' stroke='%23e73635'%3e%3c/path%3e%3cpath d='M691.18 292.33a16.5 16.5 0 1 0 0.63-33z' fill='%23037b0b'%3e%3c/path%3e%3cpath d='M224.16 500.66L270.06 500.66L270.06 546.56L224.16 546.56z' fill='%23e73635'%3e%3c/path%3e%3cpath d='M174.91 13.36 a25.6 25.6 0 1 0 51.2 0 a25.6 25.6 0 1 0 -51.2 0z' stroke='%23037b0b'%3e%3c/path%3e%3cpath d='M1396.28 502.2 a16.58 16.58 0 1 0 33.16 0 a16.58 16.58 0 1 0 -33.16 0z' fill='%23037b0b'%3e%3c/path%3e%3cpath d='M1337.35 217.49L1380.42 217.49L1380.42 268.22L1337.35 268.22z' fill='%23d3b714'%3e%3c/path%3e%3cpath d='M429.47 162.98a54.75 54.75 0 1 0 89.11 63.64z' fill='%23d3b714'%3e%3c/path%3e%3cpath d='M56.44 369.3L67.51 369.3L67.51 397.03L56.44 397.03z' stroke='%23e73635'%3e%3c/path%3e%3c/g%3e%3cdefs%3e%3cmask id='SvgjsMask1000'%3e%3crect width='1440' height='560' fill='white'%3e%3c/rect%3e%3c/mask%3e%3c/defs%3e%3c/svg%3e");

}
:focus {
  outline-color: var(--pink);
}

Components

components/BaseFilter.css

 .filter-nav button {
    background: none;
    border: none;
    color: #bbb;
    outline: none;
    font-size: 15px;
    text-transform: uppercase;
    margin-right: 10px;
    letter-spacing: 1px;
    font-weight: bold;
    cursor: pointer;
  }
  .filter-nav button.active {
    color: goldenrod;
  }

components/BaseFilter.jsx

import React from "react";
import './BaseFilter.css';
const BaseFilter = (props) => {

    <nav className="filter-nav">
      <button
        onClick={() => props.onUpdate('all')}
        className={props.current === 'all' ? 'active' : ''}
      >
        View all
      </button>

      <button
        onClick={() => props.onUpdate('completed')}
        className={props.current === 'completed' ? 'active' : ''}
      >
        Completed
      </button>

      <button
        onClick={() => props.onUpdate('pending')}
        className={props.current === 'pending' ? 'active' : ''}
      >
        Pending
      </button>
    </nav>
  );
};

export default BaseFilter;

components/ExerciseItem.jsx

import React from 'react'
import { Link } from 'react-router-dom';

import './ExerciseItem.css';

function ExerciseItem(props) {

  const performExerciseDeletion = () => {

    fetch(`http://localhost:3111/exercises/${props.exercise.id}`, {
      method: 'DELETE',
    })
      .then(() => props.onDeleteExercise(props.exercise.id))
      .catch((err) => console.log(err));

  };

  const performExerciseCompletion = () => {
    fetch(`http://localhost:3111/exercises/${props.exercise.id}`, {
      method: 'PATCH',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ complete: !props.exercise.complete }),
    })
      .then(() => props.onCompleteExercise(props.exercise.id))
      .catch((err) => console.log(err));

  };
  const classes = ['exercise'];
  if (props.exercise.complete) {
    classes.push('complete');
  }
  return (
    <div className={classes.join(' ')}>
      <div className="actions">
        <h4>{props.exercise.title}</h4>
        <div className="buttons">
          <button onClick={performExerciseDeletion}>Delete</button>
          <Link to={`/exercises/${props.exercise.id}/edit`}>Edit</Link>
          <button onClick={performExerciseCompletion}>Toggle</button>
        </div>
      </div>
      <div className="details">
        <p>{props.exercise.details}</p>
      </div>
    </div>
  );
}

export default ExerciseItem;

components/ExerciseItem.css

.exercise {
  margin: 20px auto;
  background: white;
  padding: 10px 20px;
  border-radius: 10px;
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
  border-left: 10px solid var(--pink);
}
.exercise:hover {
  box-shadow: 0 16px 32px 0 rgba(0,0,0,0.9);
}
h3 {
  cursor: pointer;
}
.actions {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.exercise.complete {
  border-left: 10px solid var(--green);
}
.buttons {
  display:flex;
  flex-direction: column;
}
.buttons button,a {
 color: white;
  background: var(--pink);
  padding: 0.5rem;
  border: 0;
  border: 2px solid transparent;
  text-decoration: none;
  font-weight: 600;
  font-size:2rem;
  margin-bottom: 5px;
   border-radius: 6px;

}
.buttons button:hover, a:hover, button:active, a:active {
  background: rgb(225, 127, 143);
}
h4 {
  transform: skew(-21deg);
  background: var(--golden)
}

components/ExercisesList.css

.exercises-list {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  max-width: 600px;
  margin: 0 auto;
  color: #555;
}

components/ExercisesList.jsx

import React from 'react'
import ExerciseItem from './ExerciseItem';
import './ExercisesList.css'

function ExercisesList(props) {

  if (props.exercises.length === 0) return null;

  return (
    <div className="exercises-list">
      {props.exercises.map((exercise) => (
        <ExerciseItem
          key={exercise.id}
          exercise={exercise}
          onCompleteExercise={props.onCompleteExercise}
          onDeleteExercise={props.onDeleteExercise}
        />
      ))}
    </div>
  );

}

export default ExercisesList;

components/Navbar.css

.main-nav {
    text-align: center;
    margin: 40px auto;
  }
  .main-nav a{
    display: inline-block;
    text-decoration: none;
    margin: 0 10px;
    color: goldenrod;
    font-size: 20px;

  }
  .active-style {
    border-bottom: 2px solid goldenrod;
    padding-bottom: 4px;
  }

components/Navbar.jsx

import React from 'react'
import './Navbar.css';
import { NavLink } from "react-router-dom";

function Navbar() {
  return (
     <nav className="main-nav">
      <NavLink activeClassName="active-style" to="/home">
        Home
      </NavLink>
      <NavLink activeClassName="active-style" to="/create-exercise">
        Create Exercise
      </NavLink>
     </nav>
  );
}

export default Navbar

Pages (Views)

Create a folder for pages where we will house all our page components.

pages/HomePage.jsx

import React, { useState, useEffect  } from 'react';
import BaseFilter from '../components/BaseFilter';
import ExercisesList from '../components/ExercisesList';

const HomePage = () => {
  const [exercises, setExercises] = useState([]);
  const [currentFilter, setCurrentFilter] = useState('all');

  const updateFilterHandler = (newFilter) => {
    setCurrentFilter(newFilter);
  };

  const deleteExerciseHandler = function (id) {
    const patchedExercises = exercises.filter((exercise) => exercise.id !== id);
    setExercises(patchedExercises);
  };

  const completeExerciseHandler = function (id) {
    const clonedExercises = [...exercises];
    const currentExerciseIndex = clonedExercises.findIndex(
      (exercise) => exercise.id === id
    );
    const currentExercise = clonedExercises[currentExerciseIndex];
    currentExercise.complete = !currentExercise.complete;
    setExercises(clonedExercises);
  };


  useEffect(() => {
      try {
        const response = await fetch('http://localhost:3111/exercises');
        const fetchedExercises = await response.json();
        console.log(fetchedExercises);
        setExercises(fetchedExercises);

      } catch (error) {

        console.log(error);
      }
    }

    fetchExercises();

  }, []);


  let jsx = (
    <ExercisesList
      exercises={exercises}
      onCompleteExercise={completeExerciseHandler}
      onDeleteExercise={deleteExerciseHandler}
    />
  );
  if (currentFilter === 'completed') {
    jsx = (
      <ExercisesList
        exercises={exercises.filter((exercise) => exercise.complete)}
        onCompleteExercise={completeExerciseHandler}
        onDeleteExercise={deleteExerciseHandler}
      />
    );
  } else if (currentFilter === 'pending') {
    jsx = (
      <ExercisesList
        exercises={exercises.filter((exercise) => !exercise.complete)}
        onCompleteExercise={completeExerciseHandler}
        onDeleteExercise={deleteExerciseHandler}
      />
    );
  }
  return (
    <div>
      <BaseFilter 
        onUpdate={updateFilterHandler} 
        current={currentFilter} />
      {jsx}
    </div>
  );
};

export default HomePage;

pages/CreateExercise.css

form  {
    padding: 20px;
    border-radius: 10px;
    text-align: center;
  }
  label {
    display: block;
    color: goldenrod;
    text-transform: uppercase;
    font-size: 14px;
    font-weight: bold;
    letter-spacing: 1px;
    margin: 20px 0 10px 0
  }
  input {
    padding: 10px;
    width: 400px;
    max-width: 100%;
    box-sizing: border-box;
    border: 1px solid var(--grey);
  }
  textarea {
    padding: 10px;
    max-width: 100%;
    width: 400px;
    box-sizing: border-box;
    border: 1px solid var(--grey);

  }
  form button {
    display: block;
    margin: 20px auto;
    background: goldenrod;
    color: white;
    padding: 10px;
    border-radius: 6px;
    font-size: 16px;
    border: 2px solid transparent;
  }

pages/CreateExercise.jsx

import React, { useState } from 'react';
import './CreateExercise.css';
import { useHistory } from 'react-router-dom';

const CreateExercise = () => {

  const [exercise, setExercise] = useState({
    title: '',
    details: ''
  })

  const history = useHistory();

  const handleChange = event => {
    setExercise({
      ...exercise,
      [event.target.name] : event.target.value
    });
  }

  const handleExerciseCreation = (event) => {

    event.preventDefault();

    const newExercise = {
      title: exercise.title,
      details : exercise.details,
      complete: false,
      id: Math.floor(Math.random() * 10000),
    };

    console.log(newExercise);

    fetch('http://localhost:3111/exercises', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(newExercise)
    }).then(() => {
      history.push('/home');
    }).catch(err => console.log(err))
  };

  return (
    <form  onSubmit={handleExerciseCreation}>
      <label>Title</label>
      <input type="text" onChange={handleChange} name="title" value={exercise.title} maxLength="15" required />
      <label>Details</label>
      <textarea value={exercise.details} name="details"  onChange={handleChange} required></textarea>
      <button>Add Exercise</button>
    </form>
  );
};

export default CreateExercise;

pages/EditExercise.jsx

import React, { useState, useEffect } from 'react';
import './CreateExercise.css';
import { useHistory, useParams } from 'react-router-dom';

const EditExercise = () => {

  const [exercise, setExercise] = useState({
    title: '',
    details: '',
  });

  const params = useParams();
  const exerciseId = params.id;
  const history = useHistory();

  const handleChange = (event) => {
    setExercise({
      ...exercise,
      [event.target.name]: event.target.value,
    });
  };

  useEffect(() => {

    fetch(`http://localhost:3111/exercises/${exerciseId}`)
      .then((res) => res.json())
      .then((data) => {
         setExercise({
           title: data.title,
           details: data.details,
         });
      })
      .catch((err) => console.log(err));

  }, [exerciseId])

  const handleExerciseUpdation = (event) => {

     event.preventDefault();
     fetch(`http://localhost:3111/exercises/${exerciseId}`, {
       method: 'PATCH',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify(exercise),
     })
       .then(() => {
         history.push('/home');
       }).catch((err) => console.log(err));
  };

  return (
    <form onSubmit={handleExerciseUpdation}>
      <label>Title</label>
      <input
        type="text"
        onChange={handleChange}
        name="title"
        value={exercise.title}
        maxLength="15"
        required
      />
      <label>Details</label>
      <textarea
        value={exercise.details}
        name="details"
        onChange={handleChange}
        required
      ></textarea>
      <button>Update Exercise</button>
    </form>
  );
};

export default EditExercise;

So this is the code for our entire project.

PS – If you prefer learning React with a video. I do have a video on same where we create this exact project.

Check this :

Thanks for reading !

Previous Post
Next Post