Compare commits

...

5 Commits

Author SHA1 Message Date
abd3458606 stuff 2026-05-07 16:40:27 +08:00
8841b571c9 update page title to use app name 2026-05-05 17:22:37 +08:00
cf2df504b1 add task delete modal and alert 2026-05-05 17:00:42 +08:00
584ef9cbb7 changes 2026-05-05 16:40:41 +08:00
597df4e886 Enhance API documentation and error handling in task routes 2026-05-05 16:40:41 +08:00
12 changed files with 171 additions and 102 deletions

BIN
Documentation.docx Normal file

Binary file not shown.

BIN
Documentation.pdf Normal file

Binary file not shown.

View File

@@ -41,124 +41,141 @@ taskSchema.index({ dateCreated: 1 })
const Task = mongoose.model("Task", taskSchema);
// this api route gets all the tasks from our database.
// if the request included a query string, use that for our sorting.
app.get('/api/tasks', async (req, res) => {
try {
const { sortBy } = req.query; // ?sortBy=dueDate or ?sortBy=dateCreated
let sortOption = {};
const { sortBy } = req.query; // destructs the sortBy from the query string. e.g. ?sortBy=dueDate or ?sortBy=dateCreated
let sortOption = {}; // create an empty sort object for the database query
if (sortBy === 'dueDate') {
sortOption = { dueDate: 1 }; // Ascending
sortOption = { dueDate: 1 }; // if the sortBy query string is dueDate, set the sortOption to dueDate and set the order to ascending (1)
} else if (sortBy === 'dateCreated') {
sortOption = { dateCreated: 1 };
sortOption = { dateCreated: 1 }; // if the sortBy query string is dateCreated, set the sortOption to dateCreated and set the order to ascending (1)
}
const tasks = await Task.find({}).sort(sortOption);
const tasks = await Task.find({}).sort(sortOption); // fetch all task documents and sort them using sortOption
if (!tasks) {
// return the following if the query failed to return a value
return res.status(404).json({ message: "Tasks not found!" });
}
res.json(tasks);
res.json(tasks); // send a json response with the task objects
} catch (error) {
// if there are any errors, do the following
console.error("Error:", error);
res.status(500).json({ message: "Error grabbing tasks!" });
}
});
// this api route takes post requests to save a new MongoDB document, aka, our task
app.post('/api/tasks/todo', async (req, res) => {
try {
const { title, description, dueDate } = req.body;
const taskData = { title, description, dueDate };
const createTask = new Task(taskData);
const newTask = await createTask.save();
const { title, description, dueDate } = req.body; //destructures the request body
const taskData = { title, description, dueDate }; // creating the object to be used for creating the task
const createTask = new Task(taskData); // instantiates a new Task document/model instance using the taskData object values
const newTask = await createTask.save(); // this saves the new document to MongoDB using the Task model/schema.
res.json({ task: newTask, message: "New task created successfully!" });
res.json({ task: newTask, message: "New task created successfully!" }); // if there were no error above, send a response where the body is an JSON object.
} catch (error) {
// if there are any errors, do the following
console.error("Error:", error);
res.status(500).json({ message: "Error creating the task!" });
}
});
// To 'complete' the task and move columns ↓
// this api route marks a task as complete using its id.
app.patch('/api/tasks/complete/:id', async (req, res) => {
try {
const { completed } = req.body;
const taskId = req.params.id;
const completedTask = await Task.findByIdAndUpdate(taskId, { completed }, { new: true });
const { completed } = req.body; // expected boolean value coming from the frontend
const taskId = req.params.id; // grab the task id from the URL params
const completedTask = await Task.findByIdAndUpdate(taskId, { completed }, { new: true }); // update completed status and return updated document
if (!completedTask) {
// return 404 if no task matches the provided id
return res.status(404).json({ message: "Task not found!" });
}
// return updated task document and confirmation message
res.json({ task: completedTask, message: "Task set to 'Complete'" });
} catch (error) {
// return 500 if database update fails
console.error("Error:", error);
res.status(500).json({ message: "Error completing the task!" });
}
});
// To make the task 'not complete' and move columns ↓
// this api route marks a task as not complete using its id.
app.patch('/api/tasks/notComplete/:id', async (req, res) => {
try {
const { completed } = req.body;
const taskId = req.params.id;
const taskNotComplete = await Task.findByIdAndUpdate(taskId, { completed }, { new: true });
const { completed } = req.body; // expected boolean value coming from the frontend
const taskId = req.params.id; // grab the task id from the URL params
const taskNotComplete = await Task.findByIdAndUpdate(taskId, { completed }, { new: true }); // update completed status and return updated document
if (!taskNotComplete) {
// return 404 if no task matches the provided id
return res.status(404).json({ message: "Task not found!" });
}
// return updated task document and confirmation message
res.json({ task: taskNotComplete, message: "Task set to 'Not Complete'" });
} catch (error) {
// return 500 if database update fails
console.error("Error:", error);
res.status(500).json({ message: "Error making the task NOT complete!" });
}
});
// this api route deletes a task by id.
app.delete('/api/tasks/delete/:id', async (req, res) => {
try {
const taskId = req.params.id;
const deletedTask = await Task.findByIdAndDelete(taskId);
const taskId = req.params.id; // grab the task id from the URL params
const deletedTask = await Task.findByIdAndDelete(taskId); // delete the matching task document
if (!deletedTask) {
// return 404 if no task matches the provided id
return res.status(404).json({ message: "Task not found!" });
}
// return deleted task document and confirmation message
res.json({ task: deletedTask, message: "Task deleted successfully!" });
} catch (error) {
// return 500 if deletion fails
console.error("Error:", error);
res.status(500).json({ message: "Error deleting the task!" });
}
});
// To edit exisiting task and update
// this api route updates a task's editable fields by id.
app.put('/api/tasks/update/:id', async (req, res) => {
try {
const taskId = req.params.id;
const { title, description, dueDate } = req.body; // Extract data from front end request
const taskData = { title, description, dueDate };
const updatedTask = await Task.findByIdAndUpdate(taskId, taskData, { new: true });
const taskId = req.params.id; // grab the task id from the URL params
const { title, description, dueDate } = req.body; // extract updated fields from request body
const taskData = { title, description, dueDate }; // build the update object
const updatedTask = await Task.findByIdAndUpdate(taskId, taskData, { new: true }); // apply update and return updated document
if (!updatedTask) {
// return 404 if no task matches the provided id
return res.status(404).json({ message: "Task not found!" });
}
// return updated task document and confirmation message
res.json({ task: updatedTask, message: "Task updated successfully!" });
} catch (error) {
// return 500 if update fails
console.error('Error:', error);
res.status(500).json({ message: "Error updating the task!" });

View File

@@ -57,6 +57,10 @@ footer {
color: var(--accent);
}
.navbar-nav .nav-link.active {
color: white;
}
.navbar-toggler {
border: none !important;
box-shadow: none !important;

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Dashboard</title>
<title>BusketLisk &#x2022; Dashboard</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />
@@ -92,7 +92,8 @@
<div class="d-flex gap-2 flex-wrap mb-3">
<button type="button" class="btn btn-outline-light quick-date-btn" data-quick="today">Today</button>
<button type="button" class="btn btn-outline-light quick-date-btn" data-quick="tomorrow">Tomorrow</button>
<button type="button" class="btn btn-outline-light quick-date-btn" data-quick="nextWeek">Next week</button>
<button type="button" class="btn btn-outline-light quick-date-btn" data-quick="nextWeek">Next
week</button>
</div>
<button type="submit" class="btn btn-primary shadow-sm px-5">
Create Task
@@ -156,6 +157,28 @@
</div>
</div>
</footer>
<!-- Toast-style alert container, populated by showAlert() in api.js -->
<div id="alertContainer" class="position-fixed top-0 end-0 p-3" style="z-index: 1080;"></div>
<!-- Delete confirmation modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteTaskModalLabel">Delete Task</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-0">Are you sure you want to delete this task? This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button id="confirmDeleteButton" type="button" class="btn btn-danger">Delete</button>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editTaskModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
@@ -167,7 +190,8 @@
<div class="modal-body">
<form id="editTaskForm">
<input id="editTaskName" placeholder="Task Name" class="mb-2 form-control shadow-sm" type="text">
<textarea id="editTaskDescription" placeholder="Task Description" rows="7" class="form-control mb-2 shadow-sm"></textarea>
<textarea id="editTaskDescription" placeholder="Task Description" rows="7"
class="form-control mb-2 shadow-sm"></textarea>
<input id="editDueDate" placeholder="Task Name" class="mb-3 form-control shadow-sm" type="date">
</form>
</div>

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Features</title>
<title>BusketLisk &#x2022; Features</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Home</title>
<title>BusketLisk &#x2022; Home</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />
@@ -50,7 +50,8 @@
<section class="d-flex justify-content-center flex-column align-items-center bg-img text-center px-3">
<h1 class="display-2 text-white">Stay organised with BucketLisk</h1>
<p class="lead text-white-50 col-12 col-md-8 col-lg-6 mt-3">
The simple, no-nonsense to-do app that keeps your team on track, from morning stand-up to end-of-quarter shipping.
The simple, no-nonsense to-do app that keeps your team on track, from morning stand-up to end-of-quarter
shipping.
</p>
<div class="d-flex justify-content-center align-items-center gap-3 mt-4 flex-wrap">
<a href="./dashboard.html" class="btn btn-primary btn-lg px-5">Try it for free</a>
@@ -102,9 +103,11 @@
When you're done, tick it off and move it to the completed column to keep the momentum going.
</p>
<ul class="list-unstyled">
<li class="mb-2"><img src="./images/Tick.svg" alt="" width="18" class="me-2">Sort by due date or created date</li>
<li class="mb-2"><img src="./images/Tick.svg" alt="" width="18" class="me-2">Sort by due date or created
date</li>
<li class="mb-2"><img src="./images/Tick.svg" alt="" width="18" class="me-2">Edit any task inline</li>
<li class="mb-2"><img src="./images/Tick.svg" alt="" width="18" class="me-2">Works on desktop, tablet and mobile</li>
<li class="mb-2"><img src="./images/Tick.svg" alt="" width="18" class="me-2">Works on desktop, tablet and
mobile</li>
</ul>
<a href="./dashboard.html" class="btn btn-primary px-4 mt-3">Open the dashboard</a>
</div>
@@ -160,7 +163,8 @@
<div class="container py-5">
<div class="rounded shadow bg-translucent p-4 p-lg-5 col-12 col-lg-8 mx-auto text-white text-center">
<p class="fs-4 fst-italic mb-4">
"We switched our whole team over in a week. Everyone actually uses it, and that's the first time I've ever said that about a task tool."
"We switched our whole team over in a week. Everyone actually uses it, and that's the first time I've ever
said that about a task tool."
</p>
<p class="mb-0"><strong>Jamie L.</strong></p>
<p class="text-white-50 mb-0">Operations Lead, Greenfield Studio</p>

View File

@@ -42,6 +42,20 @@ function resetForm() {
taskForm.reset();
}
function showAlert(message, variant = 'success') {
const container = document.getElementById('alertContainer');
if (!container) return;
const alert = document.createElement('div');
alert.className = `alert alert-${variant} alert-dismissible fade show shadow-sm`;
alert.role = 'alert';
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
`;
container.appendChild(alert);
setTimeout(() => bootstrap.Alert.getOrCreateInstance(alert).close(), 3000);
}
const sortButton = document.getElementById("sortSelect");
window.addEventListener("DOMContentLoaded", () => {
sortButton.value = "default";
@@ -93,7 +107,11 @@ taskForm.addEventListener("submit", (event) => {
else if (event.target.classList.contains("delete")) {
const taskId = event.target.getAttribute("data-id");
deleteTask(taskId);
const confirmButton = document.getElementById('confirmDeleteButton');
confirmButton.addEventListener('click', async () => {
await deleteTask(taskId);
bootstrap.Modal.getInstance(document.getElementById('deleteModal')).hide();
}, { once: true });
}
else if (event.target.classList.contains('edit')) {
const task = {
@@ -148,7 +166,7 @@ function formatTask(task) {
li.innerHTML = `
<div class="d-flex justify-content-between align-items-start">
<h4 class="${done} col-11">${task.title}</h4>
<button data-id="${task._id}" type="button" class="btn-close delete" aria-label="Close"></button>
<button data-id="${task._id}" type="button" class="btn-close delete" data-bs-toggle="modal" data-bs-target="#deleteModal" aria-label="Close"></button>
</div>
<p class="${done}">${task.description}</p>
<p class="${done}"><strong>Due: </strong>${new Date(task.dueDate).toLocaleDateString()}</p>
@@ -328,6 +346,7 @@ async function deleteTask(taskId) {
const data = await response.json();
console.log({ message: "Deleted Task:", Task: data });
displayTasks();
showAlert('Task deleted successfully.');
} catch (error) {
console.error("Error:", error);

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Login</title>
<title>BusketLisk &#x2022; Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Pricing</title>
<title>BusketLisk &#x2022; Pricing</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Signup</title>
<title>BusketLisk &#x2022; Signup</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />

View File

@@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>To Do App | Support</title>
<title>BusketLisk &#x2022; Support</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous" />
<link rel="stylesheet" href="./css/styles.css" />
@@ -69,7 +69,8 @@
<form id="contactForm">
<input name="name" type="text" class="form-control mb-3 shadow-sm" placeholder="Your name" required>
<input name="email" type="email" class="form-control mb-3 shadow-sm" placeholder="Your email" required>
<textarea name="message" rows="5" class="form-control mb-3 shadow-sm" placeholder="How can we help?" required></textarea>
<textarea name="message" rows="5" class="form-control mb-3 shadow-sm" placeholder="How can we help?"
required></textarea>
<button type="submit" class="btn btn-primary px-4">Send message</button>
</form>
<hr class="border-light my-4">