ওয়েব ডেভেলপমেন্ট শেখার শুরুতে To-do app একটি চমৎকার প্রকল্প। কারণ ছোট একটি ইন্টারফেসের মধ্যে HTML, CSS, JavaScript, DOM নির্বাচন, ইভেন্ট, ডেটা মডেল এবং ব্রাউজার স্টোরেজ—সব একসঙ্গে অনুশীলন করা যায়। এই নিবন্ধে আমরা এমন একটি সম্পূর্ণ To-do app বানাব, যেখানে নতুন কাজ যোগ করা, কাজ সম্পন্ন হিসেবে চিহ্নিত করা, কাজ মুছে ফেলা এবং পেজ রিফ্রেশের পরেও কাজের তালিকা ধরে রাখা যাবে। পুরো প্রকল্পটি একটি মাত্র index.html ফাইলে থাকবে; কোনও framework, package বা CDN লাগবে না।
এখানে লক্ষ্য শুধু কয়েকটি বাটন চালু করা নয়। আমরা কেন একটি নির্দিষ্ট ডেটা কাঠামো বেছে নিচ্ছি, DOM-এ কীভাবে নিরাপদে লেখা বসাব, localStorage থেকে আসা JSON কীভাবে যাচাই করব, নষ্ট ডেটা পেলে কেন ব্যবহারকারীকে জানাব এবং সংবেদনশীল তথ্য কেন ব্রাউজারের এই স্টোরেজে রাখা উচিত নয়—এসবও ধাপে ধাপে দেখা হবে। ফলে একই ধারণা পরে নোট, বুকমার্ক, খরচের হিসাব বা ছোট inventory app-এ প্রয়োগ করতে পারবেন।
কী বানাব
অ্যাপটির মূল কাজ চারটি। প্রথমত, ইনপুটে লেখা একটি কাজ তালিকায় যোগ হবে। দ্বিতীয়ত, প্রতিটি কাজের পাশে checkbox থাকবে; সেটি বদলালে completed মান পরিবর্তিত হবে এবং দৃশ্যমান স্টাইলও বদলাবে। তৃতীয়ত, delete বোতাম নির্দিষ্ট কাজটি সরাবে। চতুর্থত, প্রতিটি পরিবর্তনের পরে তালিকা JSON আকারে localStorage-এ সংরক্ষিত হবে। পেজ খুললে সংরক্ষিত তালিকা আবার পড়া হবে।
প্রতিটি task-কে আমরা একটি object হিসেবে রাখব। উদাহরণস্বরূপ, একটি item-এর আকার হতে পারে { id, text, completed, createdAt }। id দিয়ে কোনও কাজকে আলাদা করে চেনা হবে, text হবে ব্যবহারকারীর লেখা, completed কেবল boolean এবং createdAt ভবিষ্যতে sort বা audit-এর কাজে লাগতে পারে। পুরো তালিকা হবে এই object-গুলোর array। এই সরল schema রাখা জরুরি, কারণ পরে নতুন property যোগ করলেও পুরনো ডেটা বোঝা সহজ থাকে।
DOM এবং state-এর সম্পর্ক
DOM হলো ব্রাউজারের তৈরি HTML tree, যেখানে প্রতিটি heading, form, input এবং list item একটি node। JavaScript দিয়ে আমরা querySelector করে node ধরে event listener বসাব। কিন্তু DOM-কে একমাত্র সত্য ধরে নিয়ে আলাদা আলাদা জায়গায় পরিবর্তন করলে state ছড়িয়ে যায় এবং bug বাড়ে। তাই এখানে tasks নামের একটি JavaScript array-কে application state হিসেবে রাখব। state বদলানোর পরে render() function পুরো list নতুন করে আঁকবে এবং saveTasks() সেটি সংরক্ষণ করবে।
এই পদ্ধতির সুবিধা হলো checkbox বদলানো, নতুন item যোগ করা বা delete করা—সব action-এর পরে একই নিয়ম কাজ করে। render() প্রথমে list খালি করে, তারপর state-এর প্রতিটি task-এর জন্য প্রয়োজনীয় element বানায়। এখানে সাধারণ text-এর জন্য textContent ব্যবহার করব, innerHTML নয়। ব্যবহারকারী যদি <img onerror=...>-এর মতো কিছু লেখেন, textContent সেটিকে কেবল লেখা হিসেবে দেখাবে; ব্রাউজার সেটিকে HTML হিসেবে চালাবে না। নিরাপদ output handling-এর এই অভ্যাস ছোট প্রকল্পেও বজায় রাখা উচিত।
localStorage কীভাবে কাজ করে
localStorage key এবং string value হিসেবে ডেটা রাখে। তাই array সরাসরি রাখা যায় না; JSON.stringify(tasks) দিয়ে string বানিয়ে রাখতে হয়, আবার পড়ার সময় JSON.parse() করতে হয়। MDN-এর Window.localStorage-এর অফিসিয়াল ডকুমেন্টেশন অনুযায়ী storage origin এবং protocol-ভিত্তিক। অর্থাৎ একই origin-এ পরে খোলা পেজ একই data পেতে পারে, কিন্তু http ও https আলাদা হতে পারে।
এই উদাহরণটি file:// দিয়ে double-click করে খোলার জন্য নির্ভরযোগ্যভাবে তৈরি করা হয়নি, কারণ file scheme-এ localStorage-এর আচরণ browser ভেদে undefined হতে পারে। বরং project folder-এ terminal খুলে python3 -m http.server 8000 চালান এবং http://localhost:8000 খুলুন। private বা incognito browsing-এ storage সাধারণত session শেষ হলে মুছে যেতে পারে। browser-এর policy, blocked storage বা নিরাপত্তা সেটিংসের কারণে SecurityError-ও হতে পারে; তাই read এবং write—দুই জায়গাতেই error handling রাখা হয়েছে।
localStorage database নয়। এতে password, access token, ব্যক্তিগত স্বাস্থ্যতথ্য বা অন্য sensitive data রাখা উচিত নয়, কারণ একই browser profile-এ চলা script বা ব্যবহারকারী-নিয়ন্ত্রিত code storage পড়তে পারে। এই app-এ শুধু সাধারণ task text রাখা হচ্ছে। বাস্তব authentication বা গোপন তথ্যের ক্ষেত্রে server-side নিরাপদ ব্যবস্থা, access control এবং উপযুক্ত encryption architecture প্রয়োজন।
নষ্ট বা অপ্রত্যাশিত storage সামলানো
শুধু JSON.parse() লিখে ফেললে malformed string এলে পুরো script error করতে পারে। আরও সমস্যা হলো valid JSON হলেও সেটি array নাও হতে পারে, অথবা array-এর item-এ সঠিক id, text কিংবা boolean completed নাও থাকতে পারে। তাই readTasks() প্রথমে storage থেকে value নেয়, parse করে, তারপর Array.isArray এবং item-level validator দিয়ে shape যাচাই করে। text trim করে empty item বাদ দেওয়া হয়; id না থাকলে সেই record-কে valid ধরা হয় না।
যদি parse ব্যর্থ হয় বা shape ভুল হয়, app silently overwrite করবে না। বরং একটি warning message দেখাবে এবং memory-তে খালি তালিকা ব্যবহার করবে। ব্যবহারকারী চাইলে warning-এর “সরান” বোতাম চাপতে পারবেন; তখন কেবল এই app-এর key মুছে ফেলা হবে। এই নকশা গুরুত্বপূর্ণ: corrupt data অজান্তে মুছে না দিয়ে আগে সমস্যা জানানো হচ্ছে। warning মুছে দিলে নতুন task সংরক্ষণ করা সম্ভব হবে।
Storage read/write-এ exception হলেও app পুরোপুরি অচল হবে না। storageAvailable flag-এর মাধ্যমে আমরা বুঝব persistence কাজ করছে কি না। storage unavailable হলে status message দেখানো হবে এবং task অন্তত বর্তমান পেজের memory-তে কাজ করবে। refresh-এর পরে তা না-ও থাকতে পারে—এটি ব্যবহারকারীকে স্পষ্টভাবে জানানো ভালো, মিথ্যা “saved” বার্তা দেখানো নয়।
unique ID কেন দরকার
শুধু array index দিয়ে delete করলে সমস্যা হয়। একটি item সরানোর পরে পরের item-এর index বদলে যায়; asynchronous event বা ভবিষ্যৎ sort যোগ করলে ভুল item মুছে যেতে পারে। তাই প্রতিটি item-এর স্থায়ী id ব্যবহার করছি। আধুনিক browser-এ crypto.randomUUID() সহজে unique UUID দেয়। localhost-এর পুরনো বা সীমিত পরিবেশে এটি না থাকলে fallback হিসেবে timestamp, random অংশ এবং counter মিলিয়ে একটি ID বানানো হয়েছে। এটি cryptographically secure পরিচয়ের বিকল্প নয়, কিন্তু এই local To-do app-এ সংঘর্ষের সম্ভাবনা কমানোর জন্য যথেষ্ট।
সম্পূর্ণ index.html
নিচের ফাইলটি index.html নামে সংরক্ষণ করুন। CSS একই ফাইলে আছে, ফলে internet বা external dependency লাগবে না। Form submit-এ preventDefault() ব্যবহার করা হয়েছে যাতে page reload না হয়। Checkbox-এর change event এবং delete button-এর click event দুটিই নির্দিষ্ট ID ধরে state পরিবর্তন করে।
<!doctype html>
<html lang="bn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>আমার To-do তালিকা</title>
<style>
:root { color-scheme: light; font-family: system-ui, sans-serif; }
body { margin: 0; background: #f3f6fb; color: #172033; }
main { width: min(680px, calc(100% - 2rem)); margin: 3rem auto; }
.panel { background: #fff; padding: 1.5rem; border-radius: 14px; box-shadow: 0 8px 30px #16213d18; }
form { display: flex; gap: .6rem; margin: 1rem 0; }
input[type="text"] { flex: 1; min-width: 0; padding: .75rem; border: 1px solid #aab5c8; border-radius: 8px; font: inherit; }
button { border: 0; border-radius: 8px; padding: .7rem 1rem; cursor: pointer; font: inherit; }
form button { background: #2457d6; color: white; }
ul { list-style: none; padding: 0; margin: 1rem 0 0; }
li { display: flex; align-items: center; gap: .7rem; padding: .75rem 0; border-bottom: 1px solid #e3e7ef; }
li span { flex: 1; overflow-wrap: anywhere; }
li.done span { text-decoration: line-through; color: #667085; }
.delete { background: #fee4e2; color: #a4231b; }
.message { min-height: 1.4rem; color: #475467; }
.warning { padding: .7rem; border-radius: 8px; background: #fff4cc; color: #684d00; }
.empty { color: #667085; text-align: center; }
@media (max-width: 480px) { form { flex-direction: column; } }
</style>
</head>
<body>
<main>
<section class="panel" aria-labelledby="title">
<h1 id="title">আমার To-do তালিকা</h1>
<p>কাজ যোগ করুন, শেষ হলে চিহ্ন দিন।</p>
<p id="status" class="message" role="status" aria-live="polite"></p>
<form id="task-form">
<label for="task-input">নতুন কাজ</label>
<input id="task-input" type="text" maxlength="200" required autocomplete="off" placeholder="যেমন: JavaScript অনুশীলন">
<button type="submit">যোগ করুন</button>
</form>
<div id="storage-warning" hidden></div>
<ul id="task-list" aria-label="কাজের তালিকা"></ul>
</section>
</main>
<script>
'use strict';
const STORAGE_KEY = 'bn-todo-tasks-v1';
const form = document.querySelector('#task-form');
const input = document.querySelector('#task-input');
const list = document.querySelector('#task-list');
const status = document.querySelector('#status');
const warning = document.querySelector('#storage-warning');
let tasks = [];
let storageAvailable = true;
let idCounter = 0;
function makeId() {
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === 'function') {
return globalThis.crypto.randomUUID();
}
idCounter += 1;
return `task-${Date.now()}-${idCounter}-${Math.random().toString(36).slice(2)}`;
}
function validTask(value) {
return value && typeof value === 'object' &&
typeof value.id === 'string' && value.id.length > 0 &&
typeof value.text === 'string' && value.text.trim().length > 0 &&
typeof value.completed === 'boolean' &&
typeof value.createdAt === 'string';
}
function showWarning(message, canClear = false) {
warning.textContent = '';
warning.hidden = false;
warning.className = 'warning';
const text = document.createElement('span');
text.textContent = message;
warning.append(text);
if (canClear) {
const clear = document.createElement('button');
clear.type = 'button';
clear.textContent = 'নষ্ট ডেটা সরান';
clear.addEventListener('click', () => {
try {
localStorage.removeItem(STORAGE_KEY);
warning.hidden = true;
status.textContent = 'পুরনো নষ্ট ডেটা সরানো হয়েছে।';
storageAvailable = true;
} catch (error) {
status.textContent = 'ডেটা সরানো যায়নি; browser storage অনুমতি পরীক্ষা করুন।';
}
});
warning.append(' ', clear);
}
}
function readTasks() {
let raw;
try {
raw = localStorage.getItem(STORAGE_KEY);
} catch (error) {
storageAvailable = false;
showWarning('Browser storage ব্যবহার করা যাচ্ছে না। কাজ refresh-এর পর নাও থাকতে পারে।');
return [];
}
if (raw === null) return [];
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) throw new Error('not an array');
const clean = parsed.filter(validTask).map(task => ({
id: task.id,
text: task.text.trim(),
completed: task.completed,
createdAt: task.createdAt
}));
if (clean.length !== parsed.length) {
showWarning('কিছু storage record-এর কাঠামো ঠিক নয়; সেগুলো দেখানো হয়নি।', true);
}
return clean;
} catch (error) {
showWarning('সংরক্ষিত To-do ডেটা নষ্ট বা অচেনা। এটি নিজে থেকে মুছে ফেলা হয়নি।', true);
return [];
}
}
function saveTasks() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
storageAvailable = true;
if (!warning.classList.contains('warning')) status.textContent = 'পরিবর্তন সংরক্ষিত হয়েছে।';
} catch (error) {
storageAvailable = false;
showWarning('কাজটি এই session-এ আছে, কিন্তু browser storage-এ সংরক্ষণ করা যায়নি।');
}
}
function render() {
list.textContent = '';
if (tasks.length === 0) {
const empty = document.createElement('li');
empty.className = 'empty';
empty.textContent = 'এখনও কোনও কাজ যোগ করা হয়নি।';
list.append(empty);
return;
}
tasks.forEach(task => {
const item = document.createElement('li');
if (task.completed) item.className = 'done';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = task.completed;
checkbox.setAttribute('aria-label', `${task.text} সম্পন্ন হয়েছে`);
checkbox.addEventListener('change', () => {
task.completed = checkbox.checked;
render();
saveTasks();
});
const text = document.createElement('span');
text.textContent = task.text;
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'delete';
remove.textContent = 'মুছুন';
remove.addEventListener('click', () => {
tasks = tasks.filter(candidate => candidate.id !== task.id);
render();
saveTasks();
});
item.append(checkbox, text, remove);
list.append(item);
});
}
form.addEventListener('submit', event => {
event.preventDefault();
const text = input.value.trim();
if (!text) {
status.textContent = 'একটি কাজ লিখে তারপর যোগ করুন।';
input.focus();
return;
}
tasks.push({ id: makeId(), text, completed: false, createdAt: new Date().toISOString() });
input.value = '';
render();
saveTasks();
input.focus();
});
tasks = readTasks();
render();
if (!storageAvailable) status.textContent = 'Persistence বন্ধ আছে; এই পেজে কাজ করা যাবে।';
</script>
</body>
</html>
কোডটি বোঝার গুরুত্বপূর্ণ অংশ
validTask() একটি ছোট schema validator। এটি সব সম্ভাব্য malicious data ঠেকানোর পূর্ণ security boundary নয়, কিন্তু application যেন array-এর বদলে object, সংখ্যা বা অপ্রত্যাশিত property নিয়ে render না করে—সে নিশ্চয়তা দেয়। map()-এ আমরা কেবল প্রয়োজনীয় চারটি property নিই। ফলে storage-এ অতিরিক্ত object থাকলেও UI-তে তা অন্ধভাবে ছড়িয়ে পড়ে না। বাস্তব প্রকল্পে schema version, maximum record count এবং text length-এর আরও সীমা যোগ করা যেতে পারে।
showWarning()-এ warning message বসানোর সময় textContent ব্যবহার করা হয়েছে। এমনকি storage থেকে আসা নষ্ট record-এর কোনও text সরাসরি HTML হিসেবে ব্যবহার করা হয়নি। বাটনটিও createElement দিয়ে তৈরি। এই pattern একটু বেশি দীর্ঘ মনে হলেও untrusted input-এর ক্ষেত্রে এটি নিরাপদ এবং পরিষ্কার। list.textContent = '' করে আগের child সরানোর পর প্রতিটি node নতুন করে তৈরি হওয়ায় event listener-ও পুরনো node-এ আটকে থাকে না।
saveTasks()-এর try/catch কেবল quota error-এর জন্য নয়; browser storage নিষিদ্ধ বা security policy-র কারণেও ব্যর্থ হতে পারে। save ব্যর্থ হলেও tasks memory-তে আপডেট থাকে, তাই ব্যবহারকারী বর্তমান page-এ কাজ চালাতে পারেন। তবে status-এ সীমাবদ্ধতা বলা হচ্ছে। কোনও error silently গিলে ফেলা হলে debugging এবং user trust—দুটিই নষ্ট হয়।
চালানো এবং পরীক্ষা
প্রথমে একটি folder-এ index.html রাখুন। তারপর সেই folder-এ terminal খুলে এই command চালান:
python3 -m http.server 8000
এবার browser-এ http://localhost:8000 খুলুন। একটি কাজ লিখে যোগ করুন, checkbox বদলান, delete করুন এবং refresh দিয়ে persistence পরীক্ষা করুন। Developer Tools-এর Application/Storage অংশে key-টির value JSON হিসেবে দেখতে পারবেন। ইচ্ছাকৃতভাবে value-টি not-json করে refresh দিলে warning দেখা উচিত এবং app কোনও data নিজে থেকে মুছে ফেলবে না। পরে warning-এর button চাপলে কেবল এই app-এর key মুছবে।
আরও কিছু পরীক্ষা করা যায়। এক origin-এ দুইটি tab খুলে দেখুন—এক tab-এর পরিবর্তন অন্য tab-এ স্বয়ংক্রিয়ভাবে render হবে না, কারণ এই ছোট app storage event শোনে না। বহু-tab synchronization দরকার হলে window.addEventListener('storage', ...) যোগ করা যায়। ২০০ অক্ষরের বেশি task input browser-এর maxlength-এ আটকে যাবে। ফাঁকা space submit করলে validation বার্তা দেখাবে। HTML-সদৃশ লেখা দিলে সেটি plain text হিসেবে দেখা উচিত।
উন্নত করার ধারণা
পরবর্তী ধাপে filter যোগ করতে পারেন: সব, চলমান এবং সম্পন্ন। তখন tasks.filter() দিয়ে render-এর আগে দৃশ্যমান তালিকা বানানো যায়। edit button যোগ করলে text আবার input-এ এনে validation করতে হবে। completed item-এর সংখ্যা, “সব সম্পন্ন মুছুন” action এবং keyboard accessibility-ও যোগ করা যায়। তবে প্রতিটি feature যোগের আগে state transition, ID এবং save timing পরিষ্কার রাখুন।
বড় ডেটার জন্য localStorage ধীর বা সীমিত হতে পারে; তখন IndexedDB উপযুক্ত বিকল্প। server-এ sync করতে গেলে conflict resolution, authentication, authorization এবং privacy policy দরকার হবে। localStorage-কে server database ভাবা যাবে না। বিশেষত অন্য ব্যবহারকারীর account data বা secret রাখার জন্য এটি ব্যবহার করবেন না।
ধাপে ধাপে data flow বুঝে নিন
ব্যবহারকারী যখন form submit করেন, প্রথমে browser-এর default navigation বন্ধ হয়। এরপর input-এর value থেকে দুই পাশের ফাঁকা জায়গা বাদ দেওয়া হয়। এই trim করার কারণ খুব বাস্তব: শুধু space লিখে task যোগ করলে সেটি তালিকায় অর্থহীন খালি item তৈরি করবে। maxlength="200" ব্যবহারকারীকে অসীম বড় string পাঠানো থেকেও রক্ষা করে, যদিও server-side app হলে server-এও একই সীমা যাচাই করতে হতো। client-side validation সুবিধার জন্য, নিরাপত্তার একমাত্র স্তর হিসেবে নয়।
Valid text পাওয়া গেলে tasks.push() একটি নতুন object যোগ করে। এখানে completed: false দিয়ে শুরু করা হয়েছে, কারণ সদ্য তৈরি কাজ শেষ হয়ে গেছে ধরে নেওয়া ঠিক নয়। createdAt ISO string-এ রাখা হয়েছে; এটি সংখ্যার চেয়ে পড়তে এবং অন্য system-এ পাঠাতে সহজ। পরে date অনুযায়ী তালিকা সাজাতে চাইলে new Date(task.createdAt) ব্যবহার করা যাবে। তবে sort যোগ করার সময় stable ID অপরিবর্তিত রাখতে হবে, যাতে checkbox বা delete action ভুল record-এ না যায়।
নতুন object যোগ হওয়ার পরে তিনটি ধাপ দেখা যায়: input.value = '' input পরিষ্কার করে, render() DOM-কে বর্তমান state-এর সঙ্গে মিলিয়ে নেয়, এবং saveTasks() পরবর্তী refresh-এর জন্য state লিখে রাখে। এই ক্রমটি বোঝা দরকার। যদি save করার আগে state বদলানো না হয়, পুরনো data-ই সংরক্ষিত হবে। আবার render না করলে state বদলালেও screen-এ ব্যবহারকারী তা দেখতে পাবেন না। ছোট app-এ এই তিনটি function পাশাপাশি থাকায় flow সহজে অনুসরণ করা যায়।
Checkbox-এর ক্ষেত্রে task object সরাসরি পাওয়া যায়, কারণ forEach-এর callback-এ সেই object-এর reference আছে। checkbox-এর নতুন checked value object-এর completed property-তে বসে। তারপর আবার render এবং save হয়। Render পুরনো list item সরিয়ে নতুন item তৈরি করলেও task object state-এ থাকে। এই separation-টি DOM এবং data-এর দায়িত্ব আলাদা রাখে: DOM হলো বর্তমান দৃশ্য, array হলো আসল state।
Delete action-এ filter() ব্যবহার করে একই ID নয় এমন item-গুলো নিয়ে নতুন array বানানো হয়েছে। এটি splice(index, 1)-এর চেয়ে এখানে বোধগম্য, কারণ index খুঁজে রাখার দরকার নেই। ID string থেকে item খোঁজা বা মুছে ফেলার সময় ব্যবহারকারীর task text-এর উপর নির্ভর করতে হয় না। একই text-এর দুটি কাজ থাকলেও প্রত্যেকটির UUID আলাদা থাকবে। ফলে “বাজার করা” নামের দুটি task থাকলে একটি মুছলে অন্যটি অক্ষত থাকবে।
accessibility ও ব্যবহারযোগ্যতার খুঁটিনাটি
শুধু functionality থাকলেই ভালো UI হয় না। input-এর সঙ্গে দৃশ্যমান label যুক্ত করা হয়েছে, যাতে screen reader এবং নতুন ব্যবহারকারী উভয়েই field-এর উদ্দেশ্য বুঝতে পারেন। list-এর checkbox-এ একটি aria-label আছে; task text দৃশ্যমান হলেও control-এর নাম স্পষ্ট থাকা সহায়ক। status-এর aria-live="polite" attribute দিয়ে সংরক্ষণ বা validation-এর বার্তা assistive technology-কে জানানো যায়, কিন্তু সেটি ব্যবহারকারীর কাজ বাধাগ্রস্ত করে না।
Delete button-এ পরিষ্কার বাংলা লেখা আছে, শুধু অচেনা icon নয়। keyboard দিয়ে tab করে form control-এ যাওয়া যায়, কারণ native button এবং input ব্যবহার করা হয়েছে। custom clickable div এড়িয়ে native element নেওয়া হয়েছে; এতে keyboard behavior, focus এবং semantics browser নিজেই ভালোভাবে দেয়। production app-এ focus ring যেন CSS দিয়ে অদৃশ্য না করা হয়, সেটিও খেয়াল করবেন। রঙের মাধ্যমে completed state দেখানোর পাশাপাশি text-decoration রাখা হয়েছে, যাতে রঙের পার্থক্য একমাত্র সংকেত না হয়।
Responsive CSS-এ ছোট screen হলে form column layout নেয়। দীর্ঘ task-এর জন্য overflow-wrap: anywhere আছে, তাই অস্বাভাবিক দীর্ঘ URL বা একটানা অক্ষর card-এর বাইরে বেরিয়ে যাবে না। একই কারণে user text-এর জন্য কোনও HTML string template বানানো হয়নি। নিরাপদ output এবং ব্যবহারযোগ্য layout এখানে একসঙ্গে কাজ করছে।
browser storage-এর সীমা এবং বাস্তব সিদ্ধান্ত
একটি origin-এর storage অন্য origin-এর storage থেকে আলাদা। http://localhost:8000 এবং অন্য port-এ চলা localhost-কে একই ধরে নেওয়া যাবে না; port origin-এর অংশ। তাই server বন্ধ করে ভিন্ন port-এ আবার চালালে পুরনো task না দেখা স্বাভাবিক। একইভাবে https deployment-এ upload করার পরে local http-এর data সেখানে চলে যাবে না। এই আচরণ bug নয়, browser-এর isolation model।
Private browsing-এ page বন্ধ করার পর data মুছে যেতে পারে। browser storage পূর্ণ হয়ে গেলেও setItem() ব্যর্থ হতে পারে। আবার ব্যবহারকারী browser settings-এ site data block করলে get বা remove operation-ও exception দিতে পারে। এই tutorial-এ exception ধরা হয়েছে, কিন্তু সব browser policy এক রকম নয়। তাই UI-তে “স্থায়ীভাবে সংরক্ষিত” না লিখে “browser storage-এ সংরক্ষিত” বলা অধিক সঠিক। ব্যবহারকারীকে নিয়মিত export বা backup সুবিধা দিতে চাইলে JSON download যোগ করা যায়।
Storage-তে data রাখার আগে quota-র কথাও মনে রাখা দরকার। হাজার হাজার বড় note-এর জন্য localStorage উপযুক্ত নয়, কারণ এটি synchronous এবং string-based। JSON.stringify() চলার সময় main thread আটকে যেতে পারে। ছোট task list-এ সমস্যা সাধারণত চোখে পড়বে না, কিন্তু বড় offline application-এ IndexedDB বা Cache API বিবেচনা করা উচিত। Server sync দরকার হলে API design, retry, conflict এবং offline queue-ও আলাদা করে পরিকল্পনা করতে হবে।
Corrupt data warning-এর নকশাটিও একটি UX সিদ্ধান্ত। আমরা parse error-এ raw string দেখাইনি, কারণ তার মধ্যে HTML-এর মতো লেখা থাকতে পারে; warning-এ স্থির সাধারণ বার্তা দেখিয়েছি। আমরা storage সঙ্গে সঙ্গে clear করিনি, কারণ সেই data-তে ব্যবহারকারীর মূল্যবান task থাকতে পারে। “নষ্ট ডেটা সরান” বোতামটি explicit consent নেয়। shape আংশিক ভুল হলে valid item দেখানো হচ্ছে, কিন্তু warning রেখে দেওয়া হচ্ছে—এতে ব্যবহারকারী বুঝতে পারেন যে সব record ফিরে আসেনি।
নিজের কোড উন্নত করার checklist
প্রথমে function-গুলোর দায়িত্ব আলাদা রাখুন: ID বানানো, shape যাচাই, read, save, render এবং event handling একসঙ্গে বিশাল function-এ লিখবেন না। দ্বিতীয়ত, প্রত্যেক user-controlled string কোথা থেকে আসছে তা চিহ্নিত করুন এবং DOM-এ বসানোর সময় textContent বা নিরাপদ property ব্যবহার করুন। তৃতীয়ত, state পরিবর্তনের পর সবসময় persistence দরকার কি না ঠিক করুন; temporary filter state storage-এ না রাখলেও চলে। চতুর্থত, error হলে user-কে সংক্ষিপ্ত কিন্তু সত্য বার্তা দিন।
পরীক্ষার জন্য happy path-এর পাশাপাশি failure path লিখে ফেলুন: empty submit, শুধু whitespace, একই text, অনেক লম্বা text, corrupt JSON, valid JSON কিন্তু object, ভুল type-এর property, storage unavailable এবং storage quota পূর্ণ। browser refresh, নতুন tab এবং private window-এ আলাদা ফলাফলও নোট করুন। এই checklist অনুসরণ করলে tutorial-এর demo ধীরে ধীরে একটি বেশি নির্ভরযোগ্য ছোট application-এ পরিণত হবে।
উপসংহার
এই ছোট প্রকল্পে আমরা একটি বাস্তবসম্মত frontend workflow দেখলাম: state model করা, DOM node নিরাপদে তৈরি করা, event দিয়ে state বদলানো, JSON হিসেবে persistence করা এবং অনির্ভরযোগ্য storage-কে যাচাই করা। textContent, schema validation, explicit warning, unique ID এবং storage error handling—এই অভ্যাসগুলি app বড় হওয়ার আগেই গড়ে তুললে পরের প্রকল্পে অনেক নিরাপদ code লেখা যায়। এখন index.html চালিয়ে নিজে feature যোগ করুন, কিন্তু প্রতিটি পরিবর্তনের পর data shape এবং failure case পরীক্ষা করতে ভুলবেন না।