783 lines
32 KiB
HTML
783 lines
32 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<title>Todo List</title>
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
<style>
|
||
:root {
|
||
--bg-color: #f5f7fa;
|
||
--primary-color: #0066ff;
|
||
--primary-dark: #0052cc;
|
||
--text-color: #333;
|
||
--border-color: #ddd;
|
||
--radius: 8px;
|
||
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
.dark-mode {
|
||
--bg-color: #1e1e1e;
|
||
--primary-color: #bb86fc;
|
||
--primary-dark: #a074f2;
|
||
--text-color: #e0e0e0;
|
||
--border-color: #444;
|
||
--shadow: none;
|
||
}
|
||
body {
|
||
font-family: "Inter", sans-serif;
|
||
background: var(--bg-color);
|
||
margin: 0;
|
||
padding: 2rem;
|
||
color: var(--text-color);
|
||
}
|
||
.container {
|
||
position: relative; /* needed for absolute toggle */
|
||
max-width: 480px;
|
||
margin: 0 auto;
|
||
background: #fff;
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--shadow);
|
||
padding: 2rem;
|
||
width: 100%;
|
||
}
|
||
.dark-mode .container {
|
||
background: #2b2b2b;
|
||
}
|
||
h1 {
|
||
font-weight: 600;
|
||
text-align: center;
|
||
color: var(--primary-color);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
.input-group {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
#newTodoInput {
|
||
flex: 1;
|
||
padding: 0.75rem;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius);
|
||
font-size: 1rem;
|
||
outline: none;
|
||
transition: border-color 0.2s;
|
||
}
|
||
#newTodoInput:focus {
|
||
border-color: var(--primary-color);
|
||
}
|
||
#addBtn {
|
||
background: var(--primary-color);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
padding: 0.75rem 1rem;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
transition: background 0.2s;
|
||
}
|
||
#addBtn:hover {
|
||
background: var(--primary-dark);
|
||
}
|
||
ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin-top: 1.5rem;
|
||
}
|
||
li {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0.75rem;
|
||
border-bottom: 1px solid var(--border-color);
|
||
border-radius: var(--radius);
|
||
background: #fafafa;
|
||
transition: background 0.2s;
|
||
gap: 0.5rem;
|
||
position: relative; /* for highlight overlay */
|
||
}
|
||
.dark-mode li {
|
||
background: #3a3a3a;
|
||
color: var(--text-color);
|
||
}
|
||
li:hover {
|
||
background: #f0f8ff;
|
||
}
|
||
.dark-mode li:hover {
|
||
background: #4a4a4a;
|
||
}
|
||
li.completed .todo-text {
|
||
text-decoration: line-through;
|
||
color: #888;
|
||
}
|
||
.drag-handle {
|
||
cursor: move;
|
||
user-select: none;
|
||
display: inline-block;
|
||
width: 1.2em;
|
||
text-align: center;
|
||
margin-right: 0.5rem;
|
||
color: #666;
|
||
font-weight: bold;
|
||
}
|
||
.drag-handle:hover {
|
||
opacity: 0.7;
|
||
}
|
||
.todo-text {
|
||
flex: 1;
|
||
margin-left: 0.75rem;
|
||
word-break: break-word;
|
||
}
|
||
.editing-input {
|
||
flex: 1;
|
||
font-size: 1rem;
|
||
padding: 0.25rem;
|
||
border: 1px solid var(--border-color);
|
||
border-radius: var(--radius);
|
||
box-sizing: border-box;
|
||
}
|
||
button.action-btn {
|
||
background: none;
|
||
border: none;
|
||
color: #007bff;
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: var(--radius);
|
||
transition: background 0.2s;
|
||
}
|
||
button.action-btn:hover {
|
||
background: #e0e0ff;
|
||
}
|
||
button.delete-btn {
|
||
background: #ff6b6b;
|
||
color: #fff;
|
||
padding: 0.25rem 0.5rem;
|
||
border-radius: var(--radius);
|
||
cursor: pointer;
|
||
font-size: 0.9rem;
|
||
border: none;
|
||
transition: background 0.2s;
|
||
}
|
||
button.delete-btn:hover {
|
||
background: #e55a5a;
|
||
}
|
||
.dragging {
|
||
opacity: 0.5;
|
||
}
|
||
/* Visual cue – draws a line either ABOVE or BELOW the target li */
|
||
.indicator-above {
|
||
border-top: 3px solid var(--primary-color);
|
||
} /* line ABOVE the item */
|
||
.indicator-below {
|
||
border-bottom: 3px solid var(--primary-color);
|
||
} /* line BELOW the item */
|
||
.theme-toggle {
|
||
position: absolute;
|
||
top: 1rem;
|
||
right: 1rem;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--text-color);
|
||
font-size: 1.5rem;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 2rem;
|
||
height: 2rem;
|
||
line-height: 1;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10;
|
||
}
|
||
.theme-toggle:hover {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div style="text-align: center; margin-bottom: 1.5rem">
|
||
<h1
|
||
id="todoTitle"
|
||
style="cursor: pointer; display: inline-block"
|
||
>
|
||
Todo List
|
||
</h1>
|
||
</div>
|
||
<div class="input-group">
|
||
<input
|
||
type="text"
|
||
id="newTodoInput"
|
||
placeholder="What needs to be done?"
|
||
/>
|
||
<button id="addBtn">Add</button>
|
||
</div>
|
||
<ul id="todoList"></ul>
|
||
<button
|
||
id="themeToggle"
|
||
class="theme-toggle"
|
||
aria-label="Toggle dark mode"
|
||
>
|
||
<svg
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
>
|
||
<path
|
||
id="theme-icon"
|
||
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
|
||
stroke="currentColor"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<script>
|
||
const input = document.getElementById("newTodoInput");
|
||
const addBtn = document.getElementById("addBtn");
|
||
const list = document.getElementById("todoList");
|
||
const themeToggle = document.getElementById("themeToggle");
|
||
const todoTitle = document.getElementById("todoTitle");
|
||
|
||
// Load theme preference from localStorage
|
||
if (localStorage.getItem("darkMode") === "enabled") {
|
||
document.body.classList.add("dark-mode");
|
||
// Update icon to sun for dark mode
|
||
themeToggle.innerHTML = `
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="12" cy="12" r="5" fill="currentColor" />
|
||
<g fill="currentColor">
|
||
<circle cx="12" cy="2" r="1" />
|
||
<circle cx="12" cy="22" r="1" />
|
||
<circle cx="2" cy="12" r="1" />
|
||
<circle cx="22" cy="12" r="1" />
|
||
<circle cx="4.22" cy="4.22" r="1" />
|
||
<circle cx="19.78" cy="19.78" r="1" />
|
||
<circle cx="4.22" cy="19.78" r="1" />
|
||
<circle cx="19.78" cy="4.22" r="1" />
|
||
</g>
|
||
</svg>
|
||
`;
|
||
}
|
||
|
||
// Load saved title from localStorage or use default
|
||
const savedTitle = localStorage.getItem("todoListTitle");
|
||
if (savedTitle) {
|
||
todoTitle.textContent = savedTitle;
|
||
document.title = savedTitle;
|
||
}
|
||
|
||
// Make title editable by clicking
|
||
todoTitle.addEventListener("click", function () {
|
||
const currentTitle = todoTitle.textContent;
|
||
todoTitle.innerHTML = `<input type="text" id="titleInput" value="${currentTitle}" style="font-size: 1.5rem; font-weight: 600; width: 200px; border: 1px solid var(--border-color); padding: 0.25rem; border-radius: var(--radius); text-align: center;">`;
|
||
const titleInput = document.getElementById("titleInput");
|
||
titleInput.focus();
|
||
titleInput.select();
|
||
|
||
// Handle saving when pressing Enter or losing focus
|
||
titleInput.addEventListener("keypress", (e) => {
|
||
if (e.key === "Enter") {
|
||
saveTitle();
|
||
}
|
||
});
|
||
|
||
titleInput.addEventListener("blur", saveTitle);
|
||
});
|
||
|
||
function saveTitle() {
|
||
const titleInput = document.getElementById("titleInput");
|
||
if (titleInput) {
|
||
const newTitle = titleInput.value.trim();
|
||
if (newTitle) {
|
||
todoTitle.textContent = newTitle;
|
||
document.title = newTitle; // Update browser tab title
|
||
localStorage.setItem("todoListTitle", newTitle); // Save to localStorage
|
||
} else {
|
||
todoTitle.textContent = "Todo List"; // Reset to default
|
||
document.title = "Todo List";
|
||
localStorage.removeItem("todoListTitle"); // Remove from localStorage
|
||
}
|
||
}
|
||
}
|
||
|
||
// Load all todos from localStorage
|
||
function loadTodos() {
|
||
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||
list.innerHTML = "";
|
||
todos.forEach((todo) => {
|
||
const li = document.createElement("li");
|
||
li.dataset.id = todo.id;
|
||
li.setAttribute("draggable", "true");
|
||
li.className = todo.completed ? "completed" : "";
|
||
|
||
const dragHandle = document.createElement("span");
|
||
dragHandle.className = "drag-handle";
|
||
dragHandle.textContent = "≡";
|
||
dragHandle.title = "Reorder";
|
||
li.prepend(dragHandle);
|
||
|
||
// Create checkbox for strikethrough
|
||
const checkbox = document.createElement("input");
|
||
checkbox.type = "checkbox";
|
||
checkbox.style.marginRight = "0.5rem";
|
||
checkbox.style.cursor = "pointer";
|
||
|
||
// Set checkbox state based on todo completion
|
||
if (todo.completed) {
|
||
checkbox.checked = true;
|
||
}
|
||
|
||
// Add event listener to checkbox
|
||
checkbox.addEventListener("change", function (e) {
|
||
e.stopPropagation(); // Prevent event from bubbling up to parent
|
||
|
||
// Find the todo text span
|
||
const textSpan = li.querySelector(".todo-text");
|
||
if (this.checked) {
|
||
textSpan.style.textDecoration = "line-through";
|
||
textSpan.style.color = "#888";
|
||
} else {
|
||
textSpan.style.textDecoration = "none";
|
||
textSpan.style.color = "";
|
||
}
|
||
|
||
// Update todo in localStorage
|
||
const todos = JSON.parse(
|
||
localStorage.getItem("todos") || "[]",
|
||
);
|
||
const updatedTodos = todos.map((t) =>
|
||
t.id === todo.id
|
||
? { ...t, completed: this.checked }
|
||
: t,
|
||
);
|
||
|
||
// Move checked item to correct position (place at bottom of list)
|
||
if (this.checked) {
|
||
// Save updated todos to localStorage
|
||
localStorage.setItem(
|
||
"todos",
|
||
JSON.stringify(updatedTodos),
|
||
);
|
||
// Mark this li as completed
|
||
li.classList.add("completed");
|
||
// Find the last unchecked item to place completed item after it
|
||
let lastUncheckedItem = null;
|
||
for (
|
||
let i = list.children.length - 1;
|
||
i >= 0;
|
||
i--
|
||
) {
|
||
if (
|
||
!list.children[i].classList.contains(
|
||
"completed",
|
||
)
|
||
) {
|
||
lastUncheckedItem = list.children[i];
|
||
break;
|
||
}
|
||
}
|
||
|
||
// If there are unchecked items, insert after the last unchecked item
|
||
// Otherwise, append to the end of the list
|
||
if (lastUncheckedItem) {
|
||
list.insertBefore(
|
||
li,
|
||
lastUncheckedItem.nextSibling,
|
||
);
|
||
} else {
|
||
list.appendChild(li);
|
||
}
|
||
|
||
// Get the new order from DOM and reorder todos
|
||
const newOrder = Array.from(list.children).map(
|
||
(li) => parseInt(li.dataset.id),
|
||
);
|
||
const reorderedTodos = newOrder.map((id) =>
|
||
updatedTodos.find((t) => t.id === id),
|
||
);
|
||
localStorage.setItem(
|
||
"todos",
|
||
JSON.stringify(reorderedTodos),
|
||
);
|
||
} else {
|
||
// Capture the current position of li BEFORE any changes
|
||
const currentIndex = Array.from(
|
||
list.children,
|
||
).indexOf(li);
|
||
|
||
// Find the first checked item BEFORE removing completed class
|
||
const firstCheckedItem = Array.from(
|
||
list.children,
|
||
).find((sibling) =>
|
||
sibling.classList.contains("completed"),
|
||
);
|
||
|
||
// Find the last unchecked item (excluding this li) to place unchecked item after it
|
||
let lastUncheckedItem = null;
|
||
for (
|
||
let i = list.children.length - 1;
|
||
i >= 0;
|
||
i--
|
||
) {
|
||
const sibling = list.children[i];
|
||
if (
|
||
sibling !== li &&
|
||
!sibling.classList.contains("completed")
|
||
) {
|
||
lastUncheckedItem = sibling;
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Remove completed class when unchecking
|
||
li.classList.remove("completed");
|
||
|
||
// If there are checked items, insert before the first checked item
|
||
// If there are unchecked items before this one, insert after the last unchecked item
|
||
// Otherwise, append to the end of unchecked items
|
||
if (firstCheckedItem) {
|
||
const firstCheckedIndex = Array.from(
|
||
list.children,
|
||
).indexOf(firstCheckedItem);
|
||
const lastIndex = Array.from(
|
||
list.children,
|
||
).indexOf(li);
|
||
|
||
if (
|
||
lastUncheckedItem &&
|
||
lastIndex > 0 &&
|
||
Array.from(list.children).indexOf(
|
||
lastUncheckedItem,
|
||
) < lastIndex
|
||
) {
|
||
list.insertBefore(
|
||
li,
|
||
lastUncheckedItem.nextSibling,
|
||
);
|
||
} else {
|
||
list.insertBefore(li, firstCheckedItem);
|
||
}
|
||
} else if (
|
||
lastUncheckedItem &&
|
||
currentIndex > 0 &&
|
||
Array.from(list.children).indexOf(
|
||
lastUncheckedItem,
|
||
) < currentIndex
|
||
) {
|
||
list.insertBefore(
|
||
li,
|
||
lastUncheckedItem.nextSibling,
|
||
);
|
||
}
|
||
|
||
// Get the new order from DOM after reordering and save to localStorage
|
||
const newOrder = Array.from(list.children).map(
|
||
(li) => parseInt(li.dataset.id),
|
||
);
|
||
const reorderedTodos = newOrder.map((id) =>
|
||
updatedTodos.find((t) => t.id === id),
|
||
);
|
||
localStorage.setItem(
|
||
"todos",
|
||
JSON.stringify(reorderedTodos),
|
||
);
|
||
}
|
||
});
|
||
|
||
// Insert checkbox at the beginning of the li
|
||
li.insertBefore(checkbox, li.firstChild);
|
||
|
||
const span = document.createElement("span");
|
||
span.className = "todo-text";
|
||
span.textContent = todo.text;
|
||
span.style.cursor = "pointer"; // Make it clear it's editable
|
||
li.appendChild(span);
|
||
|
||
// Make the entire li clickable for editing
|
||
li.addEventListener("click", (e) => {
|
||
// Don't trigger edit if clicking on the delete button, edit button, or checkbox
|
||
if (
|
||
e.target.classList.contains("delete-btn") ||
|
||
e.target.classList.contains("action-btn") ||
|
||
e.target.type === "checkbox"
|
||
) {
|
||
return;
|
||
}
|
||
startEdit(li);
|
||
});
|
||
|
||
const delBtn = document.createElement("button");
|
||
delBtn.className = "delete-btn";
|
||
delBtn.textContent = "Delete";
|
||
delBtn.onclick = () => deleteTodo(todo.id);
|
||
|
||
li.appendChild(delBtn);
|
||
|
||
// ---------- Drag & Drop Handlers ----------
|
||
li.addEventListener("dragstart", (e) => {
|
||
e.dataTransfer.setData("text/plain", todo.id);
|
||
e.dataTransfer.effectAllowed = "move";
|
||
li.classList.add("dragging");
|
||
});
|
||
|
||
li.addEventListener("dragover", (e) => {
|
||
e.preventDefault(); // allow drop
|
||
// Remove any existing indicator classes from ALL list items
|
||
document
|
||
.querySelectorAll(
|
||
".indicator-above, .indicator-below",
|
||
)
|
||
.forEach((el) =>
|
||
el.classList.remove(
|
||
"indicator-above",
|
||
"indicator-below",
|
||
),
|
||
);
|
||
|
||
const targetLi = li; // the li we are hovering over
|
||
const rect = targetLi.getBoundingClientRect();
|
||
const offsetY = e.clientY - rect.top;
|
||
const halfway = rect.height / 2;
|
||
// If cursor is in the upper half of the item, we will INSERT ABOVE it
|
||
const insertAbove = offsetY < halfway;
|
||
|
||
// Store the decision on the element so we can reuse it later
|
||
targetLi.dataset.insertAbove = insertAbove;
|
||
|
||
// Add the appropriate visual indicator class to THIS li only
|
||
if (insertAbove) {
|
||
targetLi.classList.add("indicator-above"); // draws a line ABOVE the item
|
||
} else {
|
||
targetLi.classList.add("indicator-below"); // draws a line BELOW the item
|
||
}
|
||
});
|
||
|
||
li.addEventListener("dragleave", (e) => {
|
||
// Clean up highlight as soon as the cursor leaves this li
|
||
highlightDropTarget(li, false);
|
||
});
|
||
|
||
li.addEventListener("drop", handleDrop);
|
||
li.addEventListener("dragend", () => {
|
||
li.classList.remove("dragging");
|
||
// Always strip any lingering indicator classes
|
||
document
|
||
.querySelectorAll(
|
||
".indicator-above, .indicator-below",
|
||
)
|
||
.forEach((el) =>
|
||
el.classList.remove(
|
||
"indicator-above",
|
||
"indicator-below",
|
||
),
|
||
);
|
||
// Clean the stored flag
|
||
delete targetLi.dataset.insertAbove;
|
||
});
|
||
// --------------------------- ----------------
|
||
|
||
list.appendChild(li);
|
||
});
|
||
}
|
||
|
||
// Add a new todo
|
||
async function addTodo() {
|
||
const text = input.value.trim();
|
||
if (!text) return;
|
||
|
||
// Get existing todos from localStorage
|
||
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||
|
||
// Create new todo with unique ID
|
||
const newTodo = {
|
||
id: Date.now(),
|
||
text: text,
|
||
completed: false,
|
||
};
|
||
|
||
// Add new todo to the array
|
||
todos.push(newTodo);
|
||
|
||
// Save to localStorage
|
||
localStorage.setItem("todos", JSON.stringify(todos));
|
||
|
||
input.value = "";
|
||
loadTodos();
|
||
}
|
||
|
||
// Delete a todo
|
||
async function deleteTodo(id) {
|
||
if (!confirm("Delete this todo?")) return;
|
||
|
||
// Get existing todos from localStorage
|
||
let todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||
|
||
// Filter out the todo with the given ID
|
||
todos = todos.filter((t) => t.id !== id);
|
||
|
||
// Save to localStorage
|
||
localStorage.setItem("todos", JSON.stringify(todos));
|
||
|
||
loadTodos();
|
||
}
|
||
|
||
// Inline edit
|
||
function startEdit(li) {
|
||
const id = li.dataset.id;
|
||
const textSpan = li.querySelector(".todo-text");
|
||
if (!textSpan) return;
|
||
|
||
const editingInput = document.createElement("input");
|
||
editingInput.type = "text";
|
||
editingInput.value = textSpan.textContent;
|
||
editingInput.className = "editing-input";
|
||
editingInput.style.width = "100%";
|
||
editingInput.setAttribute("autofocus", "");
|
||
|
||
li.replaceChild(editingInput, textSpan);
|
||
editingInput.focus();
|
||
|
||
function commit() {
|
||
const newText = editingInput.value.trim();
|
||
if (newText !== textSpan.textContent) {
|
||
// Get existing todos from localStorage
|
||
const todos = JSON.parse(
|
||
localStorage.getItem("todos") || "[]",
|
||
);
|
||
|
||
// Update todo text
|
||
const updatedTodos = todos.map((t) =>
|
||
t.id === id ? { ...t, text: newText } : t,
|
||
);
|
||
|
||
// Save to localStorage
|
||
localStorage.setItem(
|
||
"todos",
|
||
JSON.stringify(updatedTodos),
|
||
);
|
||
|
||
loadTodos();
|
||
} else {
|
||
loadTodos();
|
||
}
|
||
}
|
||
editingInput.addEventListener("blur", commit);
|
||
editingInput.addEventListener("keypress", (e) => {
|
||
if (e.key === "Enter") commit();
|
||
});
|
||
}
|
||
|
||
// Highlight the target li with either .indicator-above or .indicator-below
|
||
function highlightDropTarget(li, remove) {
|
||
if (!remove) {
|
||
// Caller will handle removal – we just return here for clarity
|
||
return;
|
||
}
|
||
li.classList.remove("indicator-above", "indicator-below");
|
||
}
|
||
|
||
// Handle drop to reorder items
|
||
async function handleDrop(event) {
|
||
event.preventDefault();
|
||
|
||
const draggedId = event.dataTransfer.getData("text/plain");
|
||
const targetLi = event.currentTarget.closest("li");
|
||
if (!targetLi || targetLi.dataset.id === draggedId) return;
|
||
|
||
// Retrieve the insertion side we stored earlier
|
||
const insertAbove = targetLi.dataset.insertAbove === "true";
|
||
|
||
// Remove dragged element from the DOM
|
||
const draggedLi = list.querySelector(
|
||
`[data-id="${draggedId}"]`,
|
||
);
|
||
if (!draggedLi) return;
|
||
list.removeChild(draggedLi);
|
||
|
||
// Insert based on the stored decision
|
||
if (insertAbove) {
|
||
// Insert *before* the target (so dragged item appears ABOVE it)
|
||
list.insertBefore(draggedLi, targetLi);
|
||
} else {
|
||
// Insert *after* the target (so dragged item appears BELOW it)
|
||
// If target is the last child, just append
|
||
if (targetLi.nextSibling) {
|
||
list.insertBefore(draggedLi, targetLi.nextSibling);
|
||
} else {
|
||
list.appendChild(draggedLi);
|
||
}
|
||
}
|
||
|
||
// Clean up any lingering indicator classes and the stored flag
|
||
document
|
||
.querySelectorAll(".indicator-above, .indicator-below")
|
||
.forEach((el) =>
|
||
el.classList.remove(
|
||
"indicator-above",
|
||
"indicator-below",
|
||
),
|
||
);
|
||
delete targetLi.dataset.insertAbove;
|
||
|
||
// Persist the new order to localStorage
|
||
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
|
||
const newOrder = Array.from(list.children).map((li) =>
|
||
parseInt(li.dataset.id),
|
||
);
|
||
const reorderedTodos = newOrder.map((id) =>
|
||
todos.find((t) => t.id === id),
|
||
);
|
||
localStorage.setItem("todos", JSON.stringify(reorderedTodos));
|
||
}
|
||
|
||
// Theme toggle
|
||
themeToggle.addEventListener("click", () => {
|
||
document.body.classList.toggle("dark-mode");
|
||
if (document.body.classList.contains("dark-mode")) {
|
||
localStorage.setItem("darkMode", "enabled");
|
||
// Update icon to sun for dark mode
|
||
themeToggle.innerHTML = `
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<circle cx="12" cy="12" r="5" fill="currentColor" />
|
||
<g fill="currentColor">
|
||
<circle cx="12" cy="2" r="1" />
|
||
<circle cx="12" cy="22" r="1" />
|
||
<circle cx="2" cy="12" r="1" />
|
||
<circle cx="22" cy="12" r="1" />
|
||
<circle cx="4.22" cy="4.22" r="1" />
|
||
<circle cx="19.78" cy="19.78" r="1" />
|
||
<circle cx="4.22" cy="19.78" r="1" />
|
||
<circle cx="19.78" cy="4.22" r="1" />
|
||
</g>
|
||
</svg>
|
||
`;
|
||
} else {
|
||
localStorage.setItem("darkMode", "disabled");
|
||
// Update icon to moon for light mode
|
||
themeToggle.innerHTML = `
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||
<path id="theme-icon" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
`;
|
||
}
|
||
});
|
||
|
||
// Kick off
|
||
addBtn.onclick = addTodo;
|
||
input.addEventListener("keypress", (e) => {
|
||
if (e.key === "Enter") addTodo();
|
||
});
|
||
loadTodos();
|
||
</script>
|
||
</body>
|
||
</html>
|