Task Management System is one of the most important tools when you want to organize your tasks. NodeJS and ExpressJS are used in this article to create a REST API for performing all CRUD operations on task. It has two models User and Task. ReactJS and Tailwind CSS are used to create a frontend interface part in which we can add, delete, and update tasks.
Output Preview: Let us have a look at how the final output will look like

PrerequisitesApproach to create Task Management System: Write the Approach(flow of the app) in bullets points.
- First of all we will create server for the task management application.
- In the server part we will implement API for performing operations in the task management application.
- After that we will implement the frontend part .
- Then we will run the frontend application as well as the server part.
Steps to Create the task management system:Step 1: Create the folder for the project:
mkdir task-manager cd task-manager Step 2: Create the server by using the following commands.
mkdir server cd server npm init -y Step 3: Install the required dependencies:
npm i express mongoose nodemon bcrypt dotenv cors jsonwebtoken Folder Structure(backend): Folder Structure(Backend) Dependencies(backend): The updated dependencies in package.json file for backend will look like.
"dependencies": { "bcrypt": "^5.0.1", "cors": "^2.8.5", "dotenv": "^16.0.0", "express": "^4.17.3", "jsonwebtoken": "^8.5.1", "mongoose": "^6.2.3" }, "devDependencies": { "nodemon": "^2.0.22" } Step 4: Create an .env file and store the following in it.
PORT = 8000 MONGODB_URL = mongodb://localhost:27017 ACCESS_TOKEN_SECRET = ENTERTEXTHERE Step 5: Now add the following code in the respective files
JavaScript
//app.js
const express = require("express");
const app = express();
const mongoose = require("mongoose");
const path = require("path");
const cors = require("cors");
require("dotenv").config();
// routes
const authRoutes = require("./routes/authRoutes");
const taskRoutes = require("./routes/taskRoutes");
const profileRoutes = require("./routes/profileRoutes");
app.use(express.json());
app.use(cors());
const mongoUrl = process.env.MONGODB_URL;
mongoose.connect(mongoUrl, (err) => {
if (err) throw err;
console.log("Mongodb connected...");
});
app.use("/api/auth", authRoutes);
app.use("/api/tasks", taskRoutes);
app.use("/api/profile", profileRoutes);
if (process.env.NODE_ENV === "production") {
app.use(express.static(path.resolve(__dirname, "../frontend/build")));
app.get("*", (req, res) =>
res.sendFile(path.resolve(__dirname, "../frontend/build/index.html"))
);
}
const port = process.env.PORT || 5000;
app.listen(port, () => {
console.log(`Backend is running on port ${port}`);
});
JavaScript
//controllers/authControllers.js
const User = require("../models/User");
const bcrypt = require("bcrypt");
const { createAccessToken } = require("../utils/token");
const { validateEmail } = require("../utils/validation");
exports.signup = async (req, res) => {
try {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ msg: "Please fill all the fields" });
}
if (typeof name !== "string" || typeof email !== "string" || typeof password !== "string") {
return res.status(400).json({ msg: "Please send string values only" });
}
if (password.length < 4) {
return res.status(400).json({ msg: "Password length must be atleast 4 characters" });
}
if (!validateEmail(email)) {
return res.status(400).json({ msg: "Invalid Email" });
}
const user = await User.findOne({ email });
if (user) {
return res.status(400).json({ msg: "This email is already registered" });
}
const hashedPassword = await bcrypt.hash(password, 10);
await User.create({ name, email, password: hashedPassword });
res.status(200).json({ msg: "Congratulations!! Account has been created for you.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ msg: "Internal Server Error" });
}
}
exports.login = async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ status: false, msg: "Please enter all details!!" });
}
const user = await User.findOne({ email });
if (!user) return res.status(400).json({ status: false, msg: "This email is not registered!!" });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(400).json({ status: false, msg: "Password incorrect!!" });
const token = createAccessToken({ id: user._id });
delete user.password;
res.status(200).json({ token, user, status: true, msg: "Login successful.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
JavaScript
//controllers/profileControllers.js
const User = require("../models/User");
exports.getProfile = async (req, res) => {
try {
const user = await User.findById(req.user.id).select("-password");
res.status(200).json({ user, status: true, msg: "Profile found successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
JavaScript
//controllers/taskControllers.js
const Task = require("../models/Task");
const { validateObjectId } = require("../utils/validation");
exports.getTasks = async (req, res) => {
try {
const tasks = await Task.find({ user: req.user.id });
res.status(200).json({ tasks, status: true, msg: "Tasks found successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
exports.getTask = async (req, res) => {
try {
if (!validateObjectId(req.params.taskId)) {
return res.status(400).json({ status: false, msg: "Task id not valid" });
}
const task = await Task.findOne({ user: req.user.id, _id: req.params.taskId });
if (!task) {
return res.status(400).json({ status: false, msg: "No task found.." });
}
res.status(200).json({ task, status: true, msg: "Task found successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
exports.postTask = async (req, res) => {
try {
const { description } = req.body;
if (!description) {
return res.status(400).json({ status: false, msg: "Description of task not found" });
}
const task = await Task.create({ user: req.user.id, description });
res.status(200).json({ task, status: true, msg: "Task created successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
exports.putTask = async (req, res) => {
try {
const { description } = req.body;
if (!description) {
return res.status(400).json({ status: false, msg: "Description of task not found" });
}
if (!validateObjectId(req.params.taskId)) {
return res.status(400).json({ status: false, msg: "Task id not valid" });
}
let task = await Task.findById(req.params.taskId);
if (!task) {
return res.status(400).json({ status: false, msg: "Task with given id not found" });
}
if (task.user != req.user.id) {
return res.status(403).json({ status: false, msg: "You can't update task of another user" });
}
task = await Task.findByIdAndUpdate(req.params.taskId, { description }, { new: true });
res.status(200).json({ task, status: true, msg: "Task updated successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
exports.deleteTask = async (req, res) => {
try {
if (!validateObjectId(req.params.taskId)) {
return res.status(400).json({ status: false, msg: "Task id not valid" });
}
let task = await Task.findById(req.params.taskId);
if (!task) {
return res.status(400).json({ status: false, msg: "Task with given id not found" });
}
if (task.user != req.user.id) {
return res.status(403).json({ status: false, msg: "You can't delete task of another user" });
}
await Task.findByIdAndDelete(req.params.taskId);
res.status(200).json({ status: true, msg: "Task deleted successfully.." });
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
JavaScript
//middlewares.js/index.js
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { ACCESS_TOKEN_SECRET } = process.env;
exports.verifyAccessToken = async (req, res, next) => {
const token = req.header("Authorization");
if (!token) return res.status(400).json({ status: false, msg: "Token not found" });
let user;
try {
user = jwt.verify(token, ACCESS_TOKEN_SECRET);
}
catch (err) {
return res.status(401).json({ status: false, msg: "Invalid token" });
}
try {
user = await User.findById(user.id);
if (!user) {
return res.status(401).json({ status: false, msg: "User not found" });
}
req.user = user;
next();
}
catch (err) {
console.error(err);
return res.status(500).json({ status: false, msg: "Internal Server Error" });
}
}
JavaScript
//models/Task.js
const mongoose = require("mongoose");
const taskSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true
},
description: {
type: String,
required: true,
},
}, {
timestamps: true
});
const Task = mongoose.model("Task", taskSchema);
module.exports = Task;
JavaScript
//models.Users.js
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, "Please enter your name"],
trim: true
},
email: {
type: String,
required: [true, "Please enter your email"],
trim: true,
unique: true
},
password: {
type: String,
required: [true, "Please enter your password"],
},
joiningTime: {
type: Date,
default: Date.now
}
}, {
timestamps: true
});
const User = mongoose.model("User", userSchema);
module.exports = User;
JavaScript
//routes/authRoutes.js
const express = require("express");
const router = express.Router();
const { signup, login } = require("../controllers/authControllers");
// Routes beginning with /api/auth
router.post("/signup", signup);
router.post("/login", login);
module.exports = router;
JavaScript
// routes/profileRoutes.js
const express = require("express");
const router = express.Router();
const { getProfile } = require("../controllers/profileControllers");
const { verifyAccessToken } = require("../middlewares.js");
// Routes beginning with /api/profile
router.get("/", verifyAccessToken, getProfile);
module.exports = router;
JavaScript
// routes/taskRoutes.js
const express = require("express");
const router = express.Router();
const { getTasks, getTask, postTask, putTask, deleteTask } = require("../controllers/taskControllers");
const { verifyAccessToken } = require("../middlewares.js");
// Routes beginning with /api/tasks
router.get("/", verifyAccessToken, getTasks);
router.get("/:taskId", verifyAccessToken, getTask);
router.post("/", verifyAccessToken, postTask);
router.put("/:taskId", verifyAccessToken, putTask);
router.delete("/:taskId", verifyAccessToken, deleteTask);
module.exports = router;
JavaScript
//utils/token.js
const jwt = require("jsonwebtoken");
const { ACCESS_TOKEN_SECRET } = process.env;
const createAccessToken = (payload) => {
return jwt.sign(payload, ACCESS_TOKEN_SECRET);
}
module.exports = {
createAccessToken,
}
JavaScript
//utils/validation.js
const mongoose = require("mongoose");
const validateEmail = (email) => {
return String(email)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|
(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
);
};
const validateObjectId = (string) => {
return mongoose.Types.ObjectId.isValid(string);
}
module.exports = {
validateEmail,
validateObjectId,
}
Step 6: To start the server run the following code.
nodemon app.js Step 7: Now go to the project’s root directory and create the frontend application
npx create-react-app frontend cd frontend Step 8: Install the required dependencies.
npm i axios react-redux react-router-dom react-toastify redux redux-thunk Step 9: To use Tailwind CSS in the react application, first we need to install it
npm install tailwindcss@latest postcss@latest autoprefixer@latest Then we will create a tailwind configuration file
npx tailwindcss init Now setup the tailwind.config.js file
module.exports = { content: [ "./src/**/*.{js,jsx,ts,tsx}", ], theme: { extend: { colors: { "primary": "#24ab8f", "primary-dark": "#268d77", }, animation: { "loader": "loader 1s linear infinite", }, keyframes: { loader: { "0%": { transform: "rotate(0) scale(1)" }, "50%": { transform: "rotate(180deg) scale(1.5)" }, "100%": { transform: "rotate(360deg) scale(1)" } } } }, }, plugins: [], } Now include tailwind css in index.css file
@tailwind base; @tailwind components; @tailwind utilities; Now you can use classes of Tailwind CSS in your files.
Folder Structure (Frontend): Folder Structure(frontend) Dependencies(Frontend): The updated dependencies in package.json file for frontend will look like.
"dependencies": { "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.7", "react": "^18.2.0", "react-dom": "^18.2.0", "react-redux": "^9.1.0", "react-router-dom": "^6.22.1", "react-scripts": "5.0.1", "react-toastify": "^10.0.4", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "web-vitals": "^2.1.4" } Step 10: Now add the following code in respective components in frontend part
CSS
/* index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
font-family: "Roboto", sans-serif;
}
JavaScript
//App.jsx
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
import Task from "./pages/Task";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Signup from "./pages/Signup";
import { saveProfile } from "./redux/actions/authActions";
import NotFound from "./pages/NotFound";
function App() {
const authState = useSelector(state => state.authReducer);
const dispatch = useDispatch();
useEffect(() => {
const token = localStorage.getItem("token");
if (!token) return;
dispatch(saveProfile(token));
}, [authState.isLoggedIn, dispatch]);
return (
<>
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signup" element={authState.isLoggedIn ?
<Navigate to="/" /> : <Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/tasks/add" element={authState.isLoggedIn ?
<Task /> : <Navigate to="/login"
state={{ redirectUrl: "/tasks/add" }} />} />
<Route path="/tasks/:taskId"
element={authState.isLoggedIn ?
<Task /> : <Navigate to="/login"
state={{ redirectUrl: window.location.pathname }} />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</>
);
}
export default App;
JavaScript
//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from "react-redux"
import store from './redux/store';
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ToastContainer bodyStyle={{ fontFamily: "Roboto" }} />
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
JavaScript
//validations/index.js
const isValidEmail = (email) => {
return String(email)
.toLowerCase()
.match(
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|
(".+"))@((\[[0 - 9]{ 1, 3}\.[0 - 9]{ 1, 3}\.[0 - 9]
{ 1, 3}\.[0 - 9]{ 1, 3}\])|
(([a - zA - Z\-0 - 9] +\.) +[a - zA - Z]{ 2,})) $ /
);
};
export const validate = (group, name, value) => {
if (group === "signup") {
switch (name) {
case "name": {
if (!value) return "This field is required";
return null;
}
case "email": {
if (!value) return "This field is required";
if (!isValidEmail(value))
return "Please enter valid email address";
return null;
}
case "password": {
if (!value) return "This field is required";
if (value.length < 4)
return "Password should be atleast 4 chars long";
return null;
}
default: return null;
}
}
else if (group === "login") {
switch (name) {
case "email": {
if (!value) return "This field is required";
if (!isValidEmail(value))
return "Please enter valid email address";
return null;
}
case "password": {
if (!value) return "This field is required";
return null;
}
default: return null;
}
}
else if (group === "task") {
switch (name) {
case "description": {
if (!value) return "This field is required";
if (value.length > 100) return "Max. limit is 100 characters.";
return null;
}
default: return null;
}
}
else {
return null;
}
}
const validateManyFields = (group, list) => {
const errors = [];
for (const field in list) {
const err = validate(group, field, list[field]);
if (err) errors.push({ field, err });
}
return errors;
}
export default validateManyFields;
JavaScript
//redux/store.js
import { applyMiddleware, createStore, compose } from "redux";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const middleware = [thunk];
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer,
composeEnhancers(applyMiddleware(...middleware))
);
export default store;
JavaScript
//redux/reducers/authReducer.js
import {
LOGIN_FAILURE,
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGOUT,
SAVE_PROFILE
} from "../actions/actionTypes"
const initialState = {
loading: false,
user: {},
isLoggedIn: false,
token: "",
successMsg: "",
errorMsg: "",
}
const authReducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN_REQUEST:
return {
loading: true, user: {}, isLoggedIn: false,
token: "", successMsg: "", errorMsg: "",
};
case LOGIN_SUCCESS:
return {
loading: false, user: action.payload.user,
isLoggedIn: true, token: action.payload.token,
successMsg: action.payload.msg, errorMsg: ""
};
case LOGIN_FAILURE:
return {
loading: false, user: {}, isLoggedIn: false,
token: "", successMsg: "", errorMsg: action.payload.msg
};
case LOGOUT:
return {
loading: false, user: {}, isLoggedIn: false, token: "",
successMsg: "", errorMsg: ""
}
case SAVE_PROFILE:
return {
loading: false, user: action.payload.user,
isLoggedIn: true, token: action.payload.token,
successMsg: "", errorMsg: ""
}
default:
return state;
}
}
export default authReducer;
JavaScript
//redux/reducers/index.js
import { combineReducers } from "redux"
import authReducer from "./authReducer"
const rootReducer = combineReducers({
authReducer,
});
export default rootReducer;
JavaScript
//redux/actions/actionTypes.js
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_FAILURE = 'LOGIN_FAILURE'
export const LOGOUT = 'LOGOUT'
export const SAVE_PROFILE = 'SAVE_PROFILE'
export const SIGNUP_REQUEST = 'SIGNUP_REQUEST'
export const SIGNUP_SUCCESS = 'SIGNUP_SUCCESS'
export const SIGNUP_FAILURE = 'SIGNUP_FAILURE'
JavaScript
//redux/actions/authActions.js
import api from "../../api"
import {
LOGIN_FAILURE,
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGOUT
, SAVE_PROFILE
} from "./actionTypes"
import { toast } from "react-toastify";
export const postLoginData = (email, password) =>
async (dispatch) => {
try {
dispatch({ type: LOGIN_REQUEST });
const { data } = await api.post
('/auth/login', { email, password });
dispatch({
type: LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem('token', data.token);
toast.success(data.msg);
}
catch (error) {
const msg = error.response?.data?.msg || error.message;
dispatch({
type: LOGIN_FAILURE,
payload: { msg }
})
toast.error(msg);
}
}
export const saveProfile = (token) =>
async (dispatch) => {
try {
const { data } = await api.get('/profile', {
headers: { Authorization: token }
});
dispatch({
type: SAVE_PROFILE,
payload: { user: data.user, token },
});
}
catch (error) {
// console.log(error);
}
}
export const logout = () => (dispatch) => {
localStorage.removeItem('token');
dispatch({ type: LOGOUT });
document.location.href = '/';
}
JavaScript
//api/index.jsx
import axios from "axios";
const api = axios.create({
baseURL: "/api",
});
export default api;
JavaScript
//components/utils/Input.jsx
import React from "react";
const Input = ({
id,
name,
type,
value,
className = "",
disabled = false,
placeholder,
onChange,
}) => {
return (
<input
id={id}
type={type}
name={name}
value={value}
disabled={disabled}
className={`block w-full mt-2
px-3 py-2 text-gray-600 rounded-[4px]
border-2 border-gray-100 ${disabled ? "bg-gray-50" : ""
} focus:border-primary transition
outline-none hover:border-gray-300 ${className}`}
placeholder={placeholder}
onChange={onChange}
/>
);
};
export default Input;
export const Textarea = ({
id,
name,
type,
value,
className = "",
placeholder,
onChange,
}) => {
return (
<textarea
id={id}
type={type}
name={name}
value={value}
className={`block w-full h-40
mt-2 px-3 py-2 text-gray-600
rounded-[4px] border-2 border-gray-100
focus:border-primary transition outline-none
hover:border-gray-300 ${className}`}
placeholder={placeholder}
onChange={onChange}
/>
);
};
JavaScript
// components/utils/Loader.jsx
import React from 'react'
const Loader = () => {
return (
<>
<div className='w-8 h-8 my-8 mx-auto'>
<div className="w-full h-full rounded-full
border-[3px] border-indigo-600
border-b-transparent animate-loader"></div>
</div>
</>
)
}
export default Loader
JavaScript
//utils/Tooltip.jsx
import React, { useRef, useState } from "react";
import ReactDom from "react-dom";
const Portal = ({ children }) => {
return ReactDom.createPortal(children, document.body);
};
const Tooltip = ({
children, text,
position = "bottom", space = 5 }) => {
if (!React.isValidElement(children)) {
children = children[0];
}
const [open, setOpen] = useState(false);
const tooltipRef = useRef();
const elementRef = useRef();
const handleMouseEnter = () => {
setOpen(true);
const { x, y } = getPoint(
elementRef.current,
tooltipRef.current,
position,
space
);
tooltipRef.current.style.left = `${x}px`;
tooltipRef.current.style.top = `${y}px`;
};
const getPoint = (element, tooltip, position, space) => {
const eleRect = element.getBoundingClientRect();
const pt = { x: 0, y: 0 };
switch (position) {
case "bottom": {
pt.x = eleRect.left +
(element.offsetWidth - tooltip.offsetWidth) / 2;
pt.y = eleRect.bottom + (space + 10);
break;
}
case "left": {
pt.x = eleRect.left -
(tooltip.offsetWidth + (space + 10));
pt.y = eleRect.top +
(element.offsetHeight - tooltip.offsetHeight) / 2;
break;
}
case "right": {
pt.x = eleRect.right + (space + 10);
pt.y = eleRect.top +
(element.offsetHeight - tooltip.offsetHeight) / 2;
break;
}
case "top": {
pt.x = eleRect.left +
(element.offsetWidth - tooltip.offsetWidth) / 2;
pt.y = eleRect.top -
(tooltip.offsetHeight + (space + 10));
break;
}
default: {
break;
}
}
return pt;
};
const tooltipClasses = `fixed transition ${open ? "opacity-100" : "opacity-0 "
} pointer-events-none z-50 rounded-md
bg-black text-white px-4 py-2 text-center
w-max max-w-[150px]
${position === "top" &&
" after:absolute after:content-['']
after: left - 1 / 2 after: top - full after: -translate - x - 1 / 2
after: border - [10px] after: border - transparent after: border - t - black"
}
${
position === "bottom" &&
" after:absolute after:content-[''] after:left-1/2
after: bottom - full after: -translate - x - 1 / 2
after: border - [10px] after: border - transparent
after: border - b - black"
}
${
position === "left" &&
" after:absolute after:content-[''] after:top-1/2
after: left - full after: -translate - y - 1 / 2 after: border - [10px]
after: border - transparent after: border - l - black"
}
${
position === "right" &&
" after:absolute after:content-[''] after:top-1/2
after: right - full after: -translate - y - 1 / 2 after: border - [10px]
after: border - transparent after: border - r - black"
}
`;
return (
<>
{React.cloneElement(children, {
onMouseEnter: handleMouseEnter,
onMouseLeave: () => setOpen(false),
ref: elementRef,
})}
<Portal>
<div ref={tooltipRef} className={tooltipClasses}>
{text}
</div>
</Portal>
</>
);
};
export default Tooltip;
JavaScript
//components/LoginForm.jsx
import React, { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom';
import validateManyFields from '../validations';
import Input from './utils/Input';
import { useDispatch, useSelector } from "react-redux";
import { postLoginData } from '../redux/actions/authActions';
import Loader from './utils/Loader';
import { useEffect } from 'react';
const LoginForm = ({ redirectUrl }) => {
const [formErrors, setFormErrors] = useState({});
const [formData, setFormData] = useState({
email: "",
password: ""
});
const navigate = useNavigate();
const authState = useSelector(state => state.authReducer);
const { loading, isLoggedIn } = authState;
const dispatch = useDispatch();
useEffect(() => {
if (isLoggedIn) {
navigate(redirectUrl || "/");
}
}, [authState, redirectUrl, isLoggedIn, navigate]);
const handleChange = e => {
setFormData({
...formData, [e.target.name]: e.target.value
});
}
const handleSubmit = e => {
e.preventDefault();
const errors = validateManyFields("login", formData);
setFormErrors({});
if (errors.length > 0) {
setFormErrors(errors.reduce((total, ob) =>
({ ...total, [ob.field]: ob.err }), {}));
return;
}
dispatch(postLoginData(formData.email, formData.password));
}
const fieldError = (field) => (
<p className={`mt-1 text-pink-600 text-sm
${formErrors[field] ? "block" : "hidden"}`}>
<i className='mr-2 fa-solid fa-circle-exclamation'></i>
{formErrors[field]}
</p>
)
return (
<>
<form className='m-auto my-16 max-w-[500px] bg-white
p-8 border-2 shadow-md rounded-md'>
{loading ? (
<Loader />
) : (
<>
<h2 className='text-center mb-4'>Welcome user, please login here</h2>
<div className="mb-4">
<label htmlFor="email" className="after:content-['*']
after:ml-0.5 after:text-red-500">Email</label>
<Input type="text" name="email" id="email"
value={formData.email} placeholder="[email protected]"
onChange={handleChange} />
{fieldError("email")}
</div>
<div className="mb-4">
<label htmlFor="password" className="after:content-['*']
after:ml-0.5 after:text-red-500">Password</label>
<Input type="password" name="password" id="password"
value={formData.password} placeholder="Your password.."
onChange={handleChange} />
{fieldError("password")}
</div>
<button className='bg-primary text-white px-4 py-2
font-medium hover:bg-primary-dark'
onClick={handleSubmit}>Submit</button>
<div className='pt-4'>
<Link to="/signup" className='text-blue-400'>
Don't have an account? Signup here</Link>
</div>
</>
)}
</form>
</>
)
}
export default LoginForm
JavaScript
//components/Navbr.jsx
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { logout } from '../redux/actions/authActions';
const Navbar = () => {
const authState = useSelector(state => state.authReducer);
const dispatch = useDispatch();
const [isNavbarOpen, setIsNavbarOpen] = useState(false);
const toggleNavbar = () => {
setIsNavbarOpen(!isNavbarOpen);
}
const handleLogoutClick = () => {
dispatch(logout());
}
return (
<>
<header className='flex justify-between sticky
top-0 p-4 bg-white shadow-sm items-center'>
<h2 className='cursor-pointer uppercase font-medium'>
<Link to="/"> Task Manager </Link>
</h2>
<ul className='hidden md:flex gap-4 uppercase font-medium'>
{authState.isLoggedIn ? (
<>
<li className="bg-blue-500 text-white
hover:bg-blue-600 font-medium rounded-md">
<Link to='/tasks/add' className='block w-full
h-full px-4 py-2'> <i className="fa-solid fa-plus"></i>
Add task </Link>
</li>
<li className='py-2 px-3 cursor-pointer hover:bg-gray-200
transition rounded-sm' onClick={handleLogoutClick}>Logout</li>
</>
) : (
<li className='py-2 px-3 cursor-pointer text-primary
hover:bg-gray-100 transition rounded-sm'><Link to="/login">
Login</Link></li>
)}
</ul>
<span className='md:hidden cursor-pointer'
onClick={toggleNavbar}><i className="fa-solid fa-bars">
</i></span>
<div className={`absolute md:hidden right-0 top-0 bottom-0
transition ${(isNavbarOpen === true) ? 'translate-x-0' : 'translate-x-full'}
bg-gray-100 shadow-md w-screen sm:w-9/12 h-screen`}>
<div className='flex'>
<span className='m-4 ml-auto cursor-pointer'
onClick={toggleNavbar}><i className="fa-solid fa-xmark"></i></span>
</div>
<ul className='flex flex-col gap-4 uppercase font-medium text-center'>
{authState.isLoggedIn ? (
<>
<li className="bg-blue-500 text-white
hover:bg-blue-600 font-medium transition py-2 px-3">
<Link to='/tasks/add' className='block w-full h-full'>
<i className="fa-solid fa-plus"></i> Add task </Link>
</li>
<li className='py-2 px-3 cursor-pointer hover:bg-gray-200
transition rounded-sm' onClick={handleLogoutClick}>Logout</li>
</>
) : (
<li className='py-2 px-3 cursor-pointer text-primary
hover:bg-gray-200 transition rounded-sm'>
<Link to="/login">Login</Link></li>
)}
</ul>
</div>
</header>
</>
)
}
export default Navbar
JavaScript
//components/SignupForm.jsx
import React, { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom';
import useFetch from '../hooks/useFetch';
import validateManyFields from '../validations';
import Input from './utils/Input';
import Loader from './utils/Loader';
const SignupForm = () => {
const [formErrors, setFormErrors] = useState({});
const [formData, setFormData] = useState({
name: "",
email: "",
password: ""
});
const [fetchData, { loading }] = useFetch();
const navigate = useNavigate();
const handleChange = e => {
setFormData({
...formData, [e.target.name]: e.target.value
});
}
const handleSubmit = e => {
e.preventDefault();
const errors = validateManyFields("signup", formData);
setFormErrors({});
if (errors.length > 0) {
setFormErrors(errors.reduce((total, ob) =>
({ ...total, [ob.field]: ob.err }), {}));
return;
}
const config = { url: "/auth/signup", method: "post", data: formData };
fetchData(config).then(() => {
navigate("/login");
});
}
const fieldError = (field) => (
<p className={`mt-1 text-pink-600 text-sm
${formErrors[field] ? "block" : "hidden"}`}>
<i className='mr-2 fa-solid fa-circle-exclamation'></i>
{formErrors[field]}
</p>
)
return (
<>
<form className='m-auto my-16 max-w-[500px]
p-8 bg-white border-2 shadow-md rounded-md'>
{loading ? (
<Loader />
) : (
<>
<h2 className='text-center mb-4'>
Welcome user, please signup here</h2>
<div className="mb-4">
<label htmlFor="name" className="after:content-['*']
after:ml-0.5 after:text-red-500">Name</label>
<Input type="text" name="name" id="name"
value={formData.name} placeholder="Your name"
onChange={handleChange} />
{fieldError("name")}
</div>
<div className="mb-4">
<label htmlFor="email" className="after:content-['*']
after:ml-0.5 after:text-red-500">Email</label>
<Input type="text" name="email" id="email"
value={formData.email} placeholder="[email protected]"
onChange={handleChange} />
{fieldError("email")}
</div>
<div className="mb-4">
<label htmlFor="password" className="after:content-['*']
after:ml-0.5 after:text-red-500">Password</label>
<Input type="password" name="password" id="password"
value={formData.password} placeholder="Your password.."
onChange={handleChange} />
{fieldError("password")}
</div>
<button className='bg-primary text-white px-4 py-2
font-medium hover:bg-primary-dark'
onClick={handleSubmit}>Submit</button>
<div className='pt-4'>
<Link to="/login" className='text-blue-400'>
Already have an account? Login here</Link>
</div>
</>
)}
</form>
</>
)
}
export default SignupForm
JavaScript
//components/Task.jsx
import React, { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import useFetch from '../hooks/useFetch';
import Loader from './utils/Loader';
import Tooltip from './utils/Tooltip';
const Tasks = () => {
const authState = useSelector(state =>
state.authReducer);
const [tasks, setTasks] = useState([]);
const [fetchData, { loading }] = useFetch();
const fetchTasks = useCallback(() => {
const config = {
url: "/tasks", method: "get",
headers: { Authorization: authState.token }
};
fetchData(config, { showSuccessToast: false })
.then(data => setTasks(data.tasks));
}, [authState.token, fetchData]);
useEffect(() => {
if (!authState.isLoggedIn) return;
fetchTasks();
}, [authState.isLoggedIn, fetchTasks]);
const handleDelete = (id) => {
const config = {
url: `/tasks/${id}`,
method: "delete", headers: { Authorization: authState.token }
};
fetchData(config)
.then(() => fetchTasks());
}
return (
<>
<div className="my-2 mx-auto max-w-[700px] py-4">
{tasks.length !== 0 && <h2
className='my-2 ml-2 md:ml-0 text-xl'>
Your tasks ({tasks.length})</h2>}
{loading ? (
<Loader />
) : (
<div>
{tasks.length === 0 ? (
<div className='w-[600px] h-[300px]
flex items-center justify-center gap-4'>
<span>No tasks found</span>
<Link to="/tasks/add" className="bg-blue-500
text-white hover:bg-blue-600 font-medium
rounded-md px-4 py-2">+ Add new task </Link>
</div>
) : (
tasks.map((task, index) => (
<div key={task._id} className='bg-white my-4 p-4
text-gray-600 rounded-md shadow-md'>
<div className='flex'>
<span className='font-medium'>
Task #{index + 1}</span>
<Tooltip text={"Edit this task"} position={"top"}>
<Link to={`/tasks/${task._id}`} className='ml-auto
mr-2 text-green-600 cursor-pointer'>
<i className="fa-solid fa-pen"></i>
</Link>
</Tooltip>
<Tooltip text={"Delete this task"} position={"top"}>
<span className='text-red-500 cursor-pointer'
onClick={() => handleDelete(task._id)}>
<i className="fa-solid fa-trash"></i>
</span>
</Tooltip>
</div>
<div className='whitespace-pre'>{task.description}</div>
</div>
))
)}
</div>
)}
</div>
</>
)
}
export default Tasks
JavaScript
//hooks/useFetch.jsx
import { useCallback, useState } from "react"
import { toast } from "react-toastify";
import api from "../api";
const useFetch = () => {
const [state, setState] = useState({
loading: false,
data: null,
successMsg: "",
errorMsg: "",
});
const fetchData = useCallback
(async (config, otherOptions) => {
const { showSuccessToast = true,
showErrorToast = true } = otherOptions || {};
setState(state => ({ ...state, loading: true }));
try {
const { data } = await api.request(config);
setState({
loading: false,
data,
successMsg: data.msg || "success",
errorMsg: ""
});
if (showSuccessToast) toast.success(data.msg);
return Promise.resolve(data);
}
catch (error) {
const msg = error.response?.data?.msg || error.message || "error";
setState({
loading: false,
data: null,
errorMsg: msg,
successMsg: ""
});
if (showErrorToast) toast.error(msg);
return Promise.reject();
}
}, []);
return [fetchData, state];
}
export default useFetch
JavaScript
//layouts/MainLayout.jsx
import React from 'react'
import Navbar from '../components/Navbar';
const MainLayout = ({ children }) => {
return (
<>
<div className='relative bg-gray-50
h-screen w-screen overflow-x-hidden'>
<Navbar />
{children}
</div>
</>
)
}
export default MainLayout;
JavaScript
//pages/Home.jsx
import React, { useEffect } from 'react'
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import Tasks from '../components/Tasks';
import MainLayout from '../layouts/MainLayout';
const Home = () => {
const authState = useSelector(state =>
state.authReducer);
const { isLoggedIn } = authState;
useEffect(() => {
document.title = authState.isLoggedIn ?
`${authState.user.name}'s tasks` : "Task Manager";
}, [authState]);
return (
<>
<MainLayout>
{!isLoggedIn ? (
<div className='bg-primary text-white
h-[40vh] py-8 text-center'>
<h1 className='text-2xl'>
Welcome to Task Manager App</h1>
<Link to="/signup" className='mt-10
text-xl block space-x-2 hover:space-x-4'>
<span className='transition-[margin]'>
Join now to manage your tasks</span>
<span className='relative ml-4 text-base
transition-[margin]'>
<i className="fa-solid fa-arrow-right"></i></span>
</Link>
</div>
) : (
<>
<h1 className='text-lg mt-8 mx-8 border-b
border-b-gray-300'>Welcome {authState.user.name}</h1>
<Tasks />
</>
)}
</MainLayout>
</>
)
}
export default Home
JavaScript
//pages/Login.jsx
import React, { useEffect } from 'react'
import { useLocation } from 'react-router-dom';
import LoginForm from '../components/LoginForm';
import MainLayout from '../layouts/MainLayout'
const Login = () => {
const { state } = useLocation();
const redirectUrl = state?.redirectUrl || null;
useEffect(() => {
document.title = "Login";
}, []);
return (
<>
<MainLayout>
<LoginForm redirectUrl={redirectUrl} />
</MainLayout>
</>
)
}
export default Login
JavaScript
//pages/NotFound.jsx
import React from 'react'
import MainLayout from '../layouts/MainLayout'
const NotFound = () => {
return (
<MainLayout>
<div className='w-full py-16 text-center'>
<h1 className='text-7xl my-8'>404</h1>
<h2 className='text-xl'>
The page you are looking for doesn't exist</h2>
</div>
</MainLayout>
)
}
export default NotFound
JavaScript
//pages/Signup.jsx
import React, { useEffect } from 'react'
import SignupForm from '../components/SignupForm';
import MainLayout from '../layouts/MainLayout'
const Signup = () => {
useEffect(() => {
document.title = "Signup";
}, []);
return (
<>
<MainLayout>
<SignupForm />
</MainLayout>
</>
)
}
export default Signup
JavaScript
//pages/Task.jsx
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import { Textarea } from '../components/utils/Input';
import Loader from '../components/utils/Loader';
import useFetch from '../hooks/useFetch';
import MainLayout from '../layouts/MainLayout';
import validateManyFields from '../validations';
const Task = () => {
const authState = useSelector(state => state.authReducer);
const navigate = useNavigate();
const [fetchData, { loading }] = useFetch();
const { taskId } = useParams();
const mode = taskId === undefined ? "add" : "update";
const [task, setTask] = useState(null);
const [formData, setFormData] = useState({
description: ""
});
const [formErrors, setFormErrors] = useState({});
useEffect(() => {
document.title = mode === "add" ? "Add task" : "Update Task";
}, [mode]);
useEffect(() => {
if (mode === "update") {
const config = {
url: `/tasks/${taskId}`, method: "get",
headers: { Authorization: authState.token }
};
fetchData(config, { showSuccessToast: false })
.then((data) => {
setTask(data.task);
setFormData({ description: data.task.description });
});
}
}, [mode, authState, taskId, fetchData]);
const handleChange = e => {
setFormData({
...formData, [e.target.name]: e.target.value
});
}
const handleReset = e => {
e.preventDefault();
setFormData({
description: task.description
});
}
const handleSubmit = e => {
e.preventDefault();
const errors = validateManyFields("task", formData);
setFormErrors({});
if (errors.length > 0) {
setFormErrors(errors.reduce((total, ob) =>
({ ...total, [ob.field]: ob.err }), {}));
return;
}
if (mode === "add") {
const config = {
url: "/tasks", method: "post",
data: formData, headers: { Authorization: authState.token }
};
fetchData(config).then(() => {
navigate("/");
});
}
else {
const config = {
url: `/tasks/${taskId}`, method: "put",
data: formData, headers: { Authorization: authState.token }
};
fetchData(config).then(() => {
navigate("/");
});
}
}
const fieldError = (field) => (
<p className={`mt-1 text-pink-600 text-sm
${formErrors[field] ? "block" : "hidden"}`}>
<i className='mr-2 fa-solid fa-circle-exclamation'></i>
{formErrors[field]}
</p>
)
return (
<>
<MainLayout>
<form className='m-auto my-16 max-w-[1000px]
bg-white p-8 border-2 shadow-md rounded-md'>
{loading ? (
<Loader />
) : (
<>
<h2 className='text-center mb-4'>{mode === "add" ?
"Add New Task" : "Edit Task"}</h2>
<div className="mb-4">
<label htmlFor="description">Description</label>
<Textarea type="description" name="description"
id="description" value={formData.description} placeholder="Write here.."
onChange={handleChange} />
{fieldError("description")}
</div>
<button className='bg-primary text-white px-4 py-2 font-medium hover:bg-primary-dark'
onClick={handleSubmit}>{mode === "add" ? "Add task" : "Update Task"}</button>
<button className='ml-4 bg-red-500 text-white px-4 py-2 font-medium'
onClick={() => navigate("/")}>Cancel</button>
{mode === "update" && <button className='ml-4 bg-blue-500 text-white px-4
py-2 font-medium hover:bg-blue-600'
onClick={handleReset}>Reset</button>}
</>
)}
</form>
</MainLayout>
</>
)
}
export default Task
Step 11: Now start the react application
npm start
Output:
|