23
πͺπ»ππ»ββοΈ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.
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 :
// To create a new React Project
npx create-react-app <project_name>
cd <project_name>
// To run the project
npm start
npm install react-router-dom
We will be using JSON-server for our project.
npm install json-server -g
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.
{
"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
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();
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 {
text-align: center;
}
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);
}
.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;
}
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;
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;
.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)
}
.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;
}
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;
.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;
}
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
Create a folder for pages where we will house all our page components.
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;
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;
}
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;
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 !
PS - If you are looking to learn Web Development, I have curated a FREE course for you on my YouTube Channel, check the below article :
ππ» Follow me on Twitter : https://twitter.com/The_Nerdy_Dev
ππ» Check out my YouTube Channel : https://youtube.com/thenerdydev
23