Capstone DOM Project — Notes SPA CAPSTONE

পুরো একটি single-page Notes app — vanilla JS-এ

~75 min Advanced 5 extension tasks Real project

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

  1. Push the three files to a GitHub repo
  2. Settings → Pages → main / root → Save
  3. Live URL appears in ~60 seconds
  4. Or drop the folder onto netlify.com (drag & drop deploy)

8. Glossary (শব্দকোষ)

TermMeaningবাংলায়
SPASingle-Page App — one HTML, JS swaps the views.এক HTML; JS-ই view পাল্টায়।
StateOne source of truth for the app's data.App-এর single source-of-truth।
RenderFunction that turns state into DOM.State থেকে DOM তৈরি।
hashchangeBrowser event when location.hash changes.location.hash পরিবর্তনের event।
RouterCode that maps a URL to a view.URL থেকে view বেছে নেওয়ার logic।
PersistenceSaving state to localStorage so reloads don't lose it.Reload-এ data হারাতে না দেওয়া।
AccessibilityMaking the app usable with keyboard, screen reader, and assistive tech.Keyboard / screen reader-এর জন্য usable।
aria-liveAnnounces dynamic updates to assistive tech.Dynamic update assistive tech-এ পৌঁছায়।
CapstoneThe 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

  1. 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();
    }
  2. 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 into state.notes, persist, and re-render.

  3. Add a search input that filters by substring.
    ✨ Show Answer
    a3.js
    const all = [
        "buy chal", "call ammu", "javascript class", "meeting"
    ];
    const q = "call".toLowerCase();
    console.log(all.filter(s => s.toLowerCase().includes(q)));
  4. 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")));
  5. 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 শেখা সহজ মনে হবে।

Next Module → Error Handling & Custom Errors।