fervor [>]CODING & CURIOSITY
FERVOR LEARNING SYSTEMTUTORIALS
← Projects

Projects / 11 MIN READ

Kanban Task Board Frontend

Kanban Task Board Frontend

From the original Fervor library. Examples may use older package versions.

Below is a tutorial for building the React frontend portion of your Kanban board. We’ll cover initializing the React app, setting up the project structure, implementing drag-and-drop using react‑beautiful‑dnd, managing state with CRUD operations via axios, and adding UI elements for creating, editing, and deleting tasks.


Tutorial: Building the Frontend with React

1. Initialize the React App

First, create a new React application and install the necessary packages:

npx create-react-app kanban-frontend
cd kanban-frontend
npm install react-beautiful-dnd axios

Packages Explained:

  • react-beautiful-dnd: Provides an easy-to-use drag-and-drop experience.
  • axios: Helps us perform HTTP requests to our backend API.

2. Project Structure

Organize your project into a few key components. Here’s a suggested structure:

src/
 ├── components/
 │    ├── Board.js    // Contains the overall board layout
 │    ├── Column.js   // Represents a single column (e.g., To Do, In Progress, Done)
 │    ├── Task.js     // Represents an individual task card
 │    └── TaskForm.js // (Optional) A form to add or edit tasks
 ├── App.js          // Main component that manages state and integrates the API
 └── index.js

3. Implementing Drag-and-Drop with react-beautiful-dnd

a. Wrap Your Board with <DragDropContext>

In App.js, import DragDropContext from react-beautiful-dnd. The onDragEnd function will update the state and call the API when a task is moved.

// src/App.js
import React, { useState, useEffect } from 'react';
import { DragDropContext } from 'react-beautiful-dnd';
import axios from 'axios';
import Board from './components/Board';

function App() {
  const [tasks, setTasks] = useState([]);

  // Fetch tasks from the backend on mount
  useEffect(() => {
    axios.get('http://localhost:5000/tasks')
      .then(response => setTasks(response.data))
      .catch(err => console.error(err));
  }, []);

  const onDragEnd = async (result) => {
    const { source, destination, draggableId } = result;
    // If dropped outside any droppable, do nothing.
    if (!destination) return;

    // Find the task being moved
    const updatedTask = tasks.find(task => task._id === draggableId);
    if (!updatedTask) return;

    // Update the task’s status based on the destination droppableId
    const newStatus = destination.droppableId;

    // Update local state (for immediate UI feedback)
    const updatedTasks = tasks.map(task =>
      task._id === draggableId ? { ...task, status: newStatus } : task
    );
    setTasks(updatedTasks);

    // Update the task on the server
    try {
      await axios.put(`http://localhost:5000/tasks/${draggableId}`, {
        status: newStatus,
      });
    } catch (error) {
      console.error('Error updating task status:', error);
      // Optionally, revert the change in state on error.
    }
  };

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <Board tasks={tasks} setTasks={setTasks} />
    </DragDropContext>
  );
}

export default App;

b. Creating the Board Component

The Board.js component organizes columns (e.g., “To Do”, “In Progress”, “Done”). It will filter tasks based on their status and render a corresponding Column for each.

// src/components/Board.js
import React from 'react';
import Column from './Column';

const columns = [
  { id: 'To Do', title: 'To Do' },
  { id: 'In Progress', title: 'In Progress' },
  { id: 'Done', title: 'Done' },
];

const Board = ({ tasks, setTasks }) => {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-around' }}>
      {columns.map(column => {
        const columnTasks = tasks.filter(task => task.status === column.id);
        return (
          <Column 
            key={column.id} 
            columnId={column.id} 
            title={column.title} 
            tasks={columnTasks}
            setTasks={setTasks}
          />
        );
      })}
    </div>
  );
};

export default Board;

c. Creating the Column Component

Each Column is a droppable area where tasks can be dragged in. Use Droppable from react‑beautiful‑dnd.

// src/components/Column.js
import React from 'react';
import { Droppable } from 'react-beautiful-dnd';
import Task from './Task';

