Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🖱️ Days 8–14 – Data, DOM & Events  ·  Day 14

Day 14: Mini Project: To-Do App

⏱ 3 hours📶 Project🎯 100% self-study
Day 14 / 30

✅ Today you will learn

  • Plan an app as state + actions + render
  • Add tasks with validation, priorities and due dates
  • Complete, edit (double-click / ✏️), delete and filter tasks
  • One delegated listener for the whole list
  • Save to localStorage safely and handle blocked storage

🏢 In the real world

During Diwali week Sharma Sweets has 50 small jobs a day: order cashews, pack gift boxes, call wedding customers. Paper lists get lost. A simple, fast to-do app that remembers tasks on the shop’s phone solves a real problem – and the to-do app is the classic interview project because it uses every fundamental from Days 1–13.

The finished app

▶  Try it: add a task, tick it, double-click a title to edit, use the filters
🧠 Why the “Saving is disabled” note?

This preview runs in a sandboxed frame where browsers block localStorage. The app detects that and keeps working in memory. Open the file from the download kit and your tasks survive a reload – that graceful fallback is exactly what professional code does.

Step 1: plan the data and features

FeatureState changeConcept (Day)
Add tasktasks.unshift({...})Forms & validation (13)
Completetask.done = !task.donefind (8), events (11)
EditeditingId = id → title = new textKeyboard events (11)
Deletetasks = tasks.filter(...)filter (8)
Filter All / Active / Donefilter = "active"State → render (12)
Sort: open first, high priority firstcomputed in visibleTasks()sort with compare (8)
Remember taskslocalStorage + JSONJSON (7), try…catch preview of Day 18
Overdue warningdue < todayDates (9)
Data shape – one task object
{
  id: 1761380000000,          // Date.now() – unique enough for one user
  title: 'Pack 20 Diwali gift boxes',
  done: false,
  priority: 'high',           // 'high' | 'normal' | 'low'
  due: '2026-10-28',          // ISO date string or ''
  createdAt: 1761380000000
}

Step 2: semantic, accessible HTML

HTML – index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My Tasks – To-Do App (JavaScript Mini Project)</title>
  <meta name="description" content="A to-do app built with plain JavaScript: add, edit, complete, filter and delete tasks, with priorities, due dates and saving in localStorage.">
  <link rel="stylesheet" href="style.css">
  <script src="app.js" defer></script>
</head>
<body>
  <main class="app">
    <header class="app__head">
      <h1>📝 My Tasks</h1>
      <p class="app__count" id="count" aria-live="polite"></p>
    </header>

    <form class="add" id="add-form" autocomplete="off">
      <label class="sr-only" for="task-input">New task</label>
      <input id="task-input" name="title" placeholder="e.g. Pack 20 Diwali boxes" maxlength="120" required>
      <label class="sr-only" for="task-priority">Priority</label>
      <select id="task-priority" name="priority">
        <option value="normal">Normal</option>
        <option value="high">High 🔥</option>
        <option value="low">Low</option>
      </select>
      <label class="sr-only" for="task-due">Due date</label>
      <input id="task-due" name="due" type="date">
      <button type="submit">Add</button>
    </form>
    <p class="error" id="error" role="alert"></p>

    <nav class="filters" aria-label="Filter tasks">
      <button type="button" data-filter="all" aria-pressed="true">All</button>
      <button type="button" data-filter="active" aria-pressed="false">Active</button>
      <button type="button" data-filter="done" aria-pressed="false">Done</button>
      <button type="button" class="link" id="clear-done">Clear completed</button>
    </nav>

    <ul class="list" id="list"></ul>
    <p class="empty" id="empty">No tasks here. Add your first one above! 🎉</p>
    <p class="note" id="storage-note" hidden>⚠️ Saving is disabled in this browser mode – tasks will reset on reload.</p>
  </main>
</body>
</html>
  • Labels exist for every field (visually hidden with .sr-only) so screen readers announce them.
  • The counter uses aria-live="polite" – screen readers announce “2 tasks left” after changes.
  • Filter buttons use aria-pressed to expose which filter is active.
  • The script uses defer – no “null” errors.

Step 3: storage helpers that never crash

JavaScript – 1. storage helpers
// ---------- 1. STORAGE HELPERS (safe even when storage is blocked) ----------
const STORAGE_KEY = 'ce-todo-v1';
let storageWorks = true;

function loadTasks() {
  try {
    const saved = localStorage.getItem(STORAGE_KEY);
    return saved ? JSON.parse(saved) : [];
  } catch (err) {
    storageWorks = false;            // private mode, sandboxed preview, etc.
    return [];
  }
}

function saveTasks(tasks) {
  if (!storageWorks) return;
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
  } catch (err) {
    storageWorks = false;
  }
}

