/** * — notebook and note management. * * Browse notebooks, create/edit notes with rich text, * search, tag management. */ interface Notebook { id: string; title: string; description: string; cover_color: string; note_count: string; updated_at: string; } interface Note { id: string; title: string; content: string; content_plain: string; type: string; tags: string[] | null; is_pinned: boolean; created_at: string; updated_at: string; } class FolkNotesApp extends HTMLElement { private shadow: ShadowRoot; private space = ""; private view: "notebooks" | "notebook" | "note" = "notebooks"; private notebooks: Notebook[] = []; private selectedNotebook: (Notebook & { notes: Note[] }) | null = null; private selectedNote: Note | null = null; private searchQuery = ""; private searchResults: Note[] = []; private loading = false; private error = ""; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); } connectedCallback() { this.space = this.getAttribute("space") || "demo"; this.loadNotebooks(); } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^\/([^/]+)\/notes/); return match ? `/${match[1]}/notes` : ""; } private async loadNotebooks() { this.loading = true; this.render(); try { const base = this.getApiBase(); const res = await fetch(`${base}/api/notebooks`); const data = await res.json(); this.notebooks = data.notebooks || []; } catch { this.error = "Failed to load notebooks"; } this.loading = false; this.render(); } private async loadNotebook(id: string) { this.loading = true; this.render(); try { const base = this.getApiBase(); const res = await fetch(`${base}/api/notebooks/${id}`); this.selectedNotebook = await res.json(); } catch { this.error = "Failed to load notebook"; } this.loading = false; this.render(); } private async loadNote(id: string) { this.loading = true; this.render(); try { const base = this.getApiBase(); const res = await fetch(`${base}/api/notes/${id}`); this.selectedNote = await res.json(); } catch { this.error = "Failed to load note"; } this.loading = false; this.render(); } private async searchNotes(query: string) { if (!query.trim()) { this.searchResults = []; this.render(); return; } try { const base = this.getApiBase(); const res = await fetch(`${base}/api/notes?q=${encodeURIComponent(query)}`); const data = await res.json(); this.searchResults = data.notes || []; } catch { this.searchResults = []; } this.render(); } private async createNotebook() { const title = prompt("Notebook name:"); if (!title?.trim()) return; try { const base = this.getApiBase(); await fetch(`${base}/api/notebooks`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); await this.loadNotebooks(); } catch { this.error = "Failed to create notebook"; this.render(); } } private getNoteIcon(type: string): string { switch (type) { case "NOTE": return "\u{1F4DD}"; case "CODE": return "\u{1F4BB}"; case "BOOKMARK": return "\u{1F517}"; case "IMAGE": return "\u{1F5BC}"; case "AUDIO": return "\u{1F3A4}"; case "FILE": return "\u{1F4CE}"; case "CLIP": return "\u2702\uFE0F"; default: return "\u{1F4C4}"; } } private formatDate(dateStr: string): string { const d = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffDays === 0) return "Today"; if (diffDays === 1) return "Yesterday"; if (diffDays < 7) return `${diffDays}d ago`; return d.toLocaleDateString(); } private render() { this.shadow.innerHTML = ` ${this.error ? `
${this.esc(this.error)}
` : ""} ${this.loading ? '
Loading...
' : ""} ${!this.loading ? this.renderView() : ""} `; this.attachListeners(); } private renderView(): string { if (this.view === "note" && this.selectedNote) return this.renderNote(); if (this.view === "notebook" && this.selectedNotebook) return this.renderNotebook(); return this.renderNotebooks(); } private renderNotebooks(): string { return `
Notebooks
${this.searchQuery && this.searchResults.length > 0 ? `
${this.searchResults.length} results for "${this.esc(this.searchQuery)}"
${this.searchResults.map((n) => this.renderNoteItem(n)).join("")} ` : ""} ${!this.searchQuery ? `
${this.notebooks.map((nb) => `
${this.esc(nb.title)}
${this.esc(nb.description || "")}
${nb.note_count} notes · ${this.formatDate(nb.updated_at)}
`).join("")}
${this.notebooks.length === 0 ? '
No notebooks yet. Create one to get started.
' : ""} ` : ""} `; } private renderNotebook(): string { const nb = this.selectedNotebook!; return `
${this.esc(nb.title)}
${nb.notes && nb.notes.length > 0 ? nb.notes.map((n) => this.renderNoteItem(n)).join("") : '
No notes in this notebook.
' } `; } private renderNoteItem(n: Note): string { return `
${this.getNoteIcon(n.type)}
${n.is_pinned ? '📌 ' : ""}${this.esc(n.title)}
${this.esc(n.content_plain || "")}
${this.formatDate(n.updated_at)} ${n.type} ${n.tags ? n.tags.map((t) => `${this.esc(t)}`).join("") : ""}
`; } private renderNote(): string { const n = this.selectedNote!; return `
${this.getNoteIcon(n.type)} ${this.esc(n.title)}
${n.content || 'Empty note'}
Type: ${n.type} Created: ${this.formatDate(n.created_at)} Updated: ${this.formatDate(n.updated_at)} ${n.tags ? n.tags.map((t) => `${this.esc(t)}`).join("") : ""}
`; } private attachListeners() { // Create notebook this.shadow.getElementById("create-notebook")?.addEventListener("click", () => this.createNotebook()); // Search const searchInput = this.shadow.getElementById("search-input") as HTMLInputElement; let searchTimeout: any; searchInput?.addEventListener("input", () => { clearTimeout(searchTimeout); this.searchQuery = searchInput.value; searchTimeout = setTimeout(() => this.searchNotes(this.searchQuery), 300); }); // Notebook cards this.shadow.querySelectorAll("[data-notebook]").forEach((el) => { el.addEventListener("click", () => { const id = (el as HTMLElement).dataset.notebook!; this.view = "notebook"; this.loadNotebook(id); }); }); // Note items this.shadow.querySelectorAll("[data-note]").forEach((el) => { el.addEventListener("click", () => { const id = (el as HTMLElement).dataset.note!; this.view = "note"; this.loadNote(id); }); }); // Back buttons this.shadow.querySelectorAll("[data-back]").forEach((el) => { el.addEventListener("click", (e) => { e.stopPropagation(); const target = (el as HTMLElement).dataset.back; if (target === "notebooks") { this.view = "notebooks"; this.render(); } else if (target === "notebook") { this.view = "notebook"; this.render(); } }); }); } private esc(s: string): string { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; } } customElements.define("folk-notes-app", FolkNotesApp);