Task Manager App
Intermediate-Advanced Project — Master DOM, Events, localStorage & OOP
🧠 Deep Project Overview
A Task Manager looks simple, but it secretly trains you on 95% of what front-end devs do every day:
- • Reading user input
- • Updating the DOM based on app state
- • Saving data so it survives page refreshes
- • Organizing logic in classes instead of spaghetti code
- • Handling edge cases (empty tasks, filters, invalid input)
Apps like Todoist, Microsoft To Do, ClickUp, Trello all start from this same idea: a list of items with state (done/not done, priority, tags, etc.). If you can build this project cleanly, you're already thinking like a real JS dev.
🧱 Step 1 – Understand the Structure
Your HTML is split into three main zones:
1. Input Area (.input-group)
- • Text box for task title
- • <select> for priority (low, medium, high)
- • "Add Task" button that calls addTask()
2. Filter Bar (.filters)
- • 4 filter buttons: All, Active, Completed, High Priority
- • They call filterTasks('...')
3. Task List (#taskList)
- • An empty <ul> where JavaScript injects <li> items
💡 Tip: When you build other projects (Notes app, Budget tracker, Habit tracker), reuse this same 3-zone layout: inputs → controls/filters → results list.
🧠 Step 2 – The Brain: TaskManager Class
All the logic lives inside one class:
class TaskManager {
constructor() {
this.tasks = this.loadTasks();
this.currentFilter = 'all';
this.render();
}
}Every instance has:
- • tasks: an array of all task objects
- • currentFilter: which view is active (all, active, completed, high)
Each task looks like:
{
id: 1732012345678,
text: "Finish JavaScript lesson",
priority: "high",
completed: false,
createdAt: "2025-11-19T11:30:00.000Z"
}That's already proper data modeling – which is what you'll do with databases later.
💾 Step 3 – Saving and Loading with localStorage
Two key methods:
loadTasks() {
const saved = localStorage.getItem('tasks');
return saved ? JSON.parse(saved) : [];
}
saveTasks() {
localStorage.setItem('tasks', JSON.stringify(this.tasks));
}localStorage only stores strings, so you convert:
- • Save → JSON.stringify(this.tasks)
- • Load → JSON.parse(saved)
💡 Debug tip:
If the app acts weird, open DevTools → Application tab → Local Storage → clear the 'tasks' key, then refresh.
💡 Reuse:
This pattern (load → modify → save) is identical for: Notes apps, Shopping carts, Watch-later lists, "Recent code snippets" feature
➕ Step 4 – Adding Tasks
addTask(text, priority) {
if (!text.trim()) {
alert('Please enter a task!');
return;
}
const task = {
id: Date.now(),
text: text.trim(),
priority,
completed: false,
createdAt: new Date().toISOString()
};
this.tasks.unshift(task);
this.saveTasks();
this.render();
}What this teaches you:
- • Input validation – don't accept empty strings
- • Unique IDs – Date.now() is an easy, unique-enough ID for small apps
- • Unshift vs push – unshift() puts new tasks at the top of the list
- • Refresh UI – every change calls saveTasks() then render()
💡 Upgrade ideas:
- • Limit task length (e.g. max 120 characters) and show a little warning
- • Store a
dueDateproperty - • Store
isStarred: true/falsefor favourites
✅ Step 5 – Completing and Deleting Tasks
Toggle complete:
toggleTask(id) {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
this.saveTasks();
this.render();
}
}Delete task:
deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id);
this.saveTasks();
this.render();
}Patterns you're learning:
- • find() to get one item from an array
- • filter() to remove items by creating a new array
- • Always treat the array like state → modify → save → re-render
💡 Safer delete UX:
- • Show confirm("Are you sure?") before deleting
- • Or add a "Trash" filter where deleted tasks go instead of fully disappearing
🔍 Step 6 – Filters and UI State
getFilteredTasks() {
switch (this.currentFilter) {
case 'active':
return this.tasks.filter(t => !t.completed);
case 'completed':
return this.tasks.filter(t => t.completed);
case 'high':
return this.tasks.filter(t => t.priority === 'high');
default:
return this.tasks;
}
}Then in setFilter:
setFilter(filter) {
this.currentFilter = filter;
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
this.render();
}What you're doing here:
- • Keeping one source of truth: currentFilter
- • Styling the active button with a CSS class
- • Using .filter() to generate the current view
💡 Improvement (cleaner events):
Instead of using onclick in HTML, you could attach event listeners in JS and use e.target directly. This is called event delegation – super useful for dynamic content.
🖼️ Step 7 – Rendering HTML from Data
render() {
const taskList = document.getElementById('taskList');
const filtered = this.getFilteredTasks();
if (filtered.length === 0) {
taskList.innerHTML =
'<li style="text-align: center; color: #999; padding: 20px;">No tasks to show</li>';
return;
}
taskList.innerHTML = filtered.map(task => `
<li class="task-item ${task.completed ? 'completed' : ''}">
<div class="task-content">
<span>${task.text}</span>
<span class="priority priority-${task.priority}">
${task.priority.toUpperCase()}
</span>
</div>
<div class="task-actions">
<button class="complete-btn" onclick="taskManager.toggleTask(${task.id})">
${task.completed ? '↺' : '✓'}
</button>
<button class="delete-btn" onclick="taskManager.deleteTask(${task.id})">✕</button>
</div>
</li>
`).join('');
}Key patterns:
- • Template literals for clean HTML
- • Using dynamic classes (completed, priority-high, etc.)
- • One
.innerHTML = ...per render → avoids many DOM mutations
💡 Debug tip:
If nothing shows:
- 1. console.log(filtered) before you map
- 2. Check for JS errors in DevTools console
- 3. Make sure id="taskList" exists in the HTML
⌨️ Step 8 – Global Functions & Keyboard UX
Global helpers:
function addTask() {
const input = document.getElementById('taskInput');
const priority = document.getElementById('prioritySelect').value;
taskManager.addTask(input.value, priority);
input.value = '';
}
function filterTasks(filter) {
taskManager.setFilter(filter);
}Enter key support:
document.getElementById('taskInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addTask();
});This is already nice UX – users can quickly hit Enter instead of clicking.
💡 Upgrade:
- • Disable the Add button when input is empty
- • Focus the text input on page load
💻 Try It Yourself
Test the complete Task Manager app below. Study the code, modify it, and experiment with the features!
Task Manager Starter Code
Build a full task manager with localStorage persistence
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Task Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
...🧪 What to Practice / Extend
Here's how to use this project to become scary-good at JavaScript:
1. Add due dates
- • Add an <input type="date">
- • Store dueDate in the task object
- • Sort by dueDate in render() or create a "Due soon" filter
2. Inline editing
- • On click of task text, turn it into an <input>
- • On blur / Enter, update task.text, save, re-render
3. Search bar
- • Add <input id="search">
- • On input event, filter by both currentFilter + search query
4. Animations
- • Use CSS transitions on .task-item (opacity, transform)
- • Add a quick fade-in class when a task is created
5. Dark Mode
- • Toggle data-theme="dark" on <body>
- • Use CSS variables for colours and swap them per theme
🧾 Final Checklist for "Job-Ready" Level
Before you call this project done, check:
- ☐No errors in DevTools console
- ☐Works on mobile (try Chrome's responsive mode)
- ☐Handles 100+ tasks without feeling laggy
- ☐All filters work correctly
- ☐Data survives refresh and browser close
- ☐Code is split into logical methods, not one giant function
- ☐Important parts have comments explaining why, not just what