Step 4: state and actions

JavaScript – 2. state
// ---------- 2. STATE ----------
const state = {
  tasks: loadTasks(),     // [{ id, title, done, priority, due, createdAt }]
  filter: 'all',          // 'all' | 'active' | 'done'
  editingId: null,
};

if (state.tasks.length === 0 && !localStorageHasKey()) {
  // friendly sample tasks on the very first visit
  state.tasks = [
    { id: 1, title: 'Order cashews for kaju katli', done: true, priority: 'normal', due: '', createdAt: Date.now() - 3000 },
    { id: 2, title: 'Pack 20 Diwali gift boxes', done: false, priority: 'high', due: todayISO(), createdAt: Date.now() - 2000 },
    { id: 3, title: 'Update the website menu prices', done: false, priority: 'low', due: '', createdAt: Date.now() - 1000 },
  ];
}

function localStorageHasKey() {
  try { return localStorage.getItem(STORAGE_KEY) !== null; } catch (err) { return false; }
}

function todayISO() {
  const d = new Date();
  d.setMinutes(d.getMinutes() - d.getTimezoneOffset());   // local date, not UTC
  return d.toISOString().slice(0, 10);
}
JavaScript – 3. actions
// ---------- 3. ACTIONS (change state → save → render) ----------
function addTask(title, priority, due) {
  state.tasks.unshift({ id: Date.now(), title, done: false, priority, due, createdAt: Date.now() });
  commit();
}

function toggleTask(id) {
  const task = state.tasks.find(t => t.id === id);
  if (task) task.done = !task.done;
  commit();
}

function renameTask(id, title) {
  const task = state.tasks.find(t => t.id === id);
  if (task && title.trim()) task.title = title.trim();
  state.editingId = null;
  commit();
}

function deleteTask(id) {
  state.tasks = state.tasks.filter(t => t.id !== id);
  commit();
}

function clearDone() {
  state.tasks = state.tasks.filter(t => !t.done);
  commit();
}

function commit() {
  saveTasks(state.tasks);
  render();
}
💡 Every action ends with commit()

commit() saves and re-renders. Because every change goes through one function, the screen and the saved data can never disagree – the single most important habit in UI programming.

Step 5: render – state becomes HTML

JavaScript – 4. render
// ---------- 4. RENDER (state → HTML) ----------
const els = {
  form: document.querySelector('#add-form'),
  input: document.querySelector('#task-input'),
  priority: document.querySelector('#task-priority'),
  due: document.querySelector('#task-due'),
  error: document.querySelector('#error'),
  list: document.querySelector('#list'),
  empty: document.querySelector('#empty'),
  count: document.querySelector('#count'),
  filters: document.querySelector('.filters'),
  clearDone: document.querySelector('#clear-done'),
  storageNote: document.querySelector('#storage-note'),
};

const PRIORITY_ORDER = { high: 0, normal: 1, low: 2 };

function visibleTasks() {
  return state.tasks
    .filter(t => state.filter === 'all' || (state.filter === 'done' ? t.done : !t.done))
    .sort((a, b) => Number(a.done) - Number(b.done) || PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority] || b.createdAt - a.createdAt);
}

function createTaskItem(task) {
  const li = document.createElement('li');
  const overdue = !task.done && task.due && task.due < todayISO();
  li.className = 'task' + (task.done ? ' task--done' : '') + (task.priority === 'high' ? ' task--high' : '') + (overdue ? ' task--overdue' : '');
  li.dataset.id = task.id;

  const check = document.createElement('input');
  check.type = 'checkbox';
  check.checked = task.done;
  check.dataset.action = 'toggle';
  check.setAttribute('aria-label', 'Mark “' + task.title + '” as done');

  const body = document.createElement('div');
  body.className = 'task__body';
  if (state.editingId === task.id) {
    const edit = document.createElement('input');
    edit.className = 'task__edit';
    edit.value = task.title;
    edit.dataset.action = 'edit-input';
    edit.setAttribute('aria-label', 'Edit task');
    body.append(edit);
  } else {
    const title = document.createElement('span');
    title.className = 'task__title';
    title.textContent = task.title;                // textContent = safe from XSS
    const meta = document.createElement('small');
    meta.className = 'task__meta';
    const parts = [];
    if (task.priority === 'high') parts.push('🔥 High priority');
    if (task.priority === 'low') parts.push('Low priority');
    if (task.due) parts.push((overdue ? 'Overdue: ' : 'Due ') + formatDate(task.due));
    meta.textContent = parts.join(' · ');
    body.append(title, meta);
  }

  const editBtn = iconButton('✏️', 'edit', 'Edit “' + task.title + '”');
  const delBtn = iconButton('🗑️', 'delete', 'Delete “' + task.title + '”');
  li.append(check, body, editBtn, delBtn);
  return li;
}

