Building a Draggable Kanban Board in React with Edit and Delete Functionality

Harsh MaroliaHarsh Marolia
6 min read

Creating a Kanban board, similar to what you might see in Jira, can seem daunting, especially if you try to avoid relying on external libraries. In this tutorial, I'll walk you through how to build a simple, yet effective, draggable Kanban board in React with full support for editing and deleting tasks.

Understanding the Task Structure

The core of our Kanban board revolves around tasks, which are stored as objects with properties like id, name, description, assignee, and priority. These tasks will be managed in columns: Backlog, In Progress, and Completed.

The App Component

The main logic of our Kanban board resides in the App.tsx file. Here's how to break it down:

  1. State Management:

    • We use useState hooks to manage the state of tasks in each column.

    • newTicket stores data for creating new tasks.

    • isEditing, currentTask, and currentColumn handle editing tasks.

    • isModalOpen controls the modal visibility for viewing task details.

  2. Adding a New Task:

    • The handleAddTicket function creates a new task and adds it to the Backlog column.
  3. Editing an Existing Task:

    • The handleEditTask function populates the input fields with the task data, allowing the user to edit it.

    • handleSaveEdit updates the task in its respective column.

  4. Deleting a Task:

    • The handleDeleteTask function removes a task from the specified column.
  5. Drag and Drop Functionality:

    • We implement handleDragStart and handleDrop functions to manage the drag-and-drop feature, allowing tasks to move between columns.
  6. Task Modal:

    • Modal.tsx is used to display task details in a pop-up when clicked.

The Column Component

The Column.tsx file manages each column of tasks:

  • title: Displays the column title.

  • tasks: Lists all tasks in the column.

  • onDragStart and onDrop: Manage drag-and-drop functionality.

  • onEditTask, onDeleteTask, and onViewTask: Handle task editing, deletion, and viewing, respectively.

The Modal Component

Finally, the Modal.tsx file displays the details of a task when it's clicked. This modal can be closed by clicking the "Close" button.

Conclusion

This tutorial guides you through creating a simple, fully functional Kanban board with drag-and-drop, edit, and delete capabilities without using any third-party libraries. The approach is straightforward, making it ideal for beginners and experienced developers looking to build custom project management tools in React.

Feel free to expand upon this basic setup with additional features such as task deadlines, priority sorting, or user-specific task views. Happy coding!

Full Code

App.tsx

import React, { useState } from "react";
import { Column } from "./Column";
import { Modal } from "./Modal";

interface Task {
  id: number;
  name: string;
  description: string;
  assignee: string;
  priority: string;
}

