Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
🖱️ Days 8–14 – Data, DOM और Events  ·  दिन 14

दिन 14: Mini Project: To-Do App

⏱ 3 hours📶 Project🎯 100% self-study
दिन 14 / 30

✅ आज आप सीखेंगे

  • App को state + actions + render के रूप में plan करना
  • Validation, priorities और due dates के साथ tasks जोड़ना
  • Tasks complete, edit (double-click / ✏️), delete और filter करना
  • पूरी list के लिए एक delegated listener
  • localStorage में सुरक्षित save और blocked storage संभालना

🏢 असली दुनिया में

Diwali हफ़्ते में Sharma Sweets के पास रोज़ 50 छोटे काम होते हैं: काजू मँगाना, gift boxes pack करना, शादी वाले customers को call करना। काग़ज़ की lists खो जाती हैं। दुकान के phone पर tasks याद रखने वाला सरल, तेज़ to-do app असली समस्या हल करता है – और to-do app क्लासिक interview project है क्योंकि इसमें Days 1–13 की हर बुनियादी चीज़ लगती है।

तैयार app

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

यह preview sandboxed frame में चलता है जहाँ browsers localStorage रोकते हैं। App यह पहचानकर memory में चलता रहता है। Download kit से file खोलें तो reload के बाद भी tasks बचे रहते हैं – यही सलीक़े वाला fallback professional code करता है।

Step 1: data और features plan करें

FeatureState बदलावConcept (Day)
Task जोड़नाtasks.unshift({...})Forms और validation (13)
Completetask.done = !task.donefind (8), events (11)
EditeditingId = id → title = नया textKeyboard events (11)
Deletetasks = tasks.filter(...)filter (8)
Filter All / Active / Donefilter = "active"State → render (12)
Sort: खुले पहले, high priority पहलेvisibleTasks() में गणनाCompare से sort (8)
Tasks याद रखनाlocalStorage + JSONJSON (7), Day 18 के try…catch की झलक
Overdue चेतावनीdue < 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>
  • हर field का label है (.sr-only से दिखने से छिपा) ताकि screen readers बोलें।
  • Counter aria-live="polite" लेता है – बदलाव के बाद screen readers “2 tasks left” बोलते हैं।
  • Filter buttons aria-pressed से बताते हैं कौन सा filter चालू है।
  • Script defer लेती है – कोई “null” errors नहीं।

Step 3: कभी crash न होने वाले storage helpers

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 और 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();
}
💡 हर action commit() पर ख़त्म

commit() save करके दोबारा render करता है। हर बदलाव एक function से गुज़रता है, इसलिए screen और saved data कभी अलग नहीं हो सकते – UI programming की सबसे ज़रूरी आदत।

Step 5: render – state 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() मौजूदा tab से filter करके sort करता है: अधूरे पहले, फिर high → normal → low, फिर नए।
  • Task titles textContent से डाले जाते हैं – <img onerror=…> नाम का task text दिखता है, चलता नहीं।
  • replaceChildren() एक DOM update में पूरी list बदलता है।
  • Render के बाद खुले edit box पर focus आता है ताकि user तुरंत लिख सके।

Step 6: delegation के साथ events

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 करता है
लिखकर Enter / AddForm पर submitValidate, addTask, reset, दोबारा focus
Checkbox tickList पर click (delegated)toggleTask(id)
✏️ या title पर double-clickclick / dblclickeditingId = id, render
Edit box में Enter / Esc / बाहर जानाkeydown / focusoutrenameTask या रद्द
🗑️clickconfirm, deleteTask
Filter pillNav पर click (delegated)state.filter = …, render

Step 7: QA engineer की तरह test

Testअपेक्षितहमारा नतीजा
1-character task जोड़ेंError “at least 2 characters”✅
वही खुला task दो बारError “already in your list”✅
Task tick करके Active चुनेंTask Active से ग़ायब✅
✏️ से edit, लिखें, EnterTitle बदला✅
Delete करके page reloadDeleted task deleted ही रहे✅
Storage blocked (यह preview)App चलता रहे, note दिखे✅
<b>x</b> नाम का taskText के रूप में✅
सिर्फ़ keyboardहर control Tab से पहुँचे✅

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

✔ App को state, actions और render के रूप में design करना✔ Forms, delegation और keyboard editing एक साथ✔ localStorage और JSON से data सुरक्षित रखना✔ App को व्यवस्थित ढंग से test करना

Practice tasks

🏋️ Practice – ख़ुद करें

Task 1: लिखते समय text से tasks filter करने वाला search box जोड़ें (state.search जोड़कर visibleTasks() में इस्तेमाल करें)।
Task 2: Drag के बिना क्रम बदलना जोड़ें: ▲ ▼ buttons जो task को array में ऊपर या नीचे करें।
Task 3: पूरे हुए tasks के प्रतिशत वाला progress bar (<progress>) दिखाएँ।

Quick quiz

🧠 Quick Quiz – ख़ुद को जाँचें

1. हर action commit() क्यों बुलाता है?
बदलावों का एक रास्ता।
2. localStorage values रखता है…
JSON.stringify/parse लें।
3. localStorage को try…catch में क्यों?
Private mode, sandbox, quota।
4. Task titles textContent से क्यों डाले जाते हैं?
User text HTML के रूप में parse नहीं होता।
5. सब task buttons कितने click listeners संभालते हैं?
Event delegation।

Interview corner

💼 Interview Corner

अपने to-do app का architecture समझाइए।

State object (tasks, filter, editingId) एकमात्र सत्य स्रोत; action functions जो state बदलकर commit() बुलाते हैं; commit localStorage में save करता है (try/catch fallback के साथ) और render() बुलाता है; render filtered, sorted list बनाकर textContent से सुरक्षित DOM बनाता है; delegated event listeners user input को actions में बदलते हैं।

Devices के बीच tasks sync कैसे करेंगे?

Storage helpers की जगह API calls (authentication वाले backend पर fetch), तुरंत UI के लिए local state (optimistic updates), offline में बदलाव queue करना, और timestamps से conflicts सुलझाना।

सारांश

  • State + actions + commit() + render() = अनुमानित apps।
  • Delegated listeners हर task button संभालते हैं।
  • User text के लिए textContent; तेज़ updates के लिए replaceChildren।
  • JSON और try/catch fallback के साथ localStorage।
➡️ आगे

Day 15: localStorage, sessionStorage और JSON गहराई से – carts, themes और drafts याद रखें।

अक्सर पूछे जाने वाले प्रश्न (FAQ)

क्या यह app portfolio में डाल सकता हूँ?
हाँ – अपना feature जोड़ें (search, categories, dark mode) और HTML course की तरह GitHub Pages पर मुफ़्त host करें।
ids के लिए Date.now() क्यों?
हाथ से tasks जोड़ने वाले एक user के लिए काफ़ी अनोखा। Multi-user systems के लिए crypto.randomUUID() या database ids।
← JavaScript (30 Days)
🔗

Share this topic with a friend

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

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

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

अगला topicदिन 15: localStorage, sessionStorage और JSON आपकी प्रगति: 14 / 30 (47%) · Lesson 15 / 30 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें