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
2 changes: 1 addition & 1 deletion Sprint-3/todo-list/02-guide_to_modularize_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 16 additions & 13 deletions Sprint-3/todo-list/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
<h1>My ToDo List</h1>

<div class="todo-input">
<input type="text" id="new-task-input" placeholder="Enter a new task..." />
<button id="add-task-btn">Add</button>
</div>
<input type="text" id="new-task-input" placeholder="Enter a new task..." />
<input type="date" id="new-task-deadline" />
<button id="add-task-btn">Add</button>
</div>

<button id="delete-completed-btn">Delete completed tasks</button>

<ul id="todo-list" class="todo-list">
</ul>
Expand All @@ -26,15 +29,15 @@ <h1>My ToDo List</h1>
It can simplify the creation of list item node in JS script.
-->
<template id="todo-item-template">
<li class="todo-item"> <!-- include class "completed" if the task completed state is true -->
<span class="description">Task description</span>
<div class="actions">
<button class="complete-btn"><span class="fa-solid fa-check" aria-hidden="true"></span></button>
<button class="delete-btn"><span class="fa-solid fa-trash" aria-hidden="true"></span></button>
</div>
</li>
</template>
<li class="todo-item">
<span class="description">Task description</span>
<span class="deadline"></span>
<div class="actions">
<button class="complete-btn"><span class="fa-solid fa-check" aria-hidden="true"></span></button>
<button class="delete-btn"><span class="fa-solid fa-trash" aria-hidden="true"></span></button>
</div>
</li>
</template>

</div>
</body>
</html>
</body>
15 changes: 11 additions & 4 deletions Sprint-3/todo-list/script.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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:
Expand All @@ -45,7 +50,6 @@ function render() {
});
}


// Note:
// - First child of #todo-item-template is a <li> element.
// We will create each ToDo list item as a clone of this node.
Expand All @@ -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");
}
Expand Down
32 changes: 28 additions & 4 deletions Sprint-3/todo-list/todos.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}`;
}
81 changes: 69 additions & 12 deletions Sprint-3/todo-list/todos.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -29,28 +29,22 @@ 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();
const lengthBeforeDeletion = todos.length;
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]);
Expand All @@ -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]);
Expand All @@ -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]);
Expand All @@ -94,15 +86,13 @@ describe("deleteTask()", () => {
});

describe("toggleCompletedOnTask()", () => {

test("Expect the 'completed' property to toggle on an existing task", () => {
const todos = createMockTodos();
const taskIndex = 1;
const completedStateBeforeToggle = todos[taskIndex].completed;
Todos.toggleCompletedOnTask(todos, taskIndex);
expect(todos[taskIndex].completed).toEqual(!completedStateBeforeToggle);

// Toggle again
Todos.toggleCompletedOnTask(todos, taskIndex);
expect(todos[taskIndex].completed).toEqual(completedStateBeforeToggle);
});
Expand All @@ -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();
Expand All @@ -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");
});
});
Loading