Notes App
A full-featured notes app. Rich editing, search, categories, and automatic saving.
Data Structure
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;
}Note Editor
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();
}Search & Filter
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, "\\$&");
}Auto-Save
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();
}Advanced Features
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 formattingFAQ
Common questions about this project.