Open In App

Task Manager App using MERN Stack

Last Updated : 29 Feb, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Task Manager is very crucial to manage your tasks. In this article, we are going to develop a task manager application using the MERN stack. This application helps users to manage their tasks efficiently, offering essential features like creating new tasks, editing existing ones, and deleting tasks as needed. We’ll walk through the step-by-step process of creating this application.

Output Preview: Let us have a look at how the final output will look like.

Screenshot-from-2024-02-26-20-33

Preview of Final Output

Prerequisites:

Approach to create Task Manager App:

  • Determine the features and functionalities required for the task manager application, such as task creation, editing, deletion, and viewing tasks.
  • Choose and install the required dependencies and requirements which are more suitable for the Project.
  • Create the folder structure and components of the project.
  • Design and Implement the Frontend of the project.
  • Create a Backend Server as well as design and implement the APIs required for the project development.
  • Integrate the Backend with the Frontend and test it, either manually or using some testing library.

Steps to Create the Frontend:

Step 1: Set up React frontend and get into it using the command

npx create-react-app client
cd client

Step 2: Install the required dependencies(axios, tailwindcss).

npm install axios
npm install -D tailwindcss
npx tailwindcss init

Step 3: Configure the tailwind.config.js file

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,js,jsx}"],
theme: {
extend: {},
},
plugins: [],
}

Step 4: Add the Tailwind directives to your CSS in index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 5: Start Tailwind CLI

npx tailwindcss -i ./src/index.css -o ./src/output.css --watch

Project Structure:

Screenshot-from-2024-02-24-22-39-15

Frontend Folder Structure

The updated dependencies in package.json file 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-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"tailwindcss": "^3.4.1"
}

Example Code: Create the required files and write the following code.

Javascript




//src/Components/Filterbar.jsx
 
import React from 'react';
import { useTaskContext } from '../Context/TaskContext';
 
function Filterbar() {
    const { handleFilterClick } = useTaskContext();
 
    return (
        <div className="flex justify-center mt-8">
            <button
                className="filter-button bg-blue-500
                hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-l"
                onClick={() => handleFilterClick('all')}
            >
                All
            </button>
            <button
                className="filter-button bg-blue-500 hover:bg-blue-600
                 text-white font-bold py-2 px-4"
                onClick={() => handleFilterClick('completed')}
            >
                Completed
            </button>
            <button
                className="filter-button bg-blue-500 hover:bg-blue-600
                 text-white font-bold py-2 px-4 rounded-r"
                onClick={() => handleFilterClick('todo')}
            >
                To Do
            </button>
        </div>
    );
}
 
export default Filterbar;


Javascript




//src/Components/Navbar.jsx
 
import React, { useState } from 'react';
import AddTaskModal from '../Modals/AddTaskModal';
 
function Navbar() {
    const [isModalOpen, setIsModalOpen] = useState(false);
 
    const openModal = () => {
        setIsModalOpen(true);
    };
 
    const closeModal = () => {
        setIsModalOpen(false);
    };
 
    return (
        <nav className="bg-gray-800 py-4">
            <div className="max-w-7xl mx-auto px-4 flex
             justify-between items-center">
                <div>
                    <span className="text-white text-lg
                     font-bold">Task Manager</span>
                </div>
 
                <div>
                    <button className="bg-blue-500 hover:bg-blue-600
                     text-white font-bold py-2 px-4 rounded"
                        onClick={openModal}>
                        Add
                    </button>
                </div>
            </div>
            <AddTaskModal isOpen={isModalOpen} closeModal={closeModal} />
        </nav>
    );
}
 
export default Navbar;


Javascript




//src/Components/TaskList.jsx
 
import React, { useState } from 'react';
import { useTaskContext } from '../Context/TaskContext';
import DeleteModal from '../Modals/DeleteModal';
import EditModal from '../Modals/EditModal';
 