const Column = ({ columnId, title, tasks, setTasks }) => {
  return (
    <div className="column" style={{ margin: '0 8px', width: '30%', border: '1px solid #ccc', borderRadius: '4px', padding: '8px' }}>
      <h2>{title}</h2>
      <Droppable droppableId={columnId}>
        {(provided) => (
          <div 
            ref={provided.innerRef} 
            {...provided.droppableProps} 
            style={{ minHeight: '100px', padding: '4px', background: '#f4f4f4' }}
          >
            {tasks.map((task, index) => (
              <Task key={task._id} task={task} index={index} />
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </div>
  );
};

export default Column;

d. Creating the Task Component

Each Task is a draggable element. Use the Draggable component from react‑beautiful‑dnd.

// src/components/Task.js
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';

const Task = ({ task, index }) => {
  return (
    <Draggable draggableId={task._id} index={index}>
      {(provided) => (
        <div 
          className="task" 
          ref={provided.innerRef} 
          {...provided.draggableProps} 
          {...provided.dragHandleProps}
          style={{
            userSelect: 'none',
            padding: '16px',
            margin: '0 0 8px 0',
            minHeight: '50px',
            backgroundColor: '#fff',
            color: '#333',
            border: '1px solid #ddd',
            borderRadius: '4px',
            ...provided.draggableProps.style
          }}
        >
          <p>{task.title}</p>
        </div>
      )}
    </Draggable>
  );
};

export default Task;

4. State Management and CRUD Operations

In the example above, we fetch tasks using axios in App.js and update the state when tasks are dragged between columns. Next, we add functionality for creating and deleting tasks.

a. Creating a Task Form

You can create a simple form component to add tasks. When the form is submitted, the task is posted to the API, and the local state is updated.

// src/components/TaskForm.js
import React, { useState } from 'react';
import axios from 'axios';

const TaskForm = ({ setTasks }) => {
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await axios.post('http://localhost:5000/tasks', {
        title,
        description,
        status: 'To Do'
      });
      // Append the new task to the existing state
      setTasks(prevTasks => [...prevTasks, response.data]);
      setTitle('');
      setDescription('');
    } catch (error) {
      console.error('Error creating task:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '16px' }}>
      <input 
        type="text" 
        placeholder="Task title" 
        value={title} 
        onChange={(e) => setTitle(e.target.value)}
        required
      />
      <input 
        type="text" 
        placeholder="Task description" 
        value={description} 
        onChange={(e) => setDescription(e.target.value)}
      />
      <button type="submit">Add Task</button>
    </form>
  );
};

export default TaskForm;

Now, include the TaskForm in your App.js (or directly in Board.js) so users can create new tasks:

// In App.js (above the Board component, for example)
import TaskForm from './components/TaskForm';

function App() {
  // ... previous code

  return (
    <DragDropContext onDragEnd={onDragEnd}>
      <div style={{ padding: '16px' }}>
        <h1>Kanban Board</h1>
        <TaskForm setTasks={setTasks} />
        <Board tasks={tasks} setTasks={setTasks} />
      </div>
    </DragDropContext>
  );
}

b. Task Editing and Deletion

For editing or deleting tasks, you can add buttons within each Task component. Here’s a simple example for deletion:

  1. Update Task Component to Include a Delete Button:
// src/components/Task.js
import React from 'react';
import { Draggable } from 'react-beautiful-dnd';
import axios from 'axios';

const Task = ({ task, index, setTasks }) => {
  const deleteTask = async () => {
    try {
      await axios.delete(`http://localhost:5000/tasks/${task._id}`);
      // Remove the deleted task from state
      setTasks(prevTasks => prevTasks.filter(t => t._id !== task._id));
    } catch (error) {
      console.error('Error deleting task:', error);
    }
  };

  return (
    <Draggable draggableId={task._id} index={index}>
      {(provided) => (
        <div 
          className="task" 
          ref={provided.innerRef} 
          {...provided.draggableProps} 
          {...provided.dragHandleProps}
          style={{
            userSelect: 'none',
            padding: '16px',
            margin: '0 0 8px 0',
            minHeight: '50px',
            backgroundColor: '#fff',
            color: '#333',
            border: '1px solid #ddd',
            borderRadius: '4px',
            ...provided.draggableProps.style
          }}
        >
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
            <p>{task.title}</p>
            <button onClick={deleteTask} style={{ marginLeft: '8px' }}>Delete</button>
          </div>
        </div>
      )}
    </Draggable>
  );
};

export default Task;
  1. Pass setTasks to Task in Column Component:

Make sure the Column component passes down the setTasks function so that deletion (or editing) can update state.

// src/components/Column.js
import React from 'react';
import { Droppable } from 'react-beautiful-dnd';
import Task from './Task';

const Column = ({ columnId, title, tasks, setTasks }) => {
  return (
    <div className="column" style={{ margin: '0 8px', width: '30%', border: '1px solid #ccc', borderRadius: '4px', padding: '8px' }}>
      <h2>{title}</h2>
      <Droppable droppableId={columnId}>
        {(provided) => (
          <div 
            ref={provided.innerRef} 
            {...provided.droppableProps} 
            style={{ minHeight: '100px', padding: '4px', background: '#f4f4f4' }}
          >
            {tasks.map((task, index) => (
              <Task key={task._id} task={task} index={index} setTasks={setTasks} />
            ))}
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </div>
  );
};

export default Column;

5. Final Thoughts

This guide covers:

  • Initializing a React app and installing necessary packages.
  • Setting up a basic project structure with components for the board, columns, and tasks.
  • Implementing drag-and-drop functionality with react-beautiful-dnd.
  • Managing state and CRUD operations with axios for API communication.
  • Adding a form and basic deletion functionality for a more interactive board.

You can extend this further with additional features like task editing, filtering, or user authentication on the frontend.

Below are a couple of “cool” feature enhancements along with sample code that you can integrate into your Kanban board:


1. Editable Task Modal

Allow users to edit a task’s title and description in a pop-up modal. This lets users quickly change details without navigating away.

a. Create a TaskEditModal Component

This component displays a modal with input fields for the task title and description. You can use a simple conditional render or a library like react-modal.

// src/components/TaskEditModal.js
import React, { useState, useEffect } from 'react';

const TaskEditModal = ({ task, isOpen, onClose, onSave }) => {
  const [title, setTitle] = useState(task.title);
  const [description, setDescription] = useState(task.description || '');

  // When task changes (or modal opens), update local state
  useEffect(() => {
    if (task) {
      setTitle(task.title);
      setDescription(task.description || '');
    }
  }, [task]);

  if (!isOpen) return null;

  const handleSave = () => {
    onSave({ ...task, title, description });
  };

  return (
    <div style={modalStyle}>
      <div style={modalContentStyle}>
        <h3>Edit Task</h3>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          style={inputStyle}
          placeholder="Title"
        />
        <textarea
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          style={textareaStyle}
          placeholder="Description"
        />
        <div style={{ marginTop: '12px' }}>
          <button onClick={handleSave} style={buttonStyle}>Save</button>
          <button onClick={onClose} style={{ ...buttonStyle, marginLeft: '8px' }}>Cancel</button>
        </div>
      </div>
    </div>
  );
};

// Simple inline styles for demonstration:
const modalStyle = {
  position: 'fixed',
  top: 0, left: 0, right: 0, bottom: 0,
  backgroundColor: 'rgba(0,0,0,0.5)',
  display: 'flex',
  alignItems: 'center',
  justifyContent: 'center',
  zIndex: 1000,
};

const modalContentStyle = {
  background: '#fff',
  padding: '20px',
  borderRadius: '4px',
  width: '300px',
};

const inputStyle = {
  width: '100%',
  padding: '8px',
  marginBottom: '8px',
};

const textareaStyle = {
  width: '100%',
  padding: '8px',
  height: '80px',
};

const buttonStyle = {
  padding: '8px 16px',
  cursor: 'pointer',
};

export default TaskEditModal;

b. Integrate TaskEditModal in Your Task Component

In your Task.js, add an “Edit” button that opens the modal. When the user saves, update the task both locally and on the server.

// src/components/Task.js
import React, { useState } from 'react';
import { Draggable } from 'react-beautiful-dnd';
import axios from 'axios';
import TaskEditModal from './TaskEditModal';

const Task = ({ task, index, setTasks }) => {
  const [isEditing, setIsEditing] = useState(false);

  const deleteTask = async () => {
    try {
      await axios.delete(`http://localhost:5000/tasks/${task._id}`);
      setTasks(prevTasks => prevTasks.filter(t => t._id !== task._id));
    } catch (error) {
      console.error('Error deleting task:', error);
    }
  };

  const handleSave = async (updatedTask) => {
    try {
      const response = await axios.put(`http://localhost:5000/tasks/${task._id}`, updatedTask);
      // Update task in local state
      setTasks(prevTasks =>
        prevTasks.map(t => (t._id === task._id ? response.data : t))
      );
      setIsEditing(false);
    } catch (error) {
      console.error('Error updating task:', error);
    }
  };

  return (
    <>
      <Draggable draggableId={task._id} index={index}>
        {(provided) => (
          <div 
            className="task" 
            ref={provided.innerRef} 
            {...provided.draggableProps} 
            {...provided.dragHandleProps}
            style={{
              userSelect: 'none',
              padding: '16px',
              margin: '0 0 8px 0',
              minHeight: '50px',
              backgroundColor: '#fff',
              color: '#333',
              border: '1px solid #ddd',
              borderRadius: '4px',
              ...provided.draggableProps.style
            }}
          >
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <p style={{ margin: 0 }}>{task.title}</p>
                {task.description && <small>{task.description}</small>}
              </div>
              <div>
                <button onClick={() => setIsEditing(true)} style={{ marginRight: '8px' }}>Edit</button>
                <button onClick={deleteTask}>Delete</button>
              </div>
            </div>
          </div>
        )}
      </Draggable>
      <TaskEditModal
        task={task}
        isOpen={isEditing}
        onClose={() => setIsEditing(false)}
        onSave={handleSave}
      />
    </>
  );
};

export default Task;

2. Adding Task Comments

Enhance your tasks by allowing users to add comments. Comments can be stored as part of the task document on the backend (if you update your model) or in a separate collection. For simplicity, let’s assume we add comments as an array inside the task.

a. Update Task Model (Backend)

On the backend, you might update your Task model (Task.js) to include comments:

// Add a comments field to TaskSchema
const TaskSchema = new mongoose.Schema({
  title: { type: String, required: true },
  description: String,
  status: { type: String, enum: ['To Do', 'In Progress', 'Done'], default: 'To Do' },
  comments: [
    {
      text: String,
      createdAt: { type: Date, default: Date.now },
    }
  ],
  createdAt: { type: Date, default: Date.now },
});

Note: If your project is already in production, you’ll need to run a migration or handle missing fields gracefully.

b. Create a Comments Component

This component allows users to view and add comments for a task.

// src/components/Comments.js
import React, { useState } from 'react';
import axios from 'axios';

const Comments = ({ task, setTasks }) => {
  const [commentText, setCommentText] = useState('');

  const addComment = async () => {
    if (!commentText.trim()) return;
    try {
      // Make API call to update the task with a new comment
      const updatedTask = {
        ...task,
        comments: [...(task.comments || []), { text: commentText }],
      };

      const response = await axios.put(`http://localhost:5000/tasks/${task._id}`, updatedTask);
      // Update local state with the updated task
      setTasks(prevTasks =>
        prevTasks.map(t => (t._id === task._id ? response.data : t))
      );
      setCommentText('');
    } catch (error) {
      console.error('Error adding comment:', error);
    }
  };

  return (
    <div style={{ marginTop: '8px' }}>
      <h4>Comments</h4>
      <ul>
        {(task.comments || []).map((c, index) => (
          <li key={index}>{c.text} <small>({new Date(c.createdAt).toLocaleString()})</small></li>
        ))}
      </ul>
      <input
        type="text"
        placeholder="Add a comment"
        value={commentText}
        onChange={(e) => setCommentText(e.target.value)}
        style={{ width: '100%', padding: '6px' }}
      />
      <button onClick={addComment} style={{ marginTop: '4px' }}>Add Comment</button>
    </div>
  );
};

export default Comments;

c. Integrate Comments in the Task Component

Below the task details, include the Comments component.

// Update Task.js to include Comments component
import Comments from './Comments';

const Task = ({ task, index, setTasks }) => {
  // ... previous code for editing and deletion

  return (
    <>
      <Draggable draggableId={task._id} index={index}>
        {(provided) => (
          <div 
            className="task" 
            ref={provided.innerRef} 
            {...provided.draggableProps} 
            {...provided.dragHandleProps}
            style={{
              userSelect: 'none',
              padding: '16px',
              margin: '0 0 8px 0',
              minHeight: '50px',
              backgroundColor: '#fff',
              color: '#333',
              border: '1px solid #ddd',
              borderRadius: '4px',
              ...provided.draggableProps.style
            }}
          >
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <p style={{ margin: 0 }}>{task.title}</p>
                {task.description && <small>{task.description}</small>}
              </div>
              <div>
                <button onClick={() => setIsEditing(true)} style={{ marginRight: '8px' }}>Edit</button>
                <button onClick={deleteTask}>Delete</button>
              </div>
            </div>
            <Comments task={task} setTasks={setTasks} />
          </div>
        )}
      </Draggable>
      <TaskEditModal
        task={task}
        isOpen={isEditing}
        onClose={() => setIsEditing(false)}
        onSave={handleSave}
      />
    </>
  );
};

export default Task;

Final Thoughts

These two features—editable task modals and task commenting—greatly enhance the interactivity and usability of your Kanban board. They not only add cool functionality but also provide excellent practice with React state management, API integration, and conditional rendering.

Feel free to extend these ideas further by:

  • Adding task priorities or labels.
  • Implementing a filtering or search feature.
  • Integrating real-time updates (e.g., using Socket.io) so that comments and edits appear for all connected users.
Keep your curiosity going.Explore more Projects →
287 TUTORIALS · 22 TOPICSREADY