diff --git a/Sprint-3/todo-list/02-guide_to_modularize_code.md b/Sprint-3/todo-list/02-guide_to_modularize_code.md index ce90c3905..6c234b6fc 100644 --- a/Sprint-3/todo-list/02-guide_to_modularize_code.md +++ b/Sprint-3/todo-list/02-guide_to_modularize_code.md @@ -36,7 +36,7 @@ It focuses on: - ✅ What operations are needed to support data access and manipulation ### The UI Part of a Web App - +. This is the part of the app that interacts with the user interface (UI). It focuses on: diff --git a/Sprint-3/todo-list/index.html b/Sprint-3/todo-list/index.html index 4d12c4654..e5916ef4f 100644 --- a/Sprint-3/todo-list/index.html +++ b/Sprint-3/todo-list/index.html @@ -14,9 +14,12 @@

My ToDo List

- - -
+ + + + + + @@ -26,15 +29,15 @@

My ToDo List

It can simplify the creation of list item node in JS script. --> +
  • + Task description + +
    + + +
    +
  • + - - + \ No newline at end of file diff --git a/Sprint-3/todo-list/script.mjs b/Sprint-3/todo-list/script.mjs index ba0b2ceae..54b9e9e37 100644 --- a/Sprint-3/todo-list/script.mjs +++ b/Sprint-3/todo-list/script.mjs @@ -7,6 +7,10 @@ const todos = []; // Set up tasks to be performed once on page load window.addEventListener("load", () => { document.getElementById("add-task-btn").addEventListener("click", addNewTodo); + document.getElementById("delete-completed-btn").addEventListener("click", () => { + Todos.deleteCompleted(todos); + render(); + }); // Populate sample data Todos.addTask(todos, "Wash the dishes", false); @@ -15,18 +19,19 @@ window.addEventListener("load", () => { 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,7 +50,6 @@ 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. @@ -58,6 +62,9 @@ function createListItem(todo, index) { const li = todoListItemTemplate.cloneNode(true); // true => Do a deep copy of the node li.querySelector(".description").textContent = todo.task; + if (todo.deadline) { + li.querySelector(".deadline").textContent = Todos.getDeadlineStatus(todo.deadline); + } if (todo.completed) { li.classList.add("completed"); } diff --git a/Sprint-3/todo-list/todos.mjs b/Sprint-3/todo-list/todos.mjs index f17ab6a25..e453b29e2 100644 --- a/Sprint-3/todo-list/todos.mjs +++ b/Sprint-3/todo-list/todos.mjs @@ -10,20 +10,44 @@ */ // 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) { + todos.push({ task, completed, deadline }); } -// Delete todos[taskIndex] if it exists export function deleteTask(todos, taskIndex) { if (todos[taskIndex]) { todos.splice(taskIndex, 1); } } -// Toggle the "completed" property of todos[taskIndex] if the task exists. export function toggleCompletedOnTask(todos, taskIndex) { if (todos[taskIndex]) { todos[taskIndex].completed = !todos[taskIndex].completed; } +} + +export function deleteCompleted(todos) { + for (let i = todos.length - 1; i >= 0; i--) { + if (todos[i].completed) { + todos.splice(i, 1); + } + } +} +// Return a human-readable string describing time left until deadline. +// Returns null if there's no deadline. +export function getDeadlineStatus(deadline) { + if (!deadline) return null; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const dueDate = new Date(deadline); + dueDate.setHours(0, 0, 0, 0); + + const msPerDay = 1000 * 60 * 60 * 24; + const daysLeft = Math.round((dueDate - today) / msPerDay); + + if (daysLeft > 0) return `Due in ${daysLeft} day${daysLeft === 1 ? "" : "s"}`; + if (daysLeft === 0) return "Due today"; + return `Overdue by ${Math.abs(daysLeft)} day${Math.abs(daysLeft) === 1 ? "" : "s"}`; } \ No newline at end of file diff --git a/Sprint-3/todo-list/todos.test.mjs b/Sprint-3/todo-list/todos.test.mjs index bae7ae491..2d3ee1d69 100644 --- a/Sprint-3/todo-list/todos.test.mjs +++ b/Sprint-3/todo-list/todos.test.mjs @@ -18,7 +18,7 @@ function createMockTodos() { } // A mock task to simulate user input -const theTask = { task: "The Task", completed: false }; +const theTask = { task: "The Task", completed: false, deadline: null }; describe("addTask()", () => { test("Add a task to an empty ToDo list", () => { @@ -29,20 +29,15 @@ 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); - // todos should now have one more task expect(todos).toHaveLength(lengthBeforeAddition + 1); - - // New task should be appended to the todos expect(todos[todos.length - 1]).toEqual(theTask); }); }); describe("deleteTask()", () => { - test("Delete the first task", () => { const todos = createMockTodos(); const todosBeforeDeletion = createMockTodos(); @@ -50,7 +45,6 @@ describe("deleteTask()", () => { Todos.deleteTask(todos, 0); expect(todos).toHaveLength(lengthBeforeDeletion - 1); - expect(todos[0]).toEqual(todosBeforeDeletion[1]); expect(todos[1]).toEqual(todosBeforeDeletion[2]); expect(todos[2]).toEqual(todosBeforeDeletion[3]); @@ -63,7 +57,6 @@ describe("deleteTask()", () => { Todos.deleteTask(todos, 1); expect(todos).toHaveLength(lengthBeforeDeletion - 1); - expect(todos[0]).toEqual(todosBeforeDeletion[0]); expect(todos[1]).toEqual(todosBeforeDeletion[2]); expect(todos[2]).toEqual(todosBeforeDeletion[3]); @@ -76,7 +69,6 @@ describe("deleteTask()", () => { Todos.deleteTask(todos, todos.length - 1); expect(todos).toHaveLength(lengthBeforeDeletion - 1); - expect(todos[0]).toEqual(todosBeforeDeletion[0]); expect(todos[1]).toEqual(todosBeforeDeletion[1]); expect(todos[2]).toEqual(todosBeforeDeletion[2]); @@ -94,7 +86,6 @@ describe("deleteTask()", () => { }); describe("toggleCompletedOnTask()", () => { - test("Expect the 'completed' property to toggle on an existing task", () => { const todos = createMockTodos(); const taskIndex = 1; @@ -102,7 +93,6 @@ describe("toggleCompletedOnTask()", () => { Todos.toggleCompletedOnTask(todos, taskIndex); expect(todos[taskIndex].completed).toEqual(!completedStateBeforeToggle); - // Toggle again Todos.toggleCompletedOnTask(todos, taskIndex); expect(todos[taskIndex].completed).toEqual(completedStateBeforeToggle); }); @@ -117,7 +107,6 @@ describe("toggleCompletedOnTask()", () => { 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 +119,71 @@ describe("toggleCompletedOnTask()", () => { }); }); +describe("deleteCompleted()", () => { + test("Should remove all completed tasks, keeping incomplete ones in order", () => { + const todos = createMockTodos(); + const todosBeforeDeletion = createMockTodos(); + Todos.deleteCompleted(todos); + + expect(todos).toHaveLength(2); + expect(todos[0]).toEqual(todosBeforeDeletion[1]); + expect(todos[1]).toEqual(todosBeforeDeletion[3]); + }); + + test("Should result in an empty list if all tasks are completed", () => { + const todos = [ + { task: "Task A", completed: true }, + { task: "Task B", completed: true }, + ]; + Todos.deleteCompleted(todos); + expect(todos).toHaveLength(0); + }); + + test("Should leave the list unchanged if no tasks are completed", () => { + const todos = [ + { task: "Task A", completed: false }, + { task: "Task B", completed: false }, + ]; + const todosBeforeDeletion = [...todos]; + Todos.deleteCompleted(todos); + expect(todos).toEqual(todosBeforeDeletion); + }); + + test("Should do nothing on an empty ToDo list", () => { + const todos = []; + Todos.deleteCompleted(todos); + expect(todos).toEqual([]); + }); +}); + +describe("getDeadlineStatus()", () => { + test("Should return null when there is no deadline", () => { + expect(Todos.getDeadlineStatus(null)).toBeNull(); + }); + + test("Should return 'Due today' when deadline is today", () => { + const today = new Date().toISOString().split("T")[0]; + expect(Todos.getDeadlineStatus(today)).toBe("Due today"); + }); + + test("Should return days remaining for a future deadline", () => { + const future = new Date(); + future.setDate(future.getDate() + 3); + const futureStr = future.toISOString().split("T")[0]; + expect(Todos.getDeadlineStatus(futureStr)).toBe("Due in 3 days"); + }); + + test("Should return singular 'day' when exactly 1 day left", () => { + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + const tomorrowStr = tomorrow.toISOString().split("T")[0]; + expect(Todos.getDeadlineStatus(tomorrowStr)).toBe("Due in 1 day"); + }); + + test("Should return overdue message for a past deadline", () => { + const past = new Date(); + past.setDate(past.getDate() - 2); + const pastStr = past.toISOString().split("T")[0]; + expect(Todos.getDeadlineStatus(pastStr)).toBe("Overdue by 2 days"); + }); +}); \ No newline at end of file