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
54 changes: 29 additions & 25 deletions 08-todo-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Todo App</title>
<link rel="icon" type="image/png" href="todo.png" />
<link rel="stylesheet" href="style.css" />
<link
rel="stylesheet"
Expand All @@ -12,54 +13,57 @@
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Geist+Pixel&display=swap" rel="stylesheet" />
</head>
<body>
<div class="app">
<header>
<h1>
<i class="fas fa-check-circle"></i>
My Tasks
</h1>
<div class="header-top">
<h1>
<i class="fas fa-gamepad"></i>
QUEST LOG
</h1>
<div class="header-controls">
<button id="palette-toggle" class="pixel-btn" title="Change Color Palette">
<i class="fas fa-palette"></i>
</button>
<button id="theme-toggle" class="pixel-btn" title="Toggle Light/Dark">
<i class="fas fa-moon"></i>
</button>
Comment on lines +29 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expose the toggle state to assistive technology.

These controls visually change modes but never communicate whether each mode is active. Add aria-pressed="false" initially and update it alongside the body classes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@08-todo-app/index.html` around lines 29 - 34, Add aria-pressed="false" to the
palette-toggle and theme-toggle buttons, then update each button’s aria-pressed
value whenever its corresponding body class changes so assistive technology
reflects the current mode state.

</div>
</div>
<p id="date"></p>
</header>

<div class="todo-input">
<input type="text" id="task-input" placeholder="What do you need to do?" />
<button id="add-task">
<input type="text" id="task-input" placeholder="ENTER NEW QUEST..." autocomplete="off" />
<button id="add-task" class="pixel-btn">
<i class="fas fa-plus"></i>
</button>
Comment on lines +42 to 44

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Give icon-only action buttons accessible names.

Neither button exposes its purpose when the icon is unavailable to assistive technology.

  • 08-todo-app/index.html#L42-L44: add aria-label="Add quest".
  • 08-todo-app/script.js#L115-L118: set deleteBtn.setAttribute("aria-label", \Delete ${todo.text}`)`.
📍 Affects 2 files
  • 08-todo-app/index.html#L42-L44 (this comment)
  • 08-todo-app/script.js#L115-L118
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@08-todo-app/index.html` around lines 42 - 44, Add the accessible name “Add
quest” to the icon-only `#add-task` button in 08-todo-app/index.html:42-44. In
08-todo-app/script.js:115-118, update the deleteBtn creation in the
todo-rendering logic to set its aria-label to “Delete ” followed by todo.text.

</div>

<div class="filters">
<span class="filter active" data-filter="all"> All </span>
<span class="filter" data-filter="active"> Active </span>
<span class="filter" data-filter="completed"> Completed </span>
<span class="filter active" data-filter="all"> ALL </span>
<span class="filter" data-filter="active"> ACTIVE </span>
<span class="filter" data-filter="completed"> DONE </span>
</div>

<div class="todos-container">
<ul id="todos-list">
<!-- li will be inserted right here later in the video -->
<!-- <li class="todo-item">
<label class="checkbox-container">
<input type="checkbox" class="todo-checkbox" />
<span class="checkmark"></span>
</label>
<span class="todo-item-text">Buy groceries</span>
<button class="delete-btn"><i class="fas fa-times"></i></button>
</li> -->
</ul>
<ul id="todos-list"></ul>
<div class="empty-state hidden">
<i class="fas fa-clipboard-list"></i>
<p>No tasks here yet</p>
<i class="fas fa-ghost"></i>
<p>NO QUESTS FOUND</p>
</div>
</div>

<footer>
<p id="items-left">0 items left</p>
<button id="clear-completed">Clear completed</button>
<p id="items-left">0 XP LEFT</p>
<button id="clear-completed" class="pixel-btn-text">CLEAR DONE</button>
</footer>
</div>

<script src="script.js"></script>
</body>
</html>
</html>
109 changes: 90 additions & 19 deletions 08-todo-app/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,43 @@ const clearCompletedBtn = document.getElementById("clear-completed");
const emptyState = document.querySelector(".empty-state");
const dateElement = document.getElementById("date");
const filters = document.querySelectorAll(".filter");
const themeToggle = document.getElementById("theme-toggle");
const paletteToggle = document.getElementById("palette-toggle");

let todos = [];
let currentFilter = "all";

addTaskBtn.addEventListener("click", () => {
addTodo(taskInput.value);
});

// Event Listeners
addTaskBtn.addEventListener("click", () => addTodo(taskInput.value));
taskInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") addTodo(taskInput.value);
});

clearCompletedBtn.addEventListener("click", clearCompleted);

// Light/Dark Mode Toggle
themeToggle.addEventListener("click", () => {
document.body.classList.toggle("light-mode");
const icon = themeToggle.querySelector("i");
if (document.body.classList.contains("light-mode")) {
icon.classList.replace("fa-moon", "fa-sun");
} else {
icon.classList.replace("fa-sun", "fa-moon");
}
});

// Synthwave Palette Toggle
paletteToggle.addEventListener("click", () => {
document.body.classList.toggle("theme-synthwave");
});
Comment on lines +24 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Persist and restore the selected theme.

Both toggles only mutate body classes, so palette and dark-mode preferences reset on every reload. Store both states in localStorage and restore them, including the moon/sun icon and aria-pressed values, during bootstrap.

As required by the PR objective: “Local storage persistence for todos, completion statuses, and theme preferences.”

