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.
🛠️ 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
- Create a
backendfolder. - Run
npm init -y. - 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.
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.
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.
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
- Create a
frontendfolder withnpx create-react-app frontend. - 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.cssbody {
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.