Chapter 14 of 14
You'll build a fully interactive task management app — entirely in vanilla JavaScript, no frameworks. Add tasks, mark them complete, filter by status, and persist data with localStorage.
A tasks app where users can add tasks, click to mark them complete (strikethrough), filter by All/Active/Completed, and delete tasks. State persists across page reloads via localStorage. No libraries, no frameworks — just DOM manipulation and events.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tasks</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main>
<h1>Tasks</h1>
<form id="task-form">
<input type="text" id="task-input" placeholder="Add a task..." required />
<button type="submit">Add</button>
</form>
<div class="filters">
<button class="filter active" data-filter="all">All</button>
<button class="filter" data-filter="active">Active</button>
<button class="filter" data-filter="completed">Completed</button>
</div>
<ul id="task-list"></ul>
<p id="task-count"></p>
</main>
<script src="app.js"></script>
</body>
</html>// ── State ─────────────────────────────────────────────────────────
let tasks = JSON.parse(localStorage.getItem("tasks")) || [];
let activeFilter = "all";
// ── Save to localStorage ───────────────────────────────────────────
function saveTasks() {
localStorage.setItem("tasks", JSON.stringify(tasks));
}
// ── Render ─────────────────────────────────────────────────────────
function render() {
const list = document.querySelector("#task-list");
const count = document.querySelector("#task-count");
// Filter tasks based on active filter
const visible = tasks.filter(task => {
if (activeFilter === "active") return !task.completed;
if (activeFilter === "completed") return task.completed;
return true;
});
// Render list items
list.innerHTML = visible.map(task => `
<li class="${task.completed ? "done" : ""}" data-id="${task.id}">
<button class="toggle" aria-label="Toggle complete">
${task.completed ? "✓" : "○"}
</button>
<span class="task-text">${task.text}</span>
<button class="delete" aria-label="Delete task">✕</button>
</li>
`).join("");
// Count
const remaining = tasks.filter(t => !t.completed).length;
count.textContent = `${remaining} task${remaining !== 1 ? "s" : ""} remaining`;
}
// ── Add task ────────────────────────────────────────────────────────
document.querySelector("#task-form").addEventListener("submit", (e) => {
e.preventDefault();
const input = document.querySelector("#task-input");
const text = input.value.trim();
if (!text) return;
tasks.push({ id: Date.now(), text, completed: false });
saveTasks();
render();
input.value = "";
input.focus();
});
// ── Toggle and delete via event delegation ──────────────────────────
document.querySelector("#task-list").addEventListener("click", (e) => {
const li = e.target.closest("li");
if (!li) return;
const id = Number(li.dataset.id);
if (e.target.matches(".toggle")) {
tasks = tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t);
saveTasks();
render();
}
if (e.target.matches(".delete")) {
tasks = tasks.filter(t => t.id !== id);
saveTasks();
render();
}
});
// ── Filters ─────────────────────────────────────────────────────────
document.querySelectorAll(".filter").forEach(btn => {
btn.addEventListener("click", () => {
activeFilter = btn.dataset.filter;
document.querySelectorAll(".filter").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
render();
});
});
// ── Initial render ──────────────────────────────────────────────────
render();