Also applies to: 239-242

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@08-todo-app/script.js` around lines 24 - 37, Update the themeToggle and
paletteToggle handlers to persist their respective states in localStorage, then
restore both preferences during script bootstrap. Restoration must synchronize
body classes, the theme icon’s moon/sun state, and each toggle’s aria-pressed
value, while preserving the existing toggle behavior.


function addTodo(text) {
if (text.trim() === "") return;

const todo = {
id: Date.now(),
text,
completed: false,
};

todos.push(todo);

saveTodos();
renderTodos();
taskInput.value = "";
Expand All @@ -45,14 +57,12 @@ function saveTodos() {

function updateItemsCount() {
const uncompletedTodos = todos.filter((todo) => !todo.completed);
itemsLeft.textContent = `${uncompletedTodos?.length} item${
uncompletedTodos?.length !== 1 ? "s" : ""
} left`;
itemsLeft.textContent = `${uncompletedTodos.length} XP LEFT`;
}

function checkEmptyState() {
const filteredTodos = filterTodos(currentFilter);
if (filteredTodos?.length === 0) emptyState.classList.remove("hidden");
if (filteredTodos.length === 0) emptyState.classList.remove("hidden");
else emptyState.classList.add("hidden");
}

Expand All @@ -69,14 +79,20 @@ function filterTodos(filter) {

function renderTodos() {
todosList.innerHTML = "";

const filteredTodos = filterTodos(currentFilter);

filteredTodos.forEach((todo) => {
const todoItem = document.createElement("li");
todoItem.classList.add("todo-item");
if (todo.completed) todoItem.classList.add("completed");

// Setup for Drag and Drop Tile
todoItem.draggable = true;
todoItem.dataset.id = todo.id;

todoItem.addEventListener("dragstart", handleDragStart);
todoItem.addEventListener("dragend", handleDragEnd);
Comment on lines +89 to +94

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not enable drag operations whose result is discarded.

Todos remain draggable in active and completed views, but Line 180 refuses to persist those reorders. The DOM appears reordered until the next render and then snaps back. Either disable dragging outside "all" or merge the filtered order back into todos.

Minimal fix
-    todoItem.draggable = true;
+    todoItem.draggable = currentFilter === "all";

Also applies to: 179-183

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@08-todo-app/script.js` around lines 89 - 94, Update the todo item setup
around todoItem.draggable and the reorder handling near the existing persistence
logic so drag operations are enabled only in the “all” view, or ensure reordered
filtered items are merged back into todos before saving. Preserve drag behavior
and persistence consistency so the DOM cannot show a reorder that is later
discarded.


const checkboxContainer = document.createElement("label");
checkboxContainer.classList.add("checkbox-container");

Expand All @@ -98,7 +114,7 @@ function renderTodos() {

const deleteBtn = document.createElement("button");
deleteBtn.classList.add("delete-btn");
deleteBtn.innerHTML = '<i class="fas fa-times"></i>';
deleteBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
deleteBtn.addEventListener("click", () => deleteTodo(todo.id));

todoItem.appendChild(checkboxContainer);
Expand All @@ -109,6 +125,64 @@ function renderTodos() {
});
}

// --- Fully Jitter-Free Drag and Drop Math ---

todosList.addEventListener("dragover", (e) => {
e.preventDefault();
const draggingItem = document.querySelector(".dragging");
if (!draggingItem) return;

const afterElement = getDragAfterElement(todosList, e.clientY);

if (afterElement == null) {
todosList.appendChild(draggingItem);
} else {
todosList.insertBefore(draggingItem, afterElement);
}
});

function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll(".todo-item:not(.dragging)")];

return draggableElements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;

if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY }
).element;
}

function handleDragStart(e) {
setTimeout(() => e.target.classList.add("dragging"), 0);
e.dataTransfer.effectAllowed = "move";
}

function handleDragEnd(e) {
e.target.classList.remove("dragging");

// Re-sync the `todos` array based on the new visual DOM order
const newOrderIds = [...todosList.querySelectorAll(".todo-item")].map((item) => Number(item.dataset.id));

const reorderedTodos = [];
newOrderIds.forEach((id) => {
const foundTodo = todos.find((t) => t.id === id);
if (foundTodo) reorderedTodos.push(foundTodo);
});

// Only override if we are in 'all' view
if (currentFilter === "all" && reorderedTodos.length === todos.length) {
todos = reorderedTodos;
saveTodos();
}
}

function clearCompleted() {
todos = todos.filter((todo) => !todo.completed);
saveTodos();
Expand All @@ -120,7 +194,6 @@ function toggleTodo(id) {
if (todo.id === id) {
return { ...todo, completed: !todo.completed };
}

return todo;
});
saveTodos();
Expand All @@ -147,26 +220,24 @@ filters.forEach((filter) => {

function setActiveFilter(filter) {
currentFilter = filter;

filters.forEach((item) => {
if (item.getAttribute("data-filter") === filter) {
item.classList.add("active");
} else {
item.classList.remove("active");
}
});

renderTodos();
}

function setDate() {
const options = { weekday: "long", month: "short", day: "numeric" };
const today = new Date();
dateElement.textContent = today.toLocaleDateString("en-US", options);
dateElement.textContent = today.toLocaleDateString("en-US", options).toUpperCase();
}

window.addEventListener("DOMContentLoaded", () => {
loadTodos();
updateItemsCount();
setDate();
});
});
Loading