const App: React.FC = () => {
  const [newTicket, setNewTicket] = useState<Partial<Task>>({
    name: "",
    description: "",
    assignee: "",
    priority: "",
  });
  const [completed, setCompleted] = useState<Task[]>([]);
  const [inProgress, setInProgress] = useState<Task[]>([]);
  const [backlog, setBacklog] = useState<Task[]>([]);
  const [isEditing, setIsEditing] = useState<boolean>(false);
  const [currentTask, setCurrentTask] = useState<Task | null>(null);
  const [currentColumn, setCurrentColumn] = useState<string>("");
  const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

  const handleAddTicket = () => {
    if (
      newTicket.name?.trim() !== "" &&
      newTicket.description?.trim() !== "" &&
      newTicket.assignee?.trim() !== "" &&
      newTicket.priority?.trim() !== ""
    ) {
      const newTask = {
        ...newTicket,
        id: Date.now(),
      } as Task;
      setBacklog([...backlog, newTask]);
      setNewTicket({ name: "", description: "", assignee: "", priority: "" });
    }
  };

  const handleEditTask = (task: Task, column: string) => {
    setIsEditing(true);
    setCurrentTask(task);
    setCurrentColumn(column);
    setNewTicket({
      name: task.name,
      description: task.description,
      assignee: task.assignee,
      priority: task.priority,
    });
  };

  const handleSaveEdit = () => {
    if (
      newTicket.name?.trim() === "" ||
      newTicket.description?.trim() === "" ||
      newTicket.assignee?.trim() === "" ||
      newTicket.priority?.trim() === ""
    )
      return;

    const updatedTask = {
      ...currentTask,
      ...newTicket,
    } as Task;

    switch (currentColumn) {
      case "Backlog":
        setBacklog(
          backlog.map((t) => (t.id === updatedTask.id ? updatedTask : t))
        );
        break;
      case "InProgress":
        setInProgress(
          inProgress.map((t) => (t.id === updatedTask.id ? updatedTask : t))
        );
        break;
      case "Completed":
        setCompleted(
          completed.map((t) => (t.id === updatedTask.id ? updatedTask : t))
        );
        break;
      default:
        break;
    }

    setIsEditing(false);
    setNewTicket({ name: "", description: "", assignee: "", priority: "" });
    setCurrentTask(null);
    setCurrentColumn("");
  };

  const handleDeleteTask = (task: Task, column: string) => {
    switch (column) {
      case "Backlog":
        setBacklog(backlog.filter((t) => t.id !== task.id));
        break;
      case "InProgress":
        setInProgress(inProgress.filter((t) => t.id !== task.id));
        break;
      case "Completed":
        setCompleted(completed.filter((t) => t.id !== task.id));
        break;
      default:
        break;
    }
  };

  const handleDragStart = (
    e: React.DragEvent<HTMLDivElement>,
    task: Task,
    sourceColumn: string
  ) => {
    e.dataTransfer.setData("task", JSON.stringify(task));
    e.dataTransfer.setData("sourceColumn", sourceColumn);
  };

  const handleDrop = (
    e: React.DragEvent<HTMLDivElement>,
    targetColumn: string
  ) => {
    const task = JSON.parse(e.dataTransfer.getData("task")) as Task;
    const sourceColumn = e.dataTransfer.getData("sourceColumn");

    if (task && sourceColumn && targetColumn !== sourceColumn) {
      switch (targetColumn) {
        case "Backlog":
          setBacklog([...backlog, task]);
          break;
        case "InProgress":
          setInProgress([...inProgress, task]);
          break;
        case "Completed":
          setCompleted([...completed, task]);
          break;
        default:
          break;
      }

      handleDeleteTask(task, sourceColumn);
    }
  };

  const handleViewTask = (task: Task) => {
    setCurrentTask(task);
    setIsModalOpen(true);
  };

  const closeModal = () => {
    setIsModalOpen(false);
    setCurrentTask(null);
  };

  return (
    <>
      <div className="flex justify-center items-center p-4">
        <input
          type="text"
          value={newTicket.name}
          onChange={(e) => setNewTicket({ ...newTicket, name: e.target.value })}
          placeholder="Enter Ticket Name"
          className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <input
          type="text"
          value={newTicket.description}
          onChange={(e) =>
            setNewTicket({ ...newTicket, description: e.target.value })
          }
          placeholder="Enter Description"
          className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <input
          type="text"
          value={newTicket.assignee}
          onChange={(e) =>
            setNewTicket({ ...newTicket, assignee: e.target.value })
          }
          placeholder="Enter Assignee"
          className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <input
          type="text"
          value={newTicket.priority}
          onChange={(e) =>
            setNewTicket({ ...newTicket, priority: e.target.value })
          }
          placeholder="Enter Priority"
          className="border rounded-lg px-4 py-2 mr-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          onClick={isEditing ? handleSaveEdit : handleAddTicket}
          className={`px-4 py-2 rounded-lg text-white shadow-md ${
            isEditing
              ? "bg-green-500 hover:bg-green-600"
              : "bg-blue-500 hover:bg-blue-600"
          }`}
        >
          {isEditing ? "Save Edit" : "Add Ticket"}
        </button>
      </div>
      <div className="flex justify-around p-4 space-x-4">
        <Column
          title="Backlog"
          tasks={backlog}
          onDrop={(e) => handleDrop(e, "Backlog")}
          onDragStart={handleDragStart}
          onEditTask={(task) => handleEditTask(task, "Backlog")}
          onDeleteTask={(task) => handleDeleteTask(task, "Backlog")}
          onViewTask={handleViewTask}
        />
        <Column
          title="InProgress"
          tasks={inProgress}
          onDrop={(e) => handleDrop(e, "InProgress")}
          onDragStart={handleDragStart}
          onEditTask={(task) => handleEditTask(task, "InProgress")}
          onDeleteTask={(task) => handleDeleteTask(task, "InProgress")}
          onViewTask={handleViewTask}
        />
        <Column
          title="Completed"
          tasks={completed}
          onDrop={(e) => handleDrop(e, "Completed")}
          onDragStart={handleDragStart}
          onEditTask={(task) => handleEditTask(task, "Completed")}
          onDeleteTask={(task) => handleDeleteTask(task, "Completed")}
          onViewTask={handleViewTask}
        />
      </div>

      {isModalOpen && currentTask && (
        <Modal currentTask={currentTask} closeModal={closeModal} />
      )}
    </>
  );
};