function iconButton(symbol, action, label) {
  const b = document.createElement('button');
  b.type = 'button';
  b.className = 'icon';
  b.textContent = symbol;
  b.dataset.action = action;
  b.setAttribute('aria-label', label);
  return b;
}

function formatDate(iso) {
  return new Intl.DateTimeFormat('en-IN', { day: 'numeric', month: 'short' }).format(new Date(iso + 'T00:00:00'));
}

function render() {
  const tasks = visibleTasks();
  els.list.replaceChildren(...tasks.map(createTaskItem));
  const editBox = els.list.querySelector('.task__edit');
  if (editBox) { editBox.focus(); editBox.select(); }      // put the cursor in the edit box
  els.empty.hidden = tasks.length > 0;
  const left = state.tasks.filter(t => !t.done).length;
  els.count.textContent = `${left} task${left === 1 ? '' : 's'} left · ${state.tasks.length} total`;
  els.filters.querySelectorAll('[data-filter]').forEach(b => b.setAttribute('aria-pressed', b.dataset.filter === state.filter));
  els.clearDone.hidden = !state.tasks.some(t => t.done);
  els.storageNote.hidden = storageWorks;
}
  • visibleTasks() filters by the current tab, then sorts: unfinished first, then high → normal → low, then newest.
  • Task titles are inserted with textContent – a task named <img onerror=…> is shown as text, not executed.
  • replaceChildren() swaps the whole list in one DOM update.
  • After rendering, an open edit box receives focus so the user can type immediately.

Step 6: events with delegation

JavaScript – 5. events + 6. start
// ---------- 5. EVENTS ----------
els.form.addEventListener('submit', e => {
  e.preventDefault();
  const title = els.input.value.trim();
  if (title.length < 2) {
    els.error.textContent = 'Please type at least 2 characters.';
    els.input.focus();
    return;
  }
  if (state.tasks.some(t => t.title.toLowerCase() === title.toLowerCase() && !t.done)) {
    els.error.textContent = 'This task is already in your list.';
    return;
  }
  els.error.textContent = '';
  addTask(title, els.priority.value, els.due.value);
  els.form.reset();
  els.input.focus();
});

// one listener for every task (event delegation)
els.list.addEventListener('click', e => {
  const target = e.target.closest('[data-action]');
  if (!target) return;
  const id = Number(target.closest('.task').dataset.id);
  const action = target.dataset.action;
  if (action === 'toggle') toggleTask(id);
  if (action === 'delete' && confirm('Delete this task?')) deleteTask(id);
  if (action === 'edit') { state.editingId = id; render(); }
});

els.list.addEventListener('dblclick', e => {
  const item = e.target.closest('.task');
  if (item && e.target.closest('.task__title')) { state.editingId = Number(item.dataset.id); render(); }
});

els.list.addEventListener('keydown', e => {
  if (e.target.dataset.action !== 'edit-input') return;
  const id = Number(e.target.closest('.task').dataset.id);
  if (e.key === 'Enter') renameTask(id, e.target.value);
  if (e.key === 'Escape') { state.editingId = null; render(); }
});

els.list.addEventListener('focusout', e => {
  if (e.target.dataset.action === 'edit-input' && state.editingId !== null) {
    renameTask(Number(e.target.closest('.task').dataset.id), e.target.value);
  }
});

els.filters.addEventListener('click', e => {
  const btn = e.target.closest('[data-filter]');
  if (!btn) return;
  state.filter = btn.dataset.filter;
  render();
});

els.clearDone.addEventListener('click', clearDone);

// ---------- 6. START ----------
els.due.min = todayISO();
render();
User actionEventHandler does
Types and presses Enter / Addsubmit on the formValidate, addTask, reset, refocus
Ticks a checkboxclick on list (delegated)toggleTask(id)
✏️ or double-click titleclick / dblclickeditingId = id, render
Enter / Esc / leaves the edit boxkeydown / focusoutrenameTask or cancel
🗑️clickconfirm, deleteTask
Filter pillclick on nav (delegated)state.filter = …, render

Step 7: test like a QA engineer

TestExpectedOur result
Add a 1-character taskError “at least 2 characters”✅
Add the same open task twiceError “already in your list”✅
Tick a task, choose ActiveTask disappears from Active✅
Edit with ✏️, type, EnterTitle updated✅
Delete, then reload the pageDeleted task stays deleted✅
Storage blocked (this preview)App still works, shows a note✅
Task named <b>x</b>Shown as text✅
Keyboard onlyEvery control reachable with Tab✅

The stylesheet

