Loading...
Loading...
A full-featured notes app. Rich editing, search, categories, and automatic saving.
Module 11 · JavaScript Projects
Theory becomes skill through building. These guided projects — calculator, todo app, weather app, and more — combine everything you've learned into real applications you can extend, share, and put in a portfolio.
Learn Notes App in JavaScript — covering Data Structure, Note Editor, Search & Filter, Auto-Save with interactive visual examples and step-by-step explanations.
Reading about JavaScript is not the same as building with it. These guided projects combine variables, functions, DOM manipulation, async fetch, and state management into applications you can run, extend, and showcase.
Each project walks through architecture decisions, common pitfalls, and incremental steps. By the end you will have portfolio-ready apps and the confidence to start your own projects from scratch.
Understanding notes app is essential for every JavaScript developer. It shows up in frontend UI code, backend APIs, and framework internals — and getting it wrong leads to bugs that are hard to trace. This chapter builds intuition with visuals so the behavior sticks.
Notes with titles, content, timestamps, and categories.
// Note data model: // { // id: "uuid-string", // title: "Meeting Notes", // content: "# Decisions\n- Ship by Friday...", // category: "work", // pinned: false, // createdAt: 1704067200000, // updatedAt: 1704153600000, // } // App state: let notes = []; let activeNoteId = null; let searchQuery = ""; let activeCategory = "all"; // Categories: const CATEGORIES = [ { id: "all", label: "All Notes", icon: "📋" }, { id: "personal", label: "Personal", icon: "👤" }, { id: "work", label: "Work", icon: "💼" }, { id: "ideas", label: "Ideas", icon: "💡" }, { id: "archive", label: "Archive", icon: "📦" }, ]; // Create a new note: function createNote(category = "personal") { const note = { id: crypto.randomUUID(), title: "", content: "", category, pinned: false, createdAt: Date.now(), updatedAt: Date.now(), }; notes.unshift(note); // newest first activeNoteId = note.id; return note; } // Get active note: function getActiveNote() { return notes.find(n => n.id === activeNoteId) || null; }
Split view — note list on the left, editor on the right.
// Layout: sidebar | note list | editor // HTML structure: // <div class="notes-app"> // <aside class="sidebar">categories</aside> // <div class="note-list">list of notes</div> // <div class="editor"> // <input class="title-input" placeholder="Note title"> // <textarea class="content-input" placeholder="Start writing..."> // <div class="note-meta">Last edited: ...</div> // </div> // </div> // Render the note list: function renderNoteList() { const filtered = getFilteredNotes(); const listEl = document.querySelector(".note-list"); if (filtered.length === 0) { listEl.innerHTML = '<p class="empty">No notes yet</p>'; return; } // Pinned notes first, then sorted by updatedAt: const sorted = [...filtered].sort((a, b) => { if (a.pinned !== b.pinned) return b.pinned - a.pinned; return b.updatedAt - a.updatedAt; }); listEl.innerHTML = sorted.map(note => ` <div class="note-item ${note.id === activeNoteId ? "active" : ""}" data-id="${note.id}"> ${note.pinned ? '<span class="pin">📌</span>' : ""} <h3>${escapeHtml(note.title) || "Untitled"}</h3> <p class="preview">${getPreview(note.content)}</p> <time>${formatRelativeTime(note.updatedAt)}</time> </div> `).join(""); } // Preview text (first line, max 80 chars): function getPreview(content) { const first = content.split("\n").find(l => l.trim()) || ""; const plain = first.replace(/[#*_~`]/g, ""); // strip markdown return escapeHtml(plain.slice(0, 80)); } // Relative time: "2 hours ago", "yesterday": function formatRelativeTime(timestamp) { const diff = Date.now() - timestamp; const minutes = Math.floor(diff / 60000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 7) return `${days}d ago`; return new Date(timestamp).toLocaleDateString(); }
Instant search across titles and content.
// Filter notes by category and search query: function getFilteredNotes() { return notes.filter(note => { // Category filter: if (activeCategory !== "all" && note.category !== activeCategory) { return false; } // Search filter: if (searchQuery) { const query = searchQuery.toLowerCase(); return ( note.title.toLowerCase().includes(query) || note.content.toLowerCase().includes(query) ); } return true; }); } // Search input with debounce: const searchInput = document.querySelector(".search-input"); searchInput.addEventListener("input", debounce((e) => { searchQuery = e.target.value.trim(); renderNoteList(); }, 200)); // Category sidebar clicks: document.querySelector(".sidebar").addEventListener("click", (e) => { const btn = e.target.closest("[data-category]"); if (!btn) return; activeCategory = btn.dataset.category; renderSidebar(); renderNoteList(); }); // Highlight search matches in results: function highlightMatch(text, query) { if (!query) return escapeHtml(text); const escaped = escapeHtml(text); const regex = new RegExp( `(${escapeRegex(query)})`, "gi" ); return escaped.replace(regex, '<mark>$1</mark>'); } function escapeRegex(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
Save automatically as you type — no save button needed.
// Auto-save on input with debounce: const titleInput = document.querySelector(".title-input"); const contentInput = document.querySelector(".content-input"); const autoSave = debounce(() => { const note = getActiveNote(); if (!note) return; note.title = titleInput.value; note.content = contentInput.value; note.updatedAt = Date.now(); renderNoteList(); // update preview in list saveToStorage(); showSaveIndicator(); }, 500); titleInput.addEventListener("input", autoSave); contentInput.addEventListener("input", autoSave); // Visual save indicator: function showSaveIndicator() { const indicator = document.querySelector(".save-status"); indicator.textContent = "Saved"; indicator.classList.add("visible"); setTimeout(() => indicator.classList.remove("visible"), 1500); } // localStorage persistence: const STORAGE_KEY = "notes-app-v1"; function saveToStorage() { const data = { notes, activeNoteId, activeCategory }; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } function loadFromStorage() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = JSON.parse(raw); if (Array.isArray(data.notes)) notes = data.notes; if (data.activeNoteId) activeNoteId = data.activeNoteId; if (data.activeCategory) activeCategory = data.activeCategory; } catch { notes = []; } } // Switch active note: document.querySelector(".note-list").addEventListener("click", (e) => { const item = e.target.closest(".note-item"); if (!item) return; activeNoteId = item.dataset.id; renderNoteList(); renderEditor(); }); function renderEditor() { const note = getActiveNote(); if (!note) { titleInput.value = ""; contentInput.value = ""; return; } titleInput.value = note.title; contentInput.value = note.content; titleInput.focus(); }
Pin, delete, export, and keyboard shortcuts.
// Delete with confirmation: function deleteNote(id) { if (!confirm("Delete this note?")) return; notes = notes.filter(n => n.id !== id); if (activeNoteId === id) { activeNoteId = notes[0]?.id || null; } renderNoteList(); renderEditor(); saveToStorage(); } // Toggle pin: function togglePin(id) { const note = notes.find(n => n.id === id); if (note) note.pinned = !note.pinned; renderNoteList(); saveToStorage(); } // Export as Markdown file: function exportNote(id) { const note = notes.find(n => n.id === id); if (!note) return; const filename = (note.title || "untitled") + ".md"; const blob = new Blob([note.content], { type: "text/markdown" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } // Keyboard shortcuts: document.addEventListener("keydown", (e) => { // Ctrl/Cmd + N → new note if ((e.ctrlKey || e.metaKey) && e.key === "n") { e.preventDefault(); createNote(activeCategory === "all" ? "personal" : activeCategory); renderNoteList(); renderEditor(); } // Ctrl/Cmd + F → focus search if ((e.ctrlKey || e.metaKey) && e.key === "f") { e.preventDefault(); searchInput.focus(); } // Ctrl/Cmd + D → delete active note if ((e.ctrlKey || e.metaKey) && e.key === "d") { e.preventDefault(); if (activeNoteId) deleteNote(activeNoteId); } }); // Key patterns in this project: // ✓ Multi-panel UI (sidebar + list + editor) // ✓ Auto-save with debounce // ✓ Full-text search with highlighting // ✓ Category filtering // ✓ Keyboard shortcuts // ✓ File export with Blob API // ✓ Relative time formatting
Common questions about notes app.