-
Notifications
You must be signed in to change notification settings - Fork 138
feat: Add Dark Mode, Pixel Art theme, Drag-and-Drop, and Local Storage persistence #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -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> | ||
| </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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| </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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
|
|
||
| function addTodo(text) { | ||
| if (text.trim() === "") return; | ||
|
|
||
| const todo = { | ||
| id: Date.now(), | ||
| text, | ||
| completed: false, | ||
| }; | ||
|
|
||
| todos.push(todo); | ||
|
|
||
| saveTodos(); | ||
| renderTodos(); | ||
| taskInput.value = ""; | ||
|
|
@@ -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"); | ||
| } | ||
|
|
||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Minimal fix- todoItem.draggable = true;
+ todoItem.draggable = currentFilter === "all";Also applies to: 179-183 🤖 Prompt for AI Agents |
||
|
|
||
| const checkboxContainer = document.createElement("label"); | ||
| checkboxContainer.classList.add("checkbox-container"); | ||
|
|
||
|
|
@@ -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); | ||
|
|
@@ -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(); | ||
|
|
@@ -120,7 +194,6 @@ function toggleTodo(id) { | |
| if (todo.id === id) { | ||
| return { ...todo, completed: !todo.completed }; | ||
| } | ||
|
|
||
| return todo; | ||
| }); | ||
| saveTodos(); | ||
|
|
@@ -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(); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
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