A
AiTechWorlds
AiTechWorlds
Build a fully featured todo application using vanilla JavaScript no frameworks. This project exercises DOM manipulation, event handling, local storage, and clean code organization. The result is a polished, deployable app.
todo-app/
├── index.html
├── style.css
└── app.js<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="container">
<h1>Todo <span class="badge" id="count">0</span></h1>
<div class="input-row">
<input type="text" id="todo-input" placeholder="What needs to be done?" autocomplete="off">
<button id="add-btn">Add</button>
</div>
<div class="filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="active">Active</button>
<button class="filter-btn" data-filter="completed">Completed</button>
</div>
<ul id="todo-list"></ul>
<div class="footer" id="footer">
<span id="remaining"></span>
<button id="clear-completed">Clear completed</button>
</div>
</main>
<script src="app.js"></script>
</body>
</html>// ─── State ─────────────────────────────────────────────────
const STORAGE_KEY = "todo-app-v1";
let state = {
todos: loadTodos(),
filter: "all",
nextId: loadNextId()
};
function loadTodos() {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]");
} catch {
return [];
}
}
function loadNextId() {
const todos = loadTodos();
return todos.length > 0 ? Math.max(...todos.map(t => t.id)) + 1 : 1;
}
function saveTodos() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.todos));
}
// ─── Todo Operations ────────────────────────────────────────
function addTodo(text) {
const trimmed = text.trim();
if (!trimmed) return;
state.todos.unshift({
id: state.nextId++,
text: trimmed,
completed: false,
createdAt: Date.now()
});
saveTodos();
render();
}
function toggleTodo(id) {
const todo = state.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
saveTodos();
render();
}
}
function deleteTodo(id) {
state.todos = state.todos.filter(t => t.id !== id);
saveTodos();
render();
}
function editTodo(id, newText) {
const todo = state.todos.find(t => t.id === id);
if (todo && newText.trim()) {
todo.text = newText.trim();
saveTodos();
render();
}
}
function clearCompleted() {
state.todos = state.todos.filter(t => !t.completed);
saveTodos();
render();
}
// ─── Filtering ──────────────────────────────────────────────
function getFilteredTodos() {
switch (state.filter) {
case "active": return state.todos.filter(t => !t.completed);
case "completed": return state.todos.filter(t => t.completed);
default: return state.todos;
}
}
// ─── Rendering ──────────────────────────────────────────────
function createTodoElement(todo) {
const li = document.createElement("li");
li.className = `todo-item ${todo.completed ? "completed" : ""}`;
li.dataset.id = todo.id;
li.innerHTML = `
<input type="checkbox" class="todo-check" ${todo.completed ? "checked" : ""}>
<span class="todo-text">${escapeHtml(todo.text)}</span>
<button class="delete-btn" aria-label="Delete todo">✕</button>
`;
return li;
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function render() {
const list = document.getElementById("todo-list");
const count = document.getElementById("count");
const remaining = document.getElementById("remaining");
const clearBtn = document.getElementById("clear-completed");
const footer = document.getElementById("footer");
const filtered = getFilteredTodos();
const activeCount = state.todos.filter(t => !t.completed).length;
const completedCount = state.todos.filter(t => t.completed).length;
// Update list
const fragment = document.createDocumentFragment();
filtered.forEach(todo => fragment.appendChild(createTodoElement(todo)));
list.innerHTML = "";
list.appendChild(fragment);
// Update counts
count.textContent = activeCount;
remaining.textContent = `${activeCount} item${activeCount !== 1 ? "s" : ""} left`;
// Show/hide clear button
clearBtn.style.display = completedCount > 0 ? "block" : "none";
footer.style.display = state.todos.length > 0 ? "flex" : "none";
// Update filter buttons
document.querySelectorAll(".filter-btn").forEach(btn => {
btn.classList.toggle("active", btn.dataset.filter === state.filter);
});
}
// ─── Event Listeners ────────────────────────────────────────
const input = document.getElementById("todo-input");
const addBtn = document.getElementById("add-btn");
addBtn.addEventListener("click", () => {
addTodo(input.value);
input.value = "";
input.focus();
});
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
addTodo(input.value);
input.value = "";
}
});
// Event delegation for todo list
document.getElementById("todo-list").addEventListener("click", (e) => {
const li = e.target.closest(".todo-item");
if (!li) return;
const id = parseInt(li.dataset.id);
if (e.target.classList.contains("todo-check")) {
toggleTodo(id);
} else if (e.target.classList.contains("delete-btn")) {
li.classList.add("removing");
setTimeout(() => deleteTodo(id), 200); // animate then remove
}
});
// Double-click to edit
document.getElementById("todo-list").addEventListener("dblclick", (e) => {
const li = e.target.closest(".todo-item");
if (!li || !e.target.classList.contains("todo-text")) return;
const id = parseInt(li.dataset.id);
const span = e.target;
const currentText = span.textContent;
const editInput = document.createElement("input");
editInput.type = "text";
editInput.value = currentText;
editInput.className = "edit-input";
span.replaceWith(editInput);
editInput.focus();
editInput.select();
function finishEdit() {
editTodo(id, editInput.value || currentText);
}
editInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") finishEdit();
if (e.key === "Escape") editTodo(id, currentText);
});
editInput.addEventListener("blur", finishEdit);
});
// Filter buttons
document.querySelector(".filters").addEventListener("click", (e) => {
const btn = e.target.closest(".filter-btn");
if (!btn) return;
state.filter = btn.dataset.filter;
render();
});
document.getElementById("clear-completed").addEventListener("click", clearCompleted);
// ─── Initialize ─────────────────────────────────────────────
render();
input.focus();* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f5f5f5;
min-height: 100vh;
display: grid;
place-items: start center;
padding: 2rem 1rem;
}
.container { width: 100%; max-width: 560px; }
h1 {
font-size: 2.5rem;
font-weight: 300;
color: #d9d9d9;
text-align: center;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
.badge {
background: #e74c3c;
color: white;
font-size: 1rem;
font-weight: 600;
border-radius: 999px;
padding: 2px 10px;
}
.input-row {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.input-row input {
flex: 1;
padding: 0.75rem 1rem;
font-size: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
outline: none;
}
.input-row input:focus { border-color: #4a90e2; box-shadow: 0 0 0 3px rgba(74,144,226,.2); }
.input-row button {
padding: 0.75rem 1.25rem;
background: #4a90e2;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
}
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.filter-btn {
padding: 0.4rem 0.9rem;
border: 1px solid transparent;
background: none;
border-radius: 6px;
cursor: pointer;
color: #555;
}
.filter-btn.active { border-color: #4a90e2; color: #4a90e2; }
#todo-list { list-style: none; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,.08); overflow: hidden; }
.todo-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid #f0f0f0;
transition: opacity 0.2s;
}
.todo-item:last-child { border-bottom: none; }
.todo-item.completed .todo-text { text-decoration: line-through; color: #aaa; }
.todo-item.removing { opacity: 0; }
.todo-check { width: 1.2rem; height: 1.2rem; cursor: pointer; accent-color: #4a90e2; }
.todo-text { flex: 1; }
.delete-btn { background: none; border: none; color: #ddd; cursor: pointer; font-size: 1rem; padding: 0.2rem; }
.delete-btn:hover { color: #e74c3c; }
.edit-input {
flex: 1;
border: none;
outline: 2px solid #4a90e2;
border-radius: 4px;
padding: 2px 6px;
font-size: 1rem;
}
.footer { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem 0; color: #888; font-size: 0.9rem; }
#clear-completed { background: none; border: none; color: #888; cursor: pointer; }
#clear-completed:hover { text-decoration: underline; }This project demonstrates the complete vanilla JavaScript skillset. The same patterns event delegation, state management, local persistence, optimistic updates are the foundation of what React and Vue do internally.
Next project: Weather App with API fetching live data and building a dynamic UI.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises