Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions data/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,21 @@
"subtasks": [],
"createdAt": "2025-07-23T04:22:47.898Z",
"updatedAt": "2025-07-23T04:22:47.899Z"
},
{
"id": "me2rq0pr67vivr",
"title": "Write poetry for Abdul",
"description": "Express feelings through verses",
"status": "pending",
"priority": "high",
"dueDate": "2025-08-10",
"assignedTo": "Abdullahi",
"subtasks": [
"Draft lines",
"Add metaphor",
"Review rhythm"
],
"createdAt": "2025-08-08T11:54:18.783Z",
"updatedAt": "2025-08-08T11:54:18.783Z"
}
]
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
"author": "Duraan",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"body-parser": "^1.20.2",
"cors": "^2.8.5",
"express": "^4.18.2",
"node-fetch": "^3.3.2"
},
"devDependencies": {
Expand Down
318 changes: 123 additions & 195 deletions routes/tasks.js
Original file line number Diff line number Diff line change
@@ -1,232 +1,160 @@
import express from "express";
import fs from "fs/promises";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";

const router = express.Router();

// ES module __dirname equivalent
// In CommonJS, __dirname is automatically available, but in ES modules we need to create it manually
// import.meta.url gives us the URL of the current module file
// fileURLToPath converts the URL to a file path string
// path.dirname gets the directory name from the file path
// Recreate __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Path to the data file
// path.join combines the current directory with the relative path to our data file
// "../data/tasks.json" means: go up one directory, then into 'data' folder, then 'tasks.json'
const dataFilePath = path.join(__dirname, "../data/tasks.json");
const router = express.Router();
const tasksFilePath = path.join(__dirname, "../data/tasks.json");
const tasksData = await fs.promises.readFile(tasksFilePath, "utf8");
const tasks = JSON.parse(tasksData);

// Helper function to read tasks from JSON file
// fs.readFile reads the file asynchronously and returns a promise
// JSON.parse converts the string data back to a JavaScript array
// If file doesn't exist or has errors, return empty array
async function getAllTasks() {
try {
const data = await fs.readFile(dataFilePath, "utf8");
return JSON.parse(data);
} catch (error) {
return [];

// Simple ID generator
const generateId = () => `${Date.now()}`;

/* GET ALL TASKS */
router.get("/tasks", async(req, res) => {
const { status, priority, assignedTo } = req.query;

let filteredTasks = tasks;

if (status) {
filteredTasks = filteredTasks.filter(task => task.status === status);
}
}

// Helper function to write tasks to JSON file
// JSON.stringify converts the array back to a string with nice formatting (null, 2)
// fs.writeFile writes the data to the file asynchronously
async function writeTasks(tasks) {
await fs.writeFile(dataFilePath, JSON.stringify(tasks, null, 2));
}

// Helper function to validate task data
// This function checks if the required fields are present and valid
function validateTaskData(taskData) {
const requiredFields = ["title", "description", "status", "priority"];
const validStatuses = ["pending", "in-progress", "completed", "cancelled"];
const validPriorities = ["low", "medium", "high", "urgent"];

// Check if all required fields are present
for (const field of requiredFields) {
if (!taskData[field]) {
return { isValid: false, error: `Missing required field: ${field}` };
}
if (priority) {
filteredTasks = filteredTasks.filter(task => task.priority === priority);
}

// Validate status
if (!validStatuses.includes(taskData.status)) {
return {
isValid: false,
error: `Invalid status. Must be one of: ${validStatuses.join(", ")}`,
};
if (assignedTo) {
filteredTasks = filteredTasks.filter(task => task.assignedTo === assignedTo);
}

// Validate priority
if (!validPriorities.includes(taskData.priority)) {
return {
isValid: false,
error: `Invalid priority. Must be one of: ${validPriorities.join(", ")}`,
};
}
res.json({ success: true, data: filteredTasks });
});

return { isValid: true };
}
/* GET TASK BY ID */
router.get("/tasks/:id", async(req, res) => {
const task = tasks.find(t => t.id === req.params.id);

// GET /api/tasks - Get all tasks
// This route handles GET requests to /api/tasks
// req = request object (contains data sent by client)
// res = response object (used to send data back to client)
router.get("/tasks", async (req, res) => {
try {
const tasks = await getAllTasks();
if (!task) {
return res.status(404).json({ success: false, error: "Task not found" });
}

// Add query parameter support for filtering
let filteredTasks = tasks;
res.json({ success: true, data: task });
});

// Filter by status if provided
if (req.query.status) {
filteredTasks = filteredTasks.filter(
(task) => task.status === req.query.status
);
/* POST TASK */
router.post("/tasks", async(req, res) => {
try {
const {
title,
description,
status,
priority,
assignedTo,
dueDate,
subtasks,
} = req.body;

if (!title || !description || !status || !priority) {
return res.status(400).json({
success: false,
error: "Missing required field: title, description, status or priority",
});
}

// Filter by priority if provided
if (req.query.priority) {
filteredTasks = filteredTasks.filter(
(task) => task.priority === req.query.priority
);
}
const now = new Date().toISOString();
const newTask = {
id: generateId(),
title,
description,
status,
priority,
assignedTo: assignedTo || null,
dueDate: dueDate || null,
subtasks: Array.isArray(subtasks) ? subtasks : [],
createdAt: now,
updatedAt: now,
};

// Filter by assignedTo if provided
if (req.query.assignedTo) {
filteredTasks = filteredTasks.filter((task) =>
task.assignedTo
.toLowerCase()
.includes(req.query.assignedTo.toLowerCase())
);
}
tasks.push(newTask);
fs.writeFileSync(tasksFilePath, JSON.stringify(tasks, null, 2));

res.json({
success: true,
count: filteredTasks.length,
data: filteredTasks,
});
} catch (error) {
res.status(500).json({
success: false,
error: "Error retrieving tasks",
});
res.status(201).json({ success: true, message: "Task created", data: newTask });
} catch (err) {
console.error("POST error:", err);
res.status(500).json({ success: false, message: "Something went wrong!", error: err.message });
}
});

// GET /api/tasks/:id - Get task by ID
// :id is a route parameter - it captures the value from the URL
// Example: /api/tasks/1 will set req.params.id = "1"
router.get("/tasks/:id", async (req, res) => {
/* PUT TASK */
router.put("/tasks/:id", async(req, res) => {
try {
const { id } = req.params; // Extract the ID from the URL
const tasks = await getAllTasks();
const task = tasks.find((task) => task.id === id); // Find task with matching ID
const taskIndex = tasks.findIndex(t => t.id === req.params.id);

if (!task) {
return res.status(404).json({
if (taskIndex === -1) {
return res.status(404).json({ success: false, error: "Task not found" });
}

const {
title,
description,
status,
priority,
assignedTo,
dueDate,
subtasks,
} = req.body;

if (!title || !description || !status || !priority) {
return res.status(400).json({
success: false,
error: "Task not found",
}); // 404 = Not Found
error: "Missing required field: title, description, status or priority",
});
}

res.json({
success: true,
data: task,
});
} catch (error) {
res.status(500).json({
success: false,
error: "Error retrieving task",
}); // 500 = Server Error
}
});
const updatedTask = {
id: tasks[taskIndex].id,
title,
description,
status,
priority,
assignedTo: assignedTo || null,
dueDate: dueDate || null,
subtasks: Array.isArray(subtasks) ? subtasks : [],
createdAt: tasks[taskIndex].createdAt,
updatedAt: new Date().toISOString(),
};

// POST /api/tasks - Create new task
// POST requests are used to create new resources
// req.body contains the data sent in the request body
router.post("/tasks", async (req, res) => {
try {
// TODO: Implement task creation
// 1. Extract data from req.body (title, description, status, priority, etc.)
// 2. Validate the data using validateTaskData function
// 3. Get all existing tasks using getAllTasks()
// 4. Generate a new ID for the task
// 5. Create a new task object with all required fields
// 6. Add the task to the tasks array
// 7. Save to file using writeTasks()
// 8. Send success response with status 201

// Temporary response - remove this when you implement the above
res.status(501).json({
success: false,
error:
"POST endpoint not implemented yet - implement task creation above",
});
} catch (error) {
res.status(500).json({
success: false,
error: "Error creating task",
}); // 500 = Server Error
}
});
tasks[taskIndex] = updatedTask;
fs.writeFileSync(tasksFilePath, JSON.stringify(tasks, null, 2));

// PUT /api/tasks/:id - Update task
// PUT requests are used to update existing resources
// The entire resource is replaced with the new data
router.put("/tasks/:id", async (req, res) => {
try {
// TODO: Implement task update
// 1. Extract the task ID from req.params
// 2. Get the update data from req.body
// 3. Validate the data if status or priority is being updated
// 4. Get all tasks and find the task by ID
// 5. Check if task exists, return 404 if not found
// 6. Update the task with new data
// 7. Save to file using writeTasks()
// 8. Send success response with the updated task

// Temporary response - remove this when you implement the above
res.status(501).json({
success: false,
error: "PUT endpoint not implemented yet - implement task update above",
});
} catch (error) {
res.status(500).json({
success: false,
error: "Error updating task",
}); // 500 = Server Error
res.json({ success: true, message: "Task updated", data: updatedTask });
} catch (err) {
console.error("PUT error:", err);
res.status(500).json({ success: false, message: "Something went wrong!", error: err.message });
}
});

// DELETE /api/tasks/:id - Delete task
// DELETE requests are used to remove resources
router.delete("/tasks/:id", async (req, res) => {
/* DELETE TASK BY ID */
router.delete("/tasks/:id", async(req, res) => {
try {
// TODO: Implement task deletion
// 1. Extract the task ID from req.params
// 2. Get all tasks and find the task by ID
// 3. Check if task exists, return 404 if not found
// 4. Store the task before deletion (for response)
// 5. Remove the task from the array
// 6. Save to file using writeTasks()
// 7. Send success response with the deleted task

// Temporary response - remove this when you implement the above
res.status(501).json({
success: false,
error:
"DELETE endpoint not implemented yet - implement task deletion above",
});
} catch (error) {
res.status(500).json({
success: false,
error: "Error deleting task",
}); // 500 = Server Error
const taskIndex = tasks.findIndex(t => t.id === req.params.id);

if (taskIndex === -1) {
return res.status(404).json({ success: false, error: "Task not found" });
}

const deletedTask = tasks.splice(taskIndex, 1)[0];
fs.writeFileSync(tasksFilePath, JSON.stringify(tasks, null, 2));

res.json({ success: true, message: "Task deleted", data: deletedTask });
} catch (err) {
console.error("DELETE error:", err);
res.status(500).json({ success: false, message: "Something went wrong!", error: err.message });
}
});

Expand Down