Open In App

Task Management System using Node and Express.js

Last Updated : 23 Jul, 2025
Comments
Improve
Suggest changes
5 Likes
Like
Report

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

Screenshot-2024-02-25-095821

Prerequisites

Approach 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):

dwefrtyhj
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):

ewr
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:


Explore