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 formatting

FAQ

Common questions about this project.