Project Idea: Trello-Style Task Manager

This is a highly visual and interactive project. You will build a "Kanban" board, famously used by Trello, where users can create tasks and move them between columns like "To Do," "In Progress," and "Done." The core of this project is mastering a drag-and-drop library and managing the state of all columns and tasks.

✨ Key Features

  • Display multiple columns (e.g., "To Do", "In Progress", "Done").
  • Allow users to create new tasks (cards).
  • Implement drag-and-drop functionality to move tasks between columns.
  • Implement drag-and-drop to re-order tasks within the same column.
  • Persist all changes to a database.

💻 Tech Stack & Skills

  • Tech Stack: React, `react-beautiful-dnd` (Drag & Drop), Express, Node.js, MongoDB, `axios`.
  • Skills Practiced: Complex State Management, UI interactivity, Using third-party libraries (Drag & Drop), Full-stack CRUD operations, API design for nested data.
React React
React Beautiful DND Drag & Drop
Node.js Node.js
MongoDB MongoDB

🛠️ Full Step-by-Step Tutorial

Part 1: The Backend API (Node.js & Express)

This will be a simple CRUD API to manage our tasks.

Step 1.1: Setup Project

  1. Create a backend folder.
  2. Run npm init -y.
  3. Run npm install express mongoose cors dotenv nodemon.

Step 1.2: Create the Task Model

Create backend/models/Task.js. The status field is key here.

backend/models/Task.js
const mongoose = require('mongoose');

const taskSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    description: {
        type: String,
    },
    status: {
        type: String,
        enum: ['todo', 'inprogress', 'done'],
        default: 'todo',
    },
    // We can add order/index later if needed
});

module.exports = mongoose.model('Task', taskSchema);

Step 1.3: Create Routes

Create backend/routes/tasks.js. We need to GET all tasks, POST a new one, and PATCH (update) a task's status.

backend/routes/tasks.js
const express = require('express');
const router = express.Router();
const Task = require('../models/Task');

// GET all tasks
router.get('/', async (req, res) => {
    try {
        const tasks = await Task.find();
        res.json(tasks);
    } catch (err) {
        res.status(500).json({ message: err.message });
    }
});