function TaskList() {
    const { filteredTasks, updateTaskStatus } = useTaskContext();
    const [taskId, setTaskId] = useState('');
    const [taskTitle, setTaskTitle] = useState('');
    const [taskDescription, setTaskDescription] = useState('');
    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
    const [isEditModalOpen, setIsEditModalOpen] = useState(false);
    const [openDropdownId, setOpenDropdownId] = useState(null);
 
    const handleDelete = (taskId) => {
        setTaskId(taskId);
        setIsDeleteModalOpen(true);
        setOpenDropdownId(null);
    };
 
    const handleEdit = (taskId, taskTitle, taskDescription) => {
        setTaskId(taskId);
        setTaskTitle(taskTitle);
        setTaskDescription(taskDescription);
        setIsEditModalOpen(true);
        setOpenDropdownId(null);
    };
 
    const handleComplete = (taskId) => {
        updateTaskStatus(taskId, 'completed');
        setOpenDropdownId(null);
    };
 
    const toggleDropdown = (taskId) => {
        setOpenDropdownId(openDropdownId === taskId ? null : taskId);
    };
 
    const isDropdownOpen = (taskId) => {
        return openDropdownId === taskId;
    };
 
    const getStatusColor = (status) => {
        switch (status) {
            case 'todo':
                return 'bg-yellow-200';
            case 'completed':
                return 'bg-green-200';
            default:
                return 'bg-gray-200';
        }
    };
 
    return (
        <div className="my-8 grid gap-4 grid-cols-1 sm:grid-cols-2
         md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
            {filteredTasks.map(task => (
                <div key={task._id} className=
                    {`relative rounded-md shadow-md
                      ${getStatusColor(task.status)}`}>
                    <div className="p-4">
                        <div className="flex justify-between items-center mb-2">
                            <h3 className="text-lg font-semibold">
                                {task.title}
                            </h3>
                            <button onClick={() => toggleDropdown(task._id)}
                                className="text-gray-500 hover:text-gray-700">
                                <svg className="w-6 h-6" fill="none"
                                    viewBox="0 0 24 24" stroke="currentColor">
                                    <path strokeLinecap="round"
                                          strokeLinejoin="round"
                                          strokeWidth={2}
                                          d="M4 6h16M4 12h16M4 18h16" />
                                </svg>
                            </button>
                        </div>
                        <p className="text-sm text-gray-600 mb-4">
                            {task.description}</p>
                    </div>
                    {isDropdownOpen(task._id) && (
                        <div className="absolute top-full right-0 mt-2 w-48
                         bg-white border rounded-md shadow-md z-10">
                            <button className="block w-full py-2 px-4
                             text-left hover:bg-gray-100" onClick={() =>
                                    handleEdit(task._id, task.title,
                                               task.description)}>
                                Edit</button>
                            <button className="block w-full py-2 px-4
                             text-left text-red-600 hover:bg-red-100"
                                onClick={() => handleDelete(task._id)}>
                                Delete</button>
                            {task.status !== 'completed' && (
                                <button className="block w-full py-2 px-4
                                                    text-left hover:bg-gray-100"
                                        onClick={() =>
                                        handleComplete(task._id)}>
                                        Mark as Completed
                                </button>
                            )}
                        </div>
                    )}
                </div>
            ))}
            <DeleteModal isOpen={isDeleteModalOpen}
                closeModal={() => setIsDeleteModalOpen(false)}
                taskId={taskId} />
            <EditModal isOpen={isEditModalOpen}
                closeModal={() => setIsEditModalOpen(false)} taskId={taskId}
                initialTitle={taskTitle} initialDescription={taskDescription} />
        </div>
    );
}
 
export default TaskList;


Javascript




//src/Context/TaskContext.js
 
import React, {
    createContext,
    useContext,
    useEffect,
    useState
} from "react";
import axios from "axios";
 
const TaskContext = createContext();
 
const apiUrl = process.env.REACT_APP_API_URL || "http://localhost:5000";
 
export const useTaskContext = () => {
    return useContext(TaskContext);
};
 
