et’s break down Project 1: Basic Task Manager into smaller, manageable steps. We’ll implement this project by creating the frontend (React with Redux) and backend (Node.js with Express and MongoDB).
Part 1: Backend Setup (Node.js + Express + MongoDB)
Step 1: Set Up the Backend
Create a directory for the backend:
mkdir server
cd server
npm init -y
Install necessary dependencies:
npm install express mongoose dotenv
npm install nodemon --save-dev
Create a basic project structure:
server/
├── models/
│ └── Task.js
├── routes/
│ └── tasks.js
├── controllers/
│ └── taskController.js
├── .env
├── index.js
└── package.json
Create a .env
file in the server
directory and add the MongoDB URI:
MONGO_URI=your_mongodb_connection_string
Step 2: Create the Express Server
Create
index.js
in theserver
directory:
const express = require('express');
const mongoose = require('mongoose');
const dotenv = require('dotenv');
const taskRoutes = require('./routes/tasks');
dotenv.config();
const app = express();
app.use(express.json());
// Connect to MongoDB
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true })
.then(() => console.log('MongoDB connected'))
.catch((err) => console.log(err));
// Routes
app.use('/api/tasks', taskRoutes);
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Add the “start” script to package.json
for using nodemon
:
"scripts": {
"start": "nodemon index.js"
}
Step 3: Create the Task Model
- Create
models/Task.js
:
const mongoose = require('mongoose');
const taskSchema = new mongoose.Schema({
title: { type: String, required: true },
description: String,
status: { type: String, enum: ['Pending', 'In Progress', 'Completed'], default: 'Pending' },
createdAt: { type: Date, default: Date.now },
});
module.exports = mongoose.model('Task', taskSchema);
Step 4: Create the Task Controller
- Create
controllers/taskController.js
const Task = require('../models/Task');
// Get all tasks
const getTasks = async (req, res) => {
try {
const tasks = await Task.find();
res.json(tasks);
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// Create a task
const createTask = 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 });
}
};
// Update a task
const updateTask = async (req, res) => {
try {
const task = await Task.findByIdAndUpdate(req.params.id, req.body, { new: true });
res.json(task);
} catch (err) {
res.status(400).json({ message: err.message });
}
};
// Delete a task
const deleteTask = async (req, res) => {
try {
await Task.findByIdAndDelete(req.params.id);
res.json({ message: 'Task deleted' });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
module.exports = { getTasks, createTask, updateTask, deleteTask };
Step 5: Create the Routes
- Create
routes/tasks.js
const express = require('express');
const router = express.Router();
const { getTasks, createTask, updateTask, deleteTask } = require('../controllers/taskController');
router.get('/', getTasks);
router.post('/', createTask);
router.put('/:id', updateTask);
router.delete('/:id', deleteTask);
module.exports = router;
Step 6: Test the Backend
- Run the server:
npm start
- Use a tool like Postman to test the endpoints:
GET /api/tasks
to fetch all tasks.POST /api/tasks
to create a new task.PUT /api/tasks/:id
to update a task.DELETE /api/tasks/:id
to delete a task.
With this, the backend part of the project is complete. Next, we’ll move on to the frontend using React and Redux to interact with this API.
Part 2: Frontend Setup (React + Redux)
Step 1: Set Up the Frontend with Vite
Create a new React project using Vite
npm create vite@latest task-manager-frontend --template react
cd task-manager-frontend
npm install
Install dependencies:
npm install axios redux react-redux @reduxjs/toolkit react-router-dom
Set up the project structure:
task-manager-frontend/
├── src/
│ ├── components/
│ │ ├── TaskForm.jsx
│ │ ├── TaskList.jsx
│ │ ├── TaskFilter.jsx
│ ├── features/
│ │ ├── tasks/
│ │ │ ├── tasksSlice.js
│ ├── services/
│ │ ├── taskService.js
│ ├── App.jsx
│ ├── index.jsx
├── package.json
Step 2: Set Up Redux Toolkit for State Management
Create the tasks slice: Create a new file
src/features/tasks/tasksSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const initialState = {
tasks: [],
status: 'idle',
error: null,
};
// Async actions using createAsyncThunk
export const fetchTasks = createAsyncThunk('tasks/fetchTasks', async () => {
const response = await axios.get('http://localhost:5000/api/tasks');
return response.data;
});
export const createTask = createAsyncThunk('tasks/createTask', async (task) => {
const response = await axios.post('http://localhost:5000/api/tasks', task);
return response.data;
});
export const updateTask = createAsyncThunk('tasks/updateTask', async (task) => {
const response = await axios.put(`http://localhost:5000/api/tasks/${task.id}`, task);
return response.data;
});
export const deleteTask = createAsyncThunk('tasks/deleteTask', async (taskId) => {
await axios.delete(`http://localhost:5000/api/tasks/${taskId}`);
return taskId;
});
// Task slice
const tasksSlice = createSlice({
name: 'tasks',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchTasks.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchTasks.fulfilled, (state, action) => {
state.status = 'succeeded';
state.tasks = action.payload;
})
.addCase(fetchTasks.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message;
})
.addCase(createTask.fulfilled, (state, action) => {
state.tasks.push(action.payload);
})
.addCase(updateTask.fulfilled, (state, action) => {
const index = state.tasks.findIndex(task => task._id === action.payload._id);
state.tasks[index] = action.payload;
})
.addCase(deleteTask.fulfilled, (state, action) => {
state.tasks = state.tasks.filter(task => task._id !== action.payload);
});
},
});
export default tasksSlice.reducer;
Configure Redux Store: Create a new file src/store.js
.
import { configureStore } from '@reduxjs/toolkit';
import tasksReducer from './features/tasks/tasksSlice';
export const store = configureStore({
reducer: {
tasks: tasksReducer,
},
});
Provide the Store: Update src/index.jsx
.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Step 3: Create Components
TaskForm Component: Create
src/components/TaskForm.jsx
.
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { createTask } from '../features/tasks/tasksSlice';
const TaskForm = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
dispatch(createTask({ title, description }));
setTitle('');
setDescription('');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Task title"
required
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Task description"
></textarea>
<button type="submit">Add Task</button>
</form>
);
};
export default TaskForm;
TaskList Component: Create src/components/TaskList.jsx
.
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTasks, deleteTask } from '../features/tasks/tasksSlice';
const TaskList = () => {
const dispatch = useDispatch();
const tasks = useSelector((state) => state.tasks.tasks);
const taskStatus = useSelector((state) => state.tasks.status);
useEffect(() => {
if (taskStatus === 'idle') {
dispatch(fetchTasks());
}
}, [taskStatus, dispatch]);
const handleDelete = (taskId) => {
dispatch(deleteTask(taskId));
};
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>
<h3>{task.title}</h3>
<p>{task.description}</p>
<button onClick={() => handleDelete(task._id)}>Delete</button>
</li>
))}
</ul>
);
};
export default TaskList;
App Component: Update src/App.jsx
.
import React from 'react';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
const App = () => {
return (
<div>
<h1>Task Manager</h1>
<TaskForm />
<TaskList />
</div>
);
};
export default App;
Step 4: Test the Application
Run the backend:
cd server
npm start
Run the frontend:
cd task-manager-frontend
npm run dev
Next Steps: Adding More Features
- Task Status: Add a dropdown in the
TaskForm
to select the task status (Pending, In Progress, Completed). - Task Editing: Implement editing functionality for tasks.
- Theme Toggle: Add a light/dark theme toggle using context or Redux.
- Filtering: Implement a filtering component to display tasks based on their status.
Step 1: Add Task Status (Select Dropdown in TaskForm)
Update the
TaskForm
component to include a status dropdown. Modifysrc/components/TaskForm.jsx
:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { createTask } from '../features/tasks/tasksSlice';
const TaskForm = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [status, setStatus] = useState('Pending');
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
dispatch(createTask({ title, description, status }));
setTitle('');
setDescription('');
setStatus('Pending');
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Task title"
required
/>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Task description"
></textarea>
<select value={status} onChange={(e) => setStatus(e.target.value)}>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
</select>
<button type="submit">Add Task</button>
</form>
);
};
export default TaskForm;
Update the
tasksSlice
insrc/features/tasks/tasksSlice.js
to handle thestatus
field. The existing code for creating and fetching tasks already accommodates this change, as we are passing thestatus
field with the request.
Step 2: Implement Task Editing
- Add an edit form that will appear when the user clicks an “Edit” button next to each task.
- Modify the
TaskList
component to include an “Edit” button and a simple inline form for editing. Updatesrc/components/TaskList.jsx
:
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTasks, deleteTask, updateTask } from '../features/tasks/tasksSlice';
const TaskList = () => {
const dispatch = useDispatch();
const tasks = useSelector((state) => state.tasks.tasks);
const taskStatus = useSelector((state) => state.tasks.status);
const [editingTask, setEditingTask] = useState(null);
const [editTitle, setEditTitle] = useState('');
const [editDescription, setEditDescription] = useState('');
const [editStatus, setEditStatus] = useState('Pending');
useEffect(() => {
if (taskStatus === 'idle') {
dispatch(fetchTasks());
}
}, [taskStatus, dispatch]);
const handleEditClick = (task) => {
setEditingTask(task._id);
setEditTitle(task.title);
setEditDescription(task.description);
setEditStatus(task.status);
};
const handleEditSubmit = (e) => {
e.preventDefault();
dispatch(updateTask({ id: editingTask, title: editTitle, description: editDescription, status: editStatus }));
setEditingTask(null);
};
const handleDelete = (taskId) => {
dispatch(deleteTask(taskId));
};
return (
<ul>
{tasks.map((task) => (
<li key={task._id}>
{editingTask === task._id ? (
<form onSubmit={handleEditSubmit}>
<input
type="text"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
required
/>
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
></textarea>
<select value={editStatus} onChange={(e) => setEditStatus(e.target.value)}>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
</select>
<button type="submit">Save</button>
</form>
) : (
<>
<h3>{task.title}</h3>
<p>{task.description}</p>
<p>Status: {task.status}</p>
<button onClick={() => handleEditClick(task)}>Edit</button>
<button onClick={() => handleDelete(task._id)}>Delete</button>
</>
)}
</li>
))}
</ul>
);
};
export default TaskList;
Step 3: Add Light/Dark Theme Toggle
Create a theme context: Create a new file
src/context/ThemeContext.jsx
.
import React, { createContext, useState, useContext } from 'react';
const ThemeContext = createContext();
export const useTheme = () => {
return useContext(ThemeContext);
};
export const ThemeProvider = ({ children }) => {
const [isDarkTheme, setIsDarkTheme] = useState(false);
const toggleTheme = () => {
setIsDarkTheme((prevTheme) => !prevTheme);
};
return (
<ThemeContext.Provider value={{ isDarkTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
Wrap the App with ThemeProvider
: Modify src/index.jsx
.
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
import { ThemeProvider } from './context/ThemeContext';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<ThemeProvider>
<App />
</ThemeProvider>
</Provider>
</React.StrictMode>
);
Add the Theme Toggle Button: Modify src/App.jsx
.
import React from 'react';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
import { useTheme } from './context/ThemeContext';
import './App.css';
const App = () => {
const { isDarkTheme, toggleTheme } = useTheme();
return (
<div className={isDarkTheme ? 'dark-theme' : 'light-theme'}>
<h1>Task Manager</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
<TaskForm />
<TaskList />
</div>
);
};
export default App;
Add basic CSS for themes: Create or update src/App.css
.
.light-theme {
background-color: white;
color: black;
}
.dark-theme {
background-color: #333;
color: white;
}
Step 4: Implement Filtering by Task Status
Create the
TaskFilter
component: Createsrc/components/TaskFilter.jsx
.
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
const TaskFilter = () => {
const [filter, setFilter] = useState('All');
const tasks = useSelector((state) => state.tasks.tasks);
const filteredTasks = tasks.filter((task) => {
if (filter === 'All') return true;
return task.status === filter;
});
return (
<div>
<select value={filter} onChange={(e) => setFilter(e.target.value)}>
<option value="All">All</option>
<option value="Pending">Pending</option>
<option value="In Progress">In Progress</option>
<option value="Completed">Completed</option>
</select>
<ul>
{filteredTasks.map((task) => (
<li key={task._id}>
<h3>{task.title}</h3>
<p>{task.description}</p>
<p>Status: {task.status}</p>
</li>
))}
</ul>
</div>
);
};
export default TaskFilter;
Add the TaskFilter
to the App: Update src/App.jsx
.
import React from 'react';
import TaskForm from './components/TaskForm';
import TaskList from './components/TaskList';
import TaskFilter from './components/TaskFilter';
import { useTheme } from './context/ThemeContext';
import './App.css';
const App = () => {
const { isDarkTheme, toggleTheme } = useTheme();
return (
<div className={isDarkTheme ? 'dark-theme' : 'light-theme'}>
<h1>Task Manager</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
<TaskForm />
<TaskFilter />
<TaskList />
</div>
);
};
export default App;
Testing the Enhanced Application
Run the backend server:
cd server
npm start
Run the frontend:
cd task-manager-frontend
npm run dev
Welcome to DevTechTutor.com, your ultimate resource for mastering web development and technology! Whether you're a beginner eager to dive into coding or an experienced developer looking to sharpen your skills, DevTechTutor.com is here to guide you every step of the way. Our mission is to make learning web development accessible, engaging, and effective.