// POST a new task
router.post('/', async (req, res) => {
    const task = new Task({
        title: req.body.title,
        description: req.body.description,
        status: 'todo', // Default to 'todo'
    });
    try {
        const newTask = await task.save();
        res.status(201).json(newTask);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});

// PATCH (Update) a task's status
// This is the most important route for drag-and-drop
router.patch('/:id/status', async (req, res) => {
    try {
        const task = await Task.findById(req.params.id);
        if (!task) return res.status(404).json({ message: 'Task not found' });

        task.status = req.body.status;
        const updatedTask = await task.save();
        res.json(updatedTask);
    } catch (err) {
        res.status(400).json({ message: err.message });
    }
});

module.exports = router;

Step 1.4: Setup `index.js` (Server)

Create backend/index.js. This is similar to the URL Shortener project.

backend/index.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');

const app = express();
const PORT = process.env.PORT || 5000;

// Middlewares
app.use(cors());
app.use(express.json());

// DB Connect
mongoose.connect(process.env.MONGO_URI, { /* ... options ... */ })
    .then(() => console.log('MongoDB Connected'))
    .catch(err => console.log(err));

// Use Routes
app.use('/api/tasks', require('./routes/tasks'));

app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

Your backend is ready! (Don't forget your .env file with MONGO_URI).

Part 2: The Frontend (React & `react-beautiful-dnd`)

This part is complex. Pay close attention to the state management.

Step 2.1: Setup Project

  1. Create a frontend folder with npx create-react-app frontend.
  2. Install libraries:
    npm install axios react-beautiful-dnd

Step 2.2: `App.js` - State and Data Fetching

We will manage all state in `App.js`. We need to store tasks in a specific structure that `react-beautiful-dnd` understands: an object where keys are column IDs.

frontend/src/App.js (Imports and State)
import React, { useState, useEffect } from 'react';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import axios from 'axios';
import './App.css'; // We'll create this

const API_URL = 'http://localhost:5000/api/tasks';

function App() {
    const [columns, setColumns] = useState({
        todo: {
            id: 'todo',
            title: 'To Do',
            tasks: [],
        },
        inprogress: {
            id: 'inprogress',
            title: 'In Progress',
            tasks: [],
        },
        done: {
            id: 'done',
            title: 'Done',
            tasks: [],
        },
    });

    // Fetch tasks from backend on component mount
    useEffect(() => {
        const fetchTasks = async () => {
            try {
                const response = await axios.get(API_URL);
                const tasks = response.data;
                
                // Organize tasks into columns
                const newColumns = { ...columns };
                // Clear existing tasks first
                newColumns.todo.tasks = [];
                newColumns.inprogress.tasks = [];
                newColumns.done.tasks = [];

                tasks.forEach(task => {
                    newColumns[task.status].tasks.push(task);
                });
                
                setColumns(newColumns);
            } catch (err) {
                console.error("Failed to fetch tasks", err);
            }
        };
        fetchTasks();
    }, []); // Empty dependency array means this runs once

    // ... (onDragEnd function will go here)
    // ... (JSX will go here)
}
export default App;

Step 2.3: `App.js` - The `onDragEnd` Function (The Magic)

This function runs *after* you drop a card. It handles all the logic.

frontend/src/App.js (Add inside App component)
    const onDragEnd = async (result) => {
        const { source, destination, draggableId } = result;

        // 1. If dropped outside of any column
        if (!destination) {
            return;
        }

        // 2. If dropped in the same place
        if (source.droppableId === destination.droppableId && source.index === destination.index) {
            return;
        }

        const sourceColumn = columns[source.droppableId];
        const destColumn = columns[destination.droppableId];
        const task = sourceColumn.tasks.find(t => t._id === draggableId);

        // 3. If moving within the same column
        if (source.droppableId === destination.droppableId) {
            const newTasks = Array.from(sourceColumn.tasks);
            newTasks.splice(source.index, 1); // Remove item
            newTasks.splice(destination.index, 0, task); // Add item

            const newColumn = {
                ...sourceColumn,
                tasks: newTasks,
            };

            setColumns({
                ...columns,
                [newColumn.id]: newColumn,
            });
            // (Note: We are not persisting same-column order in this simple backend)
        } else {
            // 4. If moving to a different column
            const sourceTasks = Array.from(sourceColumn.tasks);
            sourceTasks.splice(source.index, 1); // Remove from source

            const destTasks = Array.from(destColumn.tasks);
            destTasks.splice(destination.index, 0, task); // Add to destination

            setColumns({
                ...columns,
                [source.droppableId]: {
                    ...sourceColumn,
                    tasks: sourceTasks,
                },
                [destination.droppableId]: {
                    ...destColumn,
                    tasks: destTasks,
                },
            });

            // 5. Update the backend
            try {
                await axios.patch(`${API_URL}/${task._id}/status`, { 
                    status: destination.droppableId 
                });
            } catch (err) {
                console.error("Failed to update task status", err);
                // (Optional: revert state on error)
            }
        }
    };

Step 2.4: `App.js` - The JSX (HTML)

Finally, we render the board using `react-beautiful-dnd` components.

frontend/src/App.js (Add inside App component, after onDragEnd)
    return (
        
{Object.values(columns).map(column => ( {(provided, snapshot) => (

{column.title}

{column.tasks.map((task, index) => ( {(provided) => (

{task.title}

{task.description}

)}
))} {provided.placeholder}
)}
))}
); }

Step 2.5: Basic Styling (`App.css`)

frontend/src/App.css
body {
    background-color: #020617; /* Dark background */
    color: #e2e8f0;
    font-family: 'Inter', sans-serif;
}

.board-container {
    display: flex;
    justify-content: space-around;
    padding: 20px;
    gap: 20px;
}

.column {
    width: 300px;
    background-color: #0f172a; /* Card background */
    border: 1px solid #1e293b;
    border-radius: 12px;
    padding: 15px;
    min-height: 500px;
}

.column-title {
    color: #fff;
    text-align: center;
    padding-bottom: 10px;
    border-bottom: 2px solid #334155;
}

.task-card {
    background-color: #1e293b;
    border: 1px solid #334155;
    border-radius: 8px;
    padding: 15px;
    margin-bottom: 10px;
    color: #cbd5e1;
}
.task-card h4 {
    margin: 0 0 5px 0;
    color: #fff;
}
.task-card p {
    margin: 0;
    font-size: 0.9rem;
}

Done! Run both backend (nodemon index.js) and frontend (npm start) and you'll have a fully persistent drag-and-drop board.

🔥 Next Steps & Challenges

After completing this tutorial, try these ideas:

  • **Add a "Create Task" Form:** Add a form to the "To Do" column to create new tasks directly from the UI (and POST to /api/tasks).
  • **User Accounts:** Allow users to sign up and have their own private boards.
  • **Persistence:** Save the *order* of tasks in each column to the database (you'd need to add an `order` field to your model).
  • **Rich Tasks:** Add features like due dates, labels, and descriptions to each task.