export const TaskProvider = ({ children }) => {
    const [tasks, setTasks] = useState([]);
    const [filteredTasks, setFilteredTasks] = useState([]);
    const [totalTasks, setTotalTasks] = useState(0);
    const [completedTasks, setCompletedTasks] = useState(0);
    const [todoTasks, setTodoTasks] = useState(0);
 
    useEffect(() => {
        fetchData();
    }, [totalTasks]);
 
    const fetchData = async () => {
        try {
            const response = await axios.get(`${apiUrl}/tasks`);
            setTasks(response.data);
            setFilteredTasks(response.data);
            setTotalTasks(response.data.length);
            const completedCount = response.data.filter(
                (task) => task.status === "completed"
            ).length;
            setCompletedTasks(completedCount);
            setTodoTasks(response.data.length - completedCount);
        } catch (err) {
            console.error("Error fetching data:", err);
        }
    };
 
    const handleFilterClick = (status) => {
        if (status === "all") {
            setFilteredTasks(tasks);
        } else {
            const filtered = tasks.filter((task) =>
                task.status === status);
            setFilteredTasks(filtered);
        }
    };
 
    const addTask = async (title, description, status) => {
        try {
            const response = await axios.post(`${apiUrl}/tasks`, {
                title,
                description,
                status,
            });
            setTasks([...tasks, response.data]);
            if (status === "completed") {
                setCompletedTasks((prev) => prev + 1);
            } else {
                setTodoTasks((prev) => prev + 1);
            }
            setTotalTasks((prev) => prev + 1);
        } catch (err) {
            console.error("Error adding task:", err);
        }
    };
 
    const deleteTask = async (taskId) => {
        try {
            await axios.delete(`${apiUrl}/tasks/${taskId}`);
            const updatedTasks = tasks.filter((task) => task.id !== taskId);
            setTasks(updatedTasks);
            setFilteredTasks(updatedTasks);
            setTotalTasks((prev) => prev - 1);
            const completedCount = updatedTasks.filter(
                (task) => task.status === "completed"
            ).length;
            setCompletedTasks(completedCount);
            setTodoTasks(updatedTasks.length - completedCount);
        } catch (err) {
            console.error("Error deleting task:", err);
        }
    };
 
    const editTask = async (
        taskId,
        updatedTitle,
        updatedDescription,
        updatedStatus
    ) => {
        try {
            await axios.put(`${apiUrl}/tasks/${taskId}`, {
                title: updatedTitle,
                description: updatedDescription,
                status: updatedStatus,
            });
            fetchData();
        } catch (err) {
            console.error("Error editing task:", err);
        }
    };
 
    const updateTaskStatus = async (taskId, status) => {
        try {
            await axios.put(`${apiUrl}/tasks/${taskId}`, { status });
            const updatedTasks = tasks.map((task) =>
                task._id === taskId ? { ...task, status } : task
            );
            setTasks(updatedTasks);
            setFilteredTasks(updatedTasks);
            setCompletedTasks((prev) =>
                status === "completed" ? prev + 1 : prev - 1
            );
            setTodoTasks((prev) => (status !== "completed" ?
                                    prev + 1 : prev - 1));
        } catch (err) {
            console.error("Error updating task status:", err);
        }
    };
 
    return (
        <TaskContext.Provider
            value={{
                filteredTasks,
                totalTasks,
                completedTasks,
                todoTasks,
                handleFilterClick,
                addTask,
                deleteTask,
                editTask,
                updateTaskStatus,
            }}
        >
            {children}
        </TaskContext.Provider>
    );
};


Javascript




//src/Modals/AddTaskModal.jsx
 
import React, { useState } from 'react';
import { useTaskContext } from '../Context/TaskContext';
 
function AddTaskModal({ isOpen, closeModal }) {
    const { addTask } = useTaskContext();
    const [title, setTitle] = useState('');
    const [description, setDescription] = useState('');
    const [status, setStatus] = useState('todo');
 
    const handleSubmit = () => {
        addTask(title, description, status);
        setTitle('');
        setDescription('');
        setStatus('todo');
        closeModal();
    };
 
    return (
        <div className={`modal ${isOpen ? 'block' : 'hidden'}
         fixed inset-0 z-10 overflow-y-auto`}
            style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
            <div className="modal-container bg-white
             w-full md:w-1/3 mx-auto mt-20 p-6 rounded shadow-lg">
                <div className="modal-header flex justify-between items-center">
                    <h3 className="text-lg font-semibold">Add New Task</h3>
                    <button className="text-gray-500 hover:text-gray-800"
                        onClick={closeModal}>X</button>
                </div>
                <div className="modal-body mt-4">
                    <div className="mb-4">
                        <label className="block text-gray-700 text-sm
                         font-bold mb-2" htmlFor="title">Title</label>
                        <input className="border rounded w-full py-2
                                           px-3 text-gray-700
                                          leading-tight focus:outline-none
                                            focus:shadow-outline"
                               id="title" type="text" value={title}
                               onChange={(e) => setTitle(e.target.value)} />
                    </div>
                    <div className="mb-4">
                        <label className="block text-gray-700 text-sm
                                           font-bold mb-2"
                               htmlFor="description">
                             Description
                        </label>
                        <input className="border rounded w-full
                                          py-2 px-3 text-gray-700
                                          leading-tight focus:outline-none
                                            focus:shadow-outline"
                               id="description"
                               type="text"
                               value={description}
                               onChange={(e) => setDescription(e.target.value)}/>
                    </div>
                    <button className="bg-blue-500 hover:bg-blue-600
                                       text-white font-bold
                                       py-2 px-4 rounded"
                            onClick={handleSubmit}>
                        Add Task
                    </button>
                </div>
            </div>
        </div>
    );
}
 
export default AddTaskModal;


Javascript




//src/Modals/DeleteModal.jsx
 
import React from 'react';
import { useTaskContext } from '../Context/TaskContext';
 
function DeleteModal({ isOpen, closeModal, taskId }) {
    const { deleteTask } = useTaskContext();
 
    const handleDelete = () => {
        deleteTask(taskId);
        closeModal();
    };
 
    return (
        <div className={`modal ${isOpen ? 'block' : 'hidden'}
         fixed inset-0 z-10 overflow-y-auto`}
            style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
            <div className="modal-container bg-white w-full
             md:w-1/3 mx-auto mt-20 p-6 rounded shadow-lg">
                <div className="modal-header flex justify-between
                 items-center">
                    <h3 className="text-lg font-semibold">Confirm Delete</h3>
                    <button className="text-gray-500 hover:text-gray-800"
                        onClick={closeModal}>X</button>
                </div>
                <div className="modal-body mt-4">
                    <p>Are you sure you want to delete this task?</p>
                    <div className="flex justify-end mt-4">
                        <button className="bg-red-500 hover:bg-red-600
                         text-white font-bold py-2 px-4 rounded mr-2"
                            onClick={handleDelete}>Delete</button>
                        <button className="bg-gray-300 hover:bg-gray-400
                         text-gray-800 font-bold py-2 px-4 rounded"
                            onClick={closeModal}>Cancel</button>
                    </div>
                </div>
            </div>
        </div>
    );
}
 
export default DeleteModal;


Javascript




//src/Modals/EditModal.jsx
 
import React, { useState } from 'react';
import { useTaskContext } from '../Context/TaskContext';
 
