Capstone DOM Project — Notes SPA CAPSTONE
পুরো একটি single-page Notes app — vanilla JS-এ
1. Brief
A Notes SPA: create, edit, delete notes; filter by All/Active/Archived via URL hash; persist to localStorage; semantic HTML with accessibility.
এই capstone-এ আপনি single-page app build করবেন: state object + render() pattern, hashchange-based router, localStorage persistence এবং accessibility — সবই vanilla JS-এ।
2. Project Layout
notes-spa/
├─ index.html
├─ style.css
├─ app.js
└─ README.md
3. index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Notes</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<h1>📝 Notes</h1>
<nav>
<a href="#/all">All</a>
<a href="#/active">Active</a>
<a href="#/archived">Archived</a>
</nav>
</header>
<main>
<form id="form" autocomplete="off">
<input id="text" required minlength="1" placeholder="Write a note…">
<button>Add</button>
</form>
<ul id="list" aria-live="polite"></ul>
<p id="empty" hidden>No notes yet.</p>
</main>
<script src="app.js" defer></script>
</body>
</html>
4. style.css (compact)
* { box-sizing: border-box; }
body { font-family: system-ui, "Hind Siliguri", sans-serif; max-width: 600px; margin: auto; padding: 1rem; background: #f8fafc; }
header { display: flex; justify-content: space-between; align-items: center; }
nav a { margin-left: 0.5rem; color: #2563eb; text-decoration: none; }
nav a.active { font-weight: bold; text-decoration: underline; }
form { display: flex; gap: 0.5rem; margin: 1rem 0; }
input { flex: 1; padding: 0.6rem; border: 1px solid #cbd5e1; border-radius: 6px; }
button { padding: 0.6rem 1rem; background: #39b549; color: white; border: none; border-radius: 6px; cursor: pointer; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem; background: white; border-radius: 6px; margin-bottom: 0.4rem; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
li.archived { opacity: 0.5; text-decoration: line-through; }
li button { background: transparent; color: #64748b; padding: 0.2rem 0.5rem; }
5. app.js — full code
// ---------- state ----------
const KEY = "notes-spa-v1";
const state = {
notes: load(), // [{id, text, archived}]
filter: "all", // all | active | archived
};
function load() {
try { return JSON.parse(localStorage.getItem(KEY)) || []; }
catch { return []; }
}
function persist() {
localStorage.setItem(KEY, JSON.stringify(state.notes));
}
// ---------- elements ----------
const $ = (id) => document.getElementById(id);
const list = $("list"), empty = $("empty"), text = $("text");
// ---------- render ----------
function render() {
const filtered = state.notes.filter(n =>
state.filter === "all" ? true :
state.filter === "active" ? !n.archived : n.archived);
list.replaceChildren(...filtered.map(noteEl));
empty.hidden = filtered.length > 0;
document.querySelectorAll("nav a").forEach(a =>
a.classList.toggle("active",
a.getAttribute("href") === `#/${state.filter}`));
}
function noteEl(n) {
const li = document.createElement("li");
li.dataset.id = n.id;
li.classList.toggle("archived", n.archived);
const span = document.createElement("span");
span.textContent = n.text;
span.style.flex = "1";
const archive = document.createElement("button");
archive.textContent = n.archived ? "↩" : "📁";
archive.title = n.archived ? "Unarchive" : "Archive";
const del = document.createElement("button");
del.textContent = "✕";
del.title = "Delete";
li.append(span, archive, del);
return li;
}
// ---------- events ----------
$("form").addEventListener("submit", e => {
e.preventDefault();
const t = text.value.trim();
if (!t) return;
state.notes.unshift({ id: Date.now(), text: t, archived: false });
persist(); text.value = ""; render();
});
list.addEventListener("click", e => {
const li = e.target.closest("li");
if (!li) return;
const id = +li.dataset.id;
const note = state.notes.find(n => n.id === id);
if (e.target.title === "Delete") {
state.notes = state.notes.filter(n => n.id !== id);
} else if (e.target.title === "Archive" || e.target.title === "Unarchive") {
note.archived = !note.archived;
}
persist(); render();
});
// ---------- router ----------
function readHash() {
const m = location.hash.match(/^#\/(all|active|archived)$/);
state.filter = m ? m[1] : "all";
render();
}
addEventListener("hashchange", readHash);
readHash();
6. Sandbox-Safe Logic Test
logic.js
const notes = [
{ id: 1, text: "buy chal", archived: false },
{ id: 2, text: "call ammu", archived: true },
{ id: 3, text: "finish ABCL course", archived: false },
];
const filterBy = (arr, mode) => arr.filter(n =>
mode === "all" ? true :
mode === "active" ? !n.archived : n.archived);
console.log("all :", filterBy(notes, "all").length);
console.log("active :", filterBy(notes, "active").length);
console.log("archived:", filterBy(notes, "archived").length);
7. Deploy
- Push the three files to a GitHub repo
- Settings → Pages → main / root → Save
- Live URL appears in ~60 seconds
- Or drop the folder onto netlify.com (drag & drop deploy)
8. Glossary (শব্দকোষ)
| Term | Meaning | বাংলায় |
|---|---|---|
| SPA | Single-Page App — one HTML, JS swaps the views. | এক HTML; JS-ই view পাল্টায়। |
| State | One source of truth for the app's data. | App-এর single source-of-truth। |
| Render | Function that turns state into DOM. | State থেকে DOM তৈরি। |
hashchange | Browser event when location.hash changes. | location.hash পরিবর্তনের event। |
| Router | Code that maps a URL to a view. | URL থেকে view বেছে নেওয়ার logic। |
| Persistence | Saving state to localStorage so reloads don't lose it. | Reload-এ data হারাতে না দেওয়া। |
| Accessibility | Making the app usable with keyboard, screen reader, and assistive tech. | Keyboard / screen reader-এর জন্য usable। |
aria-live | Announces dynamic updates to assistive tech. | Dynamic update assistive tech-এ পৌঁছায়। |
| Capstone | The integrating final project of a learning phase. | একটি phase-এর integrating final project। |
সারাংশ: এই capstone-এ আপনি একটি বাস্তব SPA build করেছেন — single state, render() pattern, hashchange router, localStorage persistence এবং accessibility। React-এর mental model এর প্রায় ১০০% মিল — তাই এরপর React শেখা সহজ মনে হবে। deploy করে CV-তে link দিন।
9. Extension Tasks
- Add an "Export to JSON" button that downloads the notes.
✨ Show Answer
function download() { const blob = new Blob([JSON.stringify(state.notes, null, 2)], { type: "application/json" }); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "notes.json"; a.click(); } - Add inline editing — click the text to edit it.
✨ Show Answer
On
dblclick, replace the span with an<input>; on blur or Enter, save the new text intostate.notes, persist, and re-render. - Add a search input that filters by substring.
✨ Show Answer
a3.jsconst all = [ "buy chal", "call ammu", "javascript class", "meeting" ]; const q = "call".toLowerCase(); console.log(all.filter(s => s.toLowerCase().includes(q))); - Add a dark-mode toggle that persists in localStorage.
✨ Show Answer
const setDark = on => { document.body.classList.toggle("dark", on); localStorage.setItem("dark", on ? "1" : "0"); }; setDark(localStorage.getItem("dark") === "1"); $("toggle").addEventListener("click", () => setDark(!document.body.classList.contains("dark"))); - Deploy to Netlify or GitHub Pages and share the URL.
✨ Show Answer
Steps: push files, enable Pages, share. There's no answer to copy here — only the live link to brag about. That's the assignment.
Summary — Module 30
You built a real, deployed SPA with vanilla JS — single source-of-truth state, render pattern, hashchange routing, localStorage persistence, and accessible markup. After this you will find React's mental model already familiar.
এই capstone-এ state, render, router এবং persistence — সব মিলিয়ে আধুনিক frontend-এর foundation। এরপর React শেখা সহজ মনে হবে।