diff --git a/Sprint-3/todo-list/script.mjs b/Sprint-3/todo-list/script.mjs
index ba0b2ceae..835fed06e 100644
--- a/Sprint-3/todo-list/script.mjs
+++ b/Sprint-3/todo-list/script.mjs
@@ -1,4 +1,4 @@
-// Store everything imported from './todos.mjs' module as properties of an object named Todos
+// Store everything imported from './todos.mjs' module as properties of an object named Todos
import * as Todos from "./todos.mjs";
// To store the todo tasks
@@ -9,24 +9,36 @@ window.addEventListener("load", () => {
document.getElementById("add-task-btn").addEventListener("click", addNewTodo);
// Populate sample data
- Todos.addTask(todos, "Wash the dishes", false);
+ Todos.addTask(todos, "Wash the dishes", false);
Todos.addTask(todos, "Do the shopping", true);
render();
});
+// Delete completed tasks
+document
+ .getElementById("delete-completed-btn")
+ .addEventListener("click", deleteCompletedTodos);
-// A callback that reads the task description from an input field and
+function deleteCompletedTodos() {
+ Todos.deleteCompleted(todos);
+ render();
+}
+
+// A callback that reads the task description from an input field and
// append a new task to the todo list.
function addNewTodo() {
const taskInput = document.getElementById("new-task-input");
+ const deadlineInput = document.getElementById("new-task-deadline");
const task = taskInput.value.trim();
+ const deadline = deadlineInput.value || null;
if (task) {
- Todos.addTask(todos, task, false);
+ Todos.addTask(todos, task, false, deadline);
render();
}
taskInput.value = "";
+ deadlineInput.value = "";
}
// Note:
@@ -45,12 +57,11 @@ function render() {
});
}
-
// Note:
// - First child of #todo-item-template is a
element.
// We will create each ToDo list item as a clone of this node.
// - This variable is declared here to be close to the only function that uses it.
-const todoListItemTemplate =
+const todoListItemTemplate =
document.getElementById("todo-item-template").content.firstElementChild;
// Create a
element for the given todo task
@@ -58,19 +69,57 @@ function createListItem(todo, index) {
const li = todoListItemTemplate.cloneNode(true); // true => Do a deep copy of the node
li.querySelector(".description").textContent = todo.task;
+
+ const deadlineBadge = li.querySelector(".deadline-badge");
+ const deadlineDate = li.querySelector(".deadline-date");
+ const icon = deadlineBadge ? deadlineBadge.querySelector("i") : null;
+ if (todo.deadline) {
+ const diffDays = Todos.getDaysRemaining(todo.deadline);
+
+ // Clear existing styling classes from deadlineBadge (keep 'deadline-badge')
+ deadlineBadge.className = "deadline-badge";
+
+ // Clear icon classes
+ if (icon) {
+ icon.className = "";
+ }
+
+ if (diffDays < 0) {
+ deadlineBadge.classList.add("overdue");
+ deadlineDate.textContent = `Deadline: ${Math.abs(diffDays)} ${Math.abs(diffDays) === 1 ? 'day' : 'days'} overdue`;
+ if (icon) icon.className = "fa-solid fa-triangle-exclamation";
+ } else if (diffDays === 0) {
+ deadlineBadge.classList.add("due-today");
+ deadlineDate.textContent = "Deadline: Due today";
+ if (icon) icon.className = "fa-solid fa-clock";
+ } else if (diffDays === 1) {
+ deadlineBadge.classList.add("due-tomorrow");
+ deadlineDate.textContent = "Deadline: Due tomorrow";
+ if (icon) icon.className = "fa-solid fa-hourglass-half";
+ } else {
+ deadlineBadge.classList.add("due-future");
+ deadlineDate.textContent = `Deadline: ${diffDays} days left`;
+ if (icon) icon.className = "fa-regular fa-calendar-days";
+ }
+ } else {
+ if (deadlineBadge) {
+ deadlineBadge.remove();
+ }
+ }
+
if (todo.completed) {
li.classList.add("completed");
}
- li.querySelector('.complete-btn').addEventListener("click", () => {
+ li.querySelector(".complete-btn").addEventListener("click", () => {
Todos.toggleCompletedOnTask(todos, index);
render();
});
-
- li.querySelector('.delete-btn').addEventListener("click", () => {
+
+ li.querySelector(".delete-btn").addEventListener("click", () => {
Todos.deleteTask(todos, index);
render();
});
return li;
-}
\ No newline at end of file
+}
diff --git a/Sprint-3/todo-list/style.css b/Sprint-3/todo-list/style.css
index 535e91227..0ae408d6d 100644
--- a/Sprint-3/todo-list/style.css
+++ b/Sprint-3/todo-list/style.css
@@ -31,17 +31,32 @@ h1 {
}
.todo-input input {
- flex: 1;
padding: 10px;
font-size: 16px;
border-radius: 6px;
border: 1px solid #ccc;
+ outline: none;
+ transition: border-color 0.2s ease-in-out;
+}
+
+.todo-input input:focus {
+ border-color: #4caf50;
+}
+
+.todo-input input[type="text"] {
+ flex: 2;
+}
+
+.todo-input input[type="date"] {
+ flex: 1;
+ color: #555;
+ cursor: pointer;
}
.todo-input button {
padding: 10px 20px;
font-size: 16px;
- background-color: #4CAF50;
+ background-color: #4caf50;
color: white;
border: none;
border-radius: 6px;
@@ -68,14 +83,87 @@ h1 {
background-color: #fff;
}
-.description {
+.todo-delete {
+ display: flex;
+ justify-content: flex-end;
+}
+
+#delete-completed-btn {
+ padding: 10px 20px;
+ margin-bottom: 10px;
+ font-size: 16px;
+ background-color: #aa0000;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+#delete-completed-btn:hover {
+ background-color: #d10000;
+}
+
+.todo-content {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
flex: 1;
margin-right: 10px;
+ min-width: 0;
+}
+
+.description {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
+.deadline-badge {
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 12px;
+ align-self: flex-start;
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-weight: 500;
+ transition: all 0.2s ease-in-out;
+}
+
+/* Overdue Badge (Red) */
+.deadline-badge.overdue {
+ color: #c62828;
+ background-color: #ffebee;
+ border: 1px solid #ffcdd2;
+}
+
+/* Due Today Badge (Orange) */
+.deadline-badge.due-today {
+ color: #146314;
+ background-color: #efffe3;
+ border: 1px solid #69eb68;
+}
+
+/* Due Tomorrow Badge (Indigo/Blue) */
+.deadline-badge.due-tomorrow {
+ color: #283593;
+ background-color: #e8eaf6;
+ border: 1px solid #c5cae9;
+}
+
+/* Due Future Badge (Green) */
+.deadline-badge.due-future {
+ color: #2e7d32;
+ background-color: #e8f5e9;
+ border: 1px solid #c8e6c9;
+}
+
+.todo-item.completed .deadline-badge {
+ background-color: #f5f5f5 !important;
+ color: #9e9e9e !important;
+ border-color: #e0e0e0 !important;
+}
+
.actions {
display: flex;
gap: 10px;
diff --git a/Sprint-3/todo-list/todos.mjs b/Sprint-3/todo-list/todos.mjs
index f17ab6a25..38dd0011f 100644
--- a/Sprint-3/todo-list/todos.mjs
+++ b/Sprint-3/todo-list/todos.mjs
@@ -10,8 +10,12 @@
*/
// Append a new task to todos[]
-export function addTask(todos, task, completed = false) {
- todos.push({ task, completed });
+export function addTask(todos, task, completed = false, deadline = null) {
+ const todo = { task, completed };
+ if (deadline) {
+ todo.deadline = deadline;
+ }
+ todos.push(todo);
}
// Delete todos[taskIndex] if it exists
@@ -26,4 +30,31 @@ export function toggleCompletedOnTask(todos, taskIndex) {
if (todos[taskIndex]) {
todos[taskIndex].completed = !todos[taskIndex].completed;
}
-}
\ No newline at end of file
+}
+
+// Delete all completed todos from todos[]
+export function deleteCompleted(todos) {
+ for (let i = todos.length - 1; i >= 0; i--) {
+ if (todos[i].completed) {
+ deleteTask(todos, i);
+ }
+ }
+ return todos;
+}
+
+// Calculate how many days are left until the deadline (relative to today).
+// Returns an integer: negative if overdue, 0 if due today, positive if due in the future.
+export function getDaysRemaining(deadlineDateStr, today = new Date()) {
+ if (!deadlineDateStr) return null;
+ const [year, month, day] = deadlineDateStr.split("-").map(Number);
+ const deadlineDate = new Date(year, month - 1, day);
+ const todayDate = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate()
+ );
+
+ const diffTime = deadlineDate - todayDate;
+ const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
+ return diffDays;
+}
diff --git a/Sprint-3/todo-list/todos.test.mjs b/Sprint-3/todo-list/todos.test.mjs
index bae7ae491..e002b2218 100644
--- a/Sprint-3/todo-list/todos.test.mjs
+++ b/Sprint-3/todo-list/todos.test.mjs
@@ -13,7 +13,7 @@ function createMockTodos() {
{ task: "Task 1 description", completed: true },
{ task: "Task 2 description", completed: false },
{ task: "Task 3 description", completed: true },
- { task: "Task 4 description", completed: false },
+ { task: "Task 4 description", completed: false },
];
}
@@ -29,7 +29,6 @@ describe("addTask()", () => {
});
test("Should append a new task to the end of a ToDo list", () => {
-
const todos = createMockTodos();
const lengthBeforeAddition = todos.length;
Todos.addTask(todos, theTask.task, theTask.completed);
@@ -39,10 +38,30 @@ describe("addTask()", () => {
// New task should be appended to the todos
expect(todos[todos.length - 1]).toEqual(theTask);
});
+
+ test("Should store deadline information if provided", () => {
+ let todos = [];
+ Todos.addTask(todos, "Task with deadline", false, "2026-07-31");
+ expect(todos).toHaveLength(1);
+ expect(todos[0]).toEqual({
+ task: "Task with deadline",
+ completed: false,
+ deadline: "2026-07-31"
+ });
+ });
+
+ test("Should not store deadline if none is provided", () => {
+ let todos = [];
+ Todos.addTask(todos, "Task without deadline", false);
+ expect(todos).toHaveLength(1);
+ expect(todos[0]).toEqual({
+ task: "Task without deadline",
+ completed: false
+ });
+ });
});
describe("deleteTask()", () => {
-
test("Delete the first task", () => {
const todos = createMockTodos();
const todosBeforeDeletion = createMockTodos();
@@ -53,7 +72,7 @@ describe("deleteTask()", () => {
expect(todos[0]).toEqual(todosBeforeDeletion[1]);
expect(todos[1]).toEqual(todosBeforeDeletion[2]);
- expect(todos[2]).toEqual(todosBeforeDeletion[3]);
+ expect(todos[2]).toEqual(todosBeforeDeletion[3]);
});
test("Delete the second task (a middle task)", () => {
@@ -66,7 +85,7 @@ describe("deleteTask()", () => {
expect(todos[0]).toEqual(todosBeforeDeletion[0]);
expect(todos[1]).toEqual(todosBeforeDeletion[2]);
- expect(todos[2]).toEqual(todosBeforeDeletion[3]);
+ expect(todos[2]).toEqual(todosBeforeDeletion[3]);
});
test("Delete the last task", () => {
@@ -79,7 +98,7 @@ describe("deleteTask()", () => {
expect(todos[0]).toEqual(todosBeforeDeletion[0]);
expect(todos[1]).toEqual(todosBeforeDeletion[1]);
- expect(todos[2]).toEqual(todosBeforeDeletion[2]);
+ expect(todos[2]).toEqual(todosBeforeDeletion[2]);
});
test("Delete a non-existing task", () => {
@@ -94,7 +113,6 @@ describe("deleteTask()", () => {
});
describe("toggleCompletedOnTask()", () => {
-
test("Expect the 'completed' property to toggle on an existing task", () => {
const todos = createMockTodos();
const taskIndex = 1;
@@ -111,13 +129,12 @@ describe("toggleCompletedOnTask()", () => {
const todos = createMockTodos();
const todosBeforeToggle = createMockTodos();
Todos.toggleCompletedOnTask(todos, 1);
-
- expect(todos[0]).toEqual(todosBeforeToggle[0]);
+
+ expect(todos[0]).toEqual(todosBeforeToggle[0]);
expect(todos[2]).toEqual(todosBeforeToggle[2]);
expect(todos[3]).toEqual(todosBeforeToggle[3]);
});
-
test("Expect no change when toggling on a non-existing task", () => {
const todos = createMockTodos();
const todosBeforeToggle = createMockTodos();
@@ -130,3 +147,69 @@ describe("toggleCompletedOnTask()", () => {
});
});
+// tests for deleteCompleted
+describe("deleteCompleted()", () => {
+ test("Delete all completed todos from todos[]", () => {
+ const todos = createMockTodos();
+ const todosBeforeDeletion = createMockTodos();
+ Todos.deleteCompleted(todos);
+
+ expect(todos).toHaveLength(2);
+
+ // expect the first task in the new todo list to be the second task from the original list
+ expect(todos[0]).toEqual(todosBeforeDeletion[1]);
+
+ // expect the second task in the new todo list to be the third task from the original list
+ expect(todos[1]).toEqual(todosBeforeDeletion[3]);
+ });
+
+ test("Delete all completed todos from todos[] when all todos are completed", () => {
+ const todos = createMockTodos().map((task) => ({
+ ...task,
+ completed: true,
+ }));
+ const todosBeforeDeletion = createMockTodos();
+ Todos.deleteCompleted(todos);
+
+ expect(todos).toHaveLength(0);
+ });
+
+ test("Delete all completed todos from todos[] when no todos are completed", () => {
+ const todos = createMockTodos().map((task) => ({
+ ...task,
+ completed: false,
+ }));
+ const initialLength = todos.length;
+ const todosBeforeDeletion = createMockTodos();
+ Todos.deleteCompleted(todos);
+
+ expect(todos).toHaveLength(initialLength);
+ });
+});
+
+describe("getDaysRemaining()", () => {
+ const fixedToday = new Date(2026, 6, 24); // July 24, 2026 (Note: month is 0-indexed, so 6 is July)
+
+ test("Should return negative days for overdue tasks", () => {
+ expect(Todos.getDaysRemaining("2026-07-20", fixedToday)).toBe(-4);
+ expect(Todos.getDaysRemaining("2026-07-23", fixedToday)).toBe(-1);
+ });
+
+ test("Should return 0 for tasks due today", () => {
+ expect(Todos.getDaysRemaining("2026-07-24", fixedToday)).toBe(0);
+ });
+
+ test("Should return 1 for tasks due tomorrow", () => {
+ expect(Todos.getDaysRemaining("2026-07-25", fixedToday)).toBe(1);
+ });
+
+ test("Should return positive days for future tasks", () => {
+ expect(Todos.getDaysRemaining("2026-07-31", fixedToday)).toBe(7);
+ });
+
+ test("Should return null for missing or empty deadline", () => {
+ expect(Todos.getDaysRemaining(null, fixedToday)).toBeNull();
+ expect(Todos.getDaysRemaining("", fixedToday)).toBeNull();
+ expect(Todos.getDaysRemaining(undefined, fixedToday)).toBeNull();
+ });
+});