दिन 14: Mini Project: To-Do App
✅ आज आप सीखेंगे
- 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
यह preview sandboxed frame में चलता है जहाँ browsers localStorage रोकते हैं। App यह पहचानकर memory में चलता रहता है। Download kit से file खोलें तो reload के बाद भी tasks बचे रहते हैं – यही सलीक़े वाला fallback professional code करता है।
Step 1: data और features plan करें
| Feature | State बदलाव | Concept (Day) |
|---|---|---|
| Task जोड़ना | tasks.unshift({...}) | Forms और validation (13) |
| Complete | task.done = !task.done | find (8), events (11) |
| Edit | editingId = id → title = नया text | Keyboard events (11) |
| Delete | tasks = tasks.filter(...) | filter (8) |
| Filter All / Active / Done | filter = "active" | State → render (12) |
| Sort: खुले पहले, high priority पहले | visibleTasks() में गणना | Compare से sort (8) |
| Tasks याद रखना | localStorage + JSON | JSON (7), Day 18 के try…catch की झलक |
| Overdue चेतावनी | due < today | Dates (9) |
{
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
<!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
// ---------- 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
// ---------- 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);
}// ---------- 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();
}commit() save करके दोबारा render करता है। हर बदलाव एक function से गुज़रता है, इसलिए screen और saved data कभी अलग नहीं हो सकते – UI programming की सबसे ज़रूरी आदत।
Step 5: render – state HTML बनती है
// ---------- 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
// ---------- 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 action | Event | Handler करता है |
|---|---|---|
| लिखकर Enter / Add | Form पर submit | Validate, addTask, reset, दोबारा focus |
| Checkbox tick | List पर click (delegated) | toggleTask(id) |
| ✏️ या title पर double-click | click / dblclick | editingId = id, render |
| Edit box में Enter / Esc / बाहर जाना | keydown / focusout | renameTask या रद्द |
| 🗑️ | click | confirm, deleteTask |
| Filter pill | Nav पर 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, लिखें, Enter | Title बदला | ✅ |
| Delete करके page reload | Deleted task deleted ही रहे | ✅ |
| Storage blocked (यह preview) | App चलता रहे, note दिखे | ✅ |
| <b>x</b> नाम का task | Text के रूप में | ✅ |
| सिर्फ़ keyboard | हर control Tab से पहुँचे | ✅ |
Stylesheet
: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
Practice tasks
🏋️ Practice – ख़ुद करें
Quick quiz
🧠 Quick Quiz – ख़ुद को जाँचें
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 याद रखें।