function EditModal({ isOpen, closeModal, taskId, initialTitle = '' }) {
    const { editTask } = useTaskContext();
    const [title, setTitle] = useState(initialTitle);
 
    const handleSubmit = () => {
        editTask(taskId, title);
        closeModal();
    };
 
    return (
        <div className={`modal ${isOpen ? 'block' : 'hidden'}
         fixed inset-0 z-10 overflow-y-auto`}
            style={{ backgroundColor: 'rgba(0, 0, 0, 0.5)' }}>
            <div className="modal-container bg-white w-full
             md:w-1/3 mx-auto mt-20 p-6 rounded shadow-lg">
                <div className="modal-header flex justify-between items-center">
                    <h3 className="text-lg font-semibold">Edit Task</h3>
                    <button className="text-gray-500 hover:text-gray-800"
                        onClick={closeModal}>X</button>
                </div>
                <div className="modal-body mt-4">
                    <div className="mb-4">
                        <label className="block text-gray-700
                        text-sm font-bold mb-2" htmlFor="title">Title</label>
                        <input className="border rounded w-full py-2
                                          px-3 text-gray-700
                                          leading-tight focus:outline-none
                                            focus:shadow-outline"
                               id="title"
                               type="text"
                               value={title}
                               onChange={(e) => setTitle(e.target.value)} />
                    </div>
                    <div className="flex justify-end mt-4">
                        <button className="bg-blue-500 hover:bg-blue-600
                        text-white font-bold py-2 px-4 rounded mr-2"
                            onClick={handleSubmit}>Save</button>
                        <button className="bg-gray-300 hover:bg-gray-400
                                            text-gray-800 font-bold
                                           py-2 px-4 rounded"
                            onClick={closeModal}>Cancel</button>
                    </div>
                </div>
            </div>
        </div>
    );
}
 
export default EditModal;


Javascript




//src/App.js
 
import './App.css';
import Filterbar from './Components/Filterbar';
import Navbar from './Components/Navbar';
import { TaskProvider } from './Context/TaskContext';
import Tasks from "./Components/TaskList"
 
function App() {
    return (
        <>
            <TaskProvider>
                <Navbar />
                <Filterbar />
                <Tasks />
            </TaskProvider>
        </>
    );
}
 
export default App;


To Start the frontend run the following command:

npm start

Steps to Create the Backend:

Step 1: Create a directory for project

mkdir server
cd server

Step 2: Initialized the Express app and installing the required packages

npm init -y
npm i express mongoose cors dotenv

Project Structure:

Screenshot-from-2024-02-24-23-14-12

Backend Folder Structure

The updated dependencies in package.json file will look like:

"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"mongoose": "^8.2.0"
}

Example: Create ‘server.js’ and write the below code.

Javascript




//server.js
 
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
 
const app = express();
const port = process.env.PORT || 5000;
 
app.use(cors());
app.use(express.json());
 
mongoose.connect(process.env.MONGODB_URI)
    .then(() => console.log("Database Connected!"))
    .catch(err => console.error("Database connection error:", err));
 
const taskSchema = new mongoose.Schema({
    title: String,
    description: String,
    status: String
});
 
const Task = mongoose.model('Task', taskSchema);
 
async function getTask(req, res, next) {
    try {
        const task = await Task.findById(req.params.id);
        if (!task) {
            return res.status(404).json({ message: 'Task not found' });
        }
        res.task = task;
        next();
    } catch (err) {
        return res.status(500).json({ message: err.message });
    }
}
 
app.get('/tasks', async (req, res) => {
    try {
        const tasks = await Task.find();
        res.json(tasks);
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
});
 
app.post('/tasks', async (req, res) => {
    const task = new Task({
        title: req.body.title,
        description: req.body.description,
        status: req.body.status
    });
    try {
        const newTask = await task.save();
        res.status(201).json(newTask);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});
 
app.put('/tasks/:id', getTask, async (req, res) => {
    if (req.body.title != null) {
        res.task.title = req.body.title;
    }
    if (req.body.description != null) {
        res.task.description = req.body.description;
    }
    if (req.body.status != null) {
        res.task.status = req.body.status;
    }
    try {
        const updatedTask = await res.task.save();
        res.json(updatedTask);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});
 
app.delete('/tasks/:id', getTask, async (req, res) => {
    try {
        await res.task.deleteOne();
        res.json({ message: 'Task deleted' });
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
});
 
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});


To start the application run the following command:

node server.js

Output:

Output of data saved in Database:

frtrh

MongoDB Data Store

Browser Output:

Screencast-from-26-02-24-08-44-19-PM-IST

Final Output



Like Article
Suggest improvement
Previous
Next
Share your thoughts in the comments

Similar Reads