Final HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>To Do List</title>
<link rel="stylesheet" href="coding.css">
</head>
<body>
<main class="todo-app">
<header>
<h1>To Do List</h1>
<p>Stay organized and keep moving.</p>
</header>
<form id="task-form">
<label for="task-input">
New task
</label>
<input
id="task-input"
name="task"
type="text"
placeholder="Enter a task..."
autocomplete="off"
required
>
<button type="submit">
Add Task
</button>
</form>
<nav aria-label="Task filters">
<button
class="filter-button is-active"
type="button"
data-filter="all"
>
All
</button>
<button
class="filter-button"
type="button"
data-filter="active"
>
Active
</button>
<button
class="filter-button"
type="button"
data-filter="completed"
>
Completed
</button>
</nav>
<ul
id="task-list"
aria-live="polite"
></ul>
<p id="task-stats"></p>
</main>
<script
type="module"
src="./app.js"
></script>
</body>
</html>
Final CSS
.todo-app {
width: min(100% - 32px, 700px);
margin: 40px auto;
}
.todo-app header {
margin-bottom: 24px;
}
#task-form {
display: grid;
gap: 10px;
}
#task-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #555555;
background: #333333;
color: #eeeeee;
font: inherit;
}
#task-input:focus-visible {
outline: 2px solid #ffb92d;
outline-offset: 2px;
}
#task-form button {
width: fit-content;
}
nav[aria-label="Task filters"] {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 25px 0;
}
.filter-button {
cursor: pointer;
}
.filter-button.is-active {
background: #aaff00;
color: #000000;
}
#task-list {
display: grid;
gap: 10px;
margin: 20px 0;
padding: 0;
list-style: none;
}
.task {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
border: 1px solid #555555;
}
.task__text {
overflow-wrap: anywhere;
}
.task__actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.task.is-complete {
opacity: 0.6;
}
.task.is-complete .task__text {
text-decoration: line-through;
}
#task-stats {
color: #aaaaaa;
}
@media (max-width: 600px) {
.task {
align-items: flex-start;
flex-direction: column;
}
.task__actions {
width: 100%;
}
}
Final JavaScript
const form =
document.querySelector("#task-form");
const input =
document.querySelector("#task-input");
const list =
document.querySelector("#task-list");
const stats =
document.querySelector("#task-stats");
const filterButtons =
document.querySelectorAll(
"[data-filter]"
);
const state = {
tasks: [],
filter: "all"
};
function createTask(text) {
return {
id: crypto.randomUUID(),
text,
completed: false
};
}
function saveTasks() {
localStorage.setItem(
"todo-tasks",
JSON.stringify(state.tasks)
);
}
function loadTasks() {
const saved =
localStorage.getItem("todo-tasks");
if (!saved) {
return;
}
try {
state.tasks =
JSON.parse(saved);
} catch (error) {
console.error(
"Could not load saved tasks:",
error
);
state.tasks = [];
}
}
function getVisibleTasks() {
if (state.filter === "active") {
return state.tasks.filter(
(task) => !task.completed
);
}
if (state.filter === "completed") {
return state.tasks.filter(
(task) => task.completed
);
}
return state.tasks;
}
function getStats() {
const total =
state.tasks.length;
const completed =
state.tasks.filter(
(task) => task.completed
).length;
const active =
total - completed;
return {
total,
active,
completed
};
}
function createTaskElement(task) {
const item =
document.createElement("li");
item.className = "task";
if (task.completed) {
item.classList.add("is-complete");
}
const text =
document.createElement("span");
text.className = "task__text";
text.textContent = task.text;
const actions =
document.createElement("div");
actions.className = "task__actions";
const completeButton =
document.createElement("button");
completeButton.type = "button";
completeButton.dataset.action = "toggle";
completeButton.dataset.id = task.id;
completeButton.textContent =
task.completed
? "Undo"
: "Complete";
const deleteButton =
document.createElement("button");
deleteButton.type = "button";
deleteButton.dataset.action = "delete";
deleteButton.dataset.id = task.id;
deleteButton.textContent = "Delete";
actions.append(
completeButton,
deleteButton
);
item.append(
text,
actions
);
return item;
}
function renderTasks() {
list.innerHTML = "";
const tasks =
getVisibleTasks();
for (const task of tasks) {
list.append(
createTaskElement(task)
);
}
}
function renderStats() {
const {
total,
active,
completed
} = getStats();
stats.textContent =
`${active} active · ` +
`${completed} completed · ` +
`${total} total`;
}
function renderFilters() {
filterButtons.forEach((button) => {
button.classList.toggle(
"is-active",
button.dataset.filter ===
state.filter
);
});
}
function render() {
renderTasks();
renderStats();
renderFilters();
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const text =
input.value.trim();
if (!text) {
return;
}
state.tasks.push(
createTask(text)
);
input.value = "";
saveTasks();
render();
input.focus();
});
list.addEventListener("click", (event) => {
const button =
event.target.closest(
"button[data-action]"
);
if (!button) {
return;
}
const id =
button.dataset.id;
const action =
button.dataset.action;
if (action === "toggle") {
state.tasks =
state.tasks.map((task) => {
if (task.id !== id) {
return task;
}
return {
...task,
completed:
!task.completed
};
});
}
if (action === "delete") {
state.tasks =
state.tasks.filter(
(task) => task.id !== id
);
}
saveTasks();
render();
});
filterButtons.forEach((button) => {
button.addEventListener(
"click",
() => {
state.filter =
button.dataset.filter;
render();
}
);
});
loadTasks();
render();