export default App;

Column.tsx

interface Task {
  id: number;
  name: string;
  description: string;
  assignee: string;
  priority: string;
}

interface TaskColumnProps {
  title: string;
  tasks: Task[];
  onDrop: (e: React.DragEvent<HTMLDivElement>) => void;
  onDragStart: (
    e: React.DragEvent<HTMLDivElement>,
    task: Task,
    sourceColumn: string
  ) => void;
  onEditTask: (task: Task) => void;
  onDeleteTask: (task: Task) => void;
  onViewTask: (task: Task) => void;
}

export const Column: React.FC<TaskColumnProps> = ({
  title,
  tasks,
  onDrop,
  onDragStart,
  onEditTask,
  onDeleteTask,
  onViewTask,
}) => {
  const handleTitle = (title: string) => {
    return title.length > 15 ? title.slice(0, 15) : title;
  };

  return (
    <div
      className="w-1/3 p-4 bg-gray-100 rounded-lg shadow-md"
      onDrop={onDrop}
      onDragOver={(e) => e.preventDefault()}
    >
      <h3 className="text-xl font-semibold mb-4 border-b pb-2">{title}</h3>
      {tasks.map((task) => (
        <div
          key={task.id}
          className="bg-white p-4 mb-2 rounded-lg shadow cursor-pointer flex justify-between items-center hover:bg-gray-50"
          draggable
          onDragStart={(e) => onDragStart(e, task, title)}
        >
          <span onClick={() => onViewTask(task)}>{handleTitle(task.name)}</span>
          <div className="flex space-x-2">
            <button
              onClick={() => onEditTask(task)}
              className="text-sm text-blue-500 hover:text-blue-700"
            >
              Edit
            </button>
            <button
              onClick={() => onDeleteTask(task)}
              className="text-sm text-red-500 hover:text-red-700"
            >
              Delete
            </button>
          </div>
        </div>
      ))}
    </div>
  );
};

Modal.tsx

export const Modal = ({ currentTask, closeModal }: any) => {
  return (
    <div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center">
      <div className="bg-white rounded-lg p-8 shadow-lg w-1/2 relative">
        <h2 className="text-2xl font-semibold mb-4">Task Details</h2>
        <p>
          <strong>Name:</strong> {currentTask.name}
        </p>
        <p>
          <strong>Description:</strong> {currentTask.description}
        </p>
        <p>
          <strong>Assignee:</strong> {currentTask.assignee}
        </p>
        <p>
          <strong>Priority:</strong> {currentTask.priority}
        </p>
        <button
          onClick={closeModal}
          className="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 absolute top-2 right-2"
        >
          Close
        </button>
      </div>
    </div>
  );
};
1
Subscribe to my newsletter

Read articles from Harsh Marolia directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Harsh Marolia
Harsh Marolia