CSS – style.css
:root { --brand: #ca8a04; --ink: #1f2937; --muted: #6b7280; --line: #e5e7eb; --bg: #fffbeb; --card: #fff; --high: #dc2626; }
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--ink); }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
button, input, select { font: inherit; }
button { cursor: pointer; }
:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }

.app { width: min(640px, 100% - 24px); margin: 24px auto; background: var(--card); border-radius: 18px; padding: 20px; box-shadow: 0 12px 30px rgb(0 0 0 / 8%); }
.app__head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.app__head h1 { margin: 0; font-size: 1.6rem; }
.app__count { margin: 0; color: var(--muted); }

.add { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
.add input[name="title"] { flex: 1 1 220px; }
.add input, .add select { padding: 10px 12px; border: 1.5px solid var(--line); border-radius: 10px; background: #fff; }
.add button { padding: 10px 18px; border: 0; border-radius: 10px; background: var(--brand); color: #fff; font-weight: 700; }
.error { color: var(--high); min-height: 1.2em; margin: 6px 0 0; font-size: .9rem; }

.filters { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 12px; }
.filters button { border: 1.5px solid var(--line); background: #fff; border-radius: 99px; padding: 6px 14px; }
.filters button[aria-pressed="true"] { background: var(--ink); color: #fff; border-color: var(--ink); }
.filters .link { margin-left: auto; border: 0; background: none; color: var(--muted); text-decoration: underline; }

.list { list-style: none; padding: 0; margin: 0; }
.task { display: flex; align-items: center; gap: 10px; padding: 10px 8px; border-bottom: 1px solid var(--line); animation: pop .2s ease-out; }
@keyframes pop { from { opacity: 0; transform: translateY(-4px); } }
.task input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--brand); flex: 0 0 auto; }
.task__body { flex: 1; min-width: 0; }
.task__title { overflow-wrap: anywhere; }
.task__meta { display: block; font-size: .8rem; color: var(--muted); }
.task--done .task__title { text-decoration: line-through; color: var(--muted); }
.task--high { border-left: 4px solid var(--high); padding-left: 8px; }
.task--overdue .task__meta { color: var(--high); font-weight: 600; }
.task__edit { width: 100%; padding: 6px 8px; border: 1.5px solid var(--brand); border-radius: 8px; }
.icon { border: 0; background: none; font-size: 1.1rem; padding: 4px 6px; border-radius: 8px; }
.icon:hover { background: var(--bg); }
.empty, .note { text-align: center; color: var(--muted); }

@media (prefers-reduced-motion: reduce) { .task { animation: none; } }

🏅 Job skills unlocked today

✔ Design an app as state, actions and render✔ Handle forms, delegation and keyboard editing together✔ Persist data with localStorage and JSON safely✔ Test an app systematically

Practice tasks

🏋️ Practice – do it yourself

Task 1: Add a search box that filters tasks by text as you type (add state.search and use it in visibleTasks()).
Task 2: Add drag-free reordering: ▲ ▼ buttons that move a task up or down in the array.
Task 3: Show a progress bar (<progress>) with the percentage of tasks completed.

Quick quiz

🧠 Quick Quiz – test yourself

1. Why does every action call commit()?
Single path for changes.
2. localStorage stores values as…
Use JSON.stringify/parse.
3. Why wrap localStorage in try…catch?
Private mode, sandbox, quota.
4. Task titles are inserted with textContent to…
User text is never parsed as HTML.
5. How many click listeners handle all task buttons?
Event delegation.

Interview corner

💼 Interview Corner

Walk me through your to-do app architecture.

State object (tasks, filter, editingId) as the single source of truth; pure-ish action functions that change state and call commit(); commit saves to localStorage (with try/catch fallback) and calls render(); render derives the filtered, sorted list and builds DOM safely with textContent; delegated event listeners translate user input into actions.

How would you sync tasks across devices?

Replace the storage helpers with API calls (fetch to a backend with authentication), keep local state for instant UI (optimistic updates), handle offline mode by queueing changes, and resolve conflicts with timestamps.

Summary

  • State + actions + commit() + render() = predictable apps.
  • Delegated listeners handle every task button.
  • textContent for user text; replaceChildren for fast updates.
  • localStorage with JSON and a try/catch fallback.
➡️ Next

Day 15: localStorage, sessionStorage and JSON in depth – remember carts, themes and drafts.

Frequently Asked Questions

Can I put this app on my portfolio?
Yes – add your own feature (search, categories, dark mode) and host it free on GitHub Pages like in the HTML course.
Why Date.now() for ids?
It is unique enough for one user adding tasks by hand. For multi-user systems use crypto.randomUUID() or database ids.
← JavaScript (30 Days)
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicDay 15: localStorage, sessionStorage & JSON Your progress: 14 / 30 (47%) · Lesson 15 / 30 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free