// --- Tiny API client --- const api = { async req(method, path, body) { const res = await fetch(path, { method, headers: body ? { "Content-Type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined, }); const data = res.status === 204 ? null : await res.json().catch(() => null); if (!res.ok) throw new Error((data && data.error) || "Request failed"); return data; }, ideas: () => api.req("GET", "/api/ideas"), createIdea: (title, description) => api.req("POST", "/api/ideas", { title, description }), deleteIdea: (id) => api.req("DELETE", `/api/ideas/${id}`), notes: (id) => api.req("GET", `/api/ideas/${id}/notes`), addNote: (id, body) => api.req("POST", `/api/ideas/${id}/notes`, { body }), deleteNote: (id) => api.req("DELETE", `/api/notes/${id}`), }; // --- State --- let ideas = []; let selectedId = null; // --- Helpers --- const $ = (sel) => document.querySelector(sel); function escapeHtml(s) { return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } // SQLite stores UTC datetimes as "YYYY-MM-DD HH:MM:SS"; parse as UTC. function parseUTC(s) { return new Date(s.replace(" ", "T") + "Z"); } function relativeTime(s) { const then = parseUTC(s); const diff = (Date.now() - then.getTime()) / 1000; if (diff < 45) return "just now"; if (diff < 90) return "a minute ago"; if (diff < 3600) return `${Math.round(diff / 60)} min ago`; if (diff < 7200) return "an hour ago"; if (diff < 86400) return `${Math.round(diff / 3600)} hours ago`; if (diff < 172800) return "yesterday"; if (diff < 604800) return `${Math.round(diff / 86400)} days ago`; return then.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric", }); } function fullTime(s) { return parseUTC(s).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short", }); } let toastTimer; function toast(msg, isError = false) { const el = $("#toast"); el.textContent = msg; el.classList.toggle("error", isError); el.classList.add("show"); clearTimeout(toastTimer); toastTimer = setTimeout(() => el.classList.remove("show"), 2600); } // --- Rendering --- function renderList() { const list = $("#ideas-list"); $("#idea-count").textContent = ideas.length; if (ideas.length === 0) { list.innerHTML = '
No ideas yet. Add your first one!
'; return; } list.innerHTML = ideas .map( (idea) => `

${escapeHtml(idea.title)}

${idea.note_count} note${idea.note_count === 1 ? "" : "s"} updated ${relativeTime(idea.updated_at)}
` ) .join(""); list.querySelectorAll(".idea-item").forEach((el) => { el.addEventListener("click", () => selectIdea(Number(el.dataset.id))); }); } async function renderDetail() { const panel = $("#detail-panel"); const idea = ideas.find((i) => i.id === selectedId); if (!idea) { panel.innerHTML = '
👈Select an idea to see its notes, or create one to get started.
'; return; } panel.innerHTML = `

${escapeHtml(idea.title)}

${idea.description ? `

${escapeHtml(idea.description)}

` : ""}
Created ${fullTime(idea.created_at)}

Notes

Loading…
`; $("#del-idea").addEventListener("click", () => onDeleteIdea(idea.id)); const form = $("#note-form"); form.addEventListener("submit", (e) => { e.preventDefault(); onAddNote(idea.id); }); $("#note-body").addEventListener("keydown", (e) => { if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); onAddNote(idea.id); } }); loadNotes(idea.id); } function renderNotes(notes) { const container = $("#notes-container"); if (!container) return; if (notes.length === 0) { container.innerHTML = '
No notes yet — add the first one below.
'; return; } container.innerHTML = notes .map( (n) => `
📝
${escapeHtml(n.body)}
${relativeTime(n.created_at)} delete
` ) .join(""); container.querySelectorAll(".del").forEach((el) => { el.addEventListener("click", () => onDeleteNote(Number(el.dataset.note))); }); } // --- Actions --- async function refresh() { try { ideas = await api.ideas(); renderList(); renderDetail(); } catch (e) { toast(e.message, true); } } function selectIdea(id) { selectedId = id; renderList(); renderDetail(); } async function loadNotes(id) { try { const notes = await api.notes(id); if (selectedId === id) renderNotes(notes); } catch (e) { toast(e.message, true); } } async function onCreateIdea(e) { e.preventDefault(); const title = $("#idea-title").value.trim(); const description = $("#idea-desc").value.trim(); if (!title) return; try { const idea = await api.createIdea(title, description); $("#idea-form").reset(); await refresh(); selectIdea(idea.id); toast("Idea added ✨"); } catch (err) { toast(err.message, true); } } async function onDeleteIdea(id) { if (!confirm("Delete this idea and all its notes?")) return; try { await api.deleteIdea(id); if (selectedId === id) selectedId = null; await refresh(); toast("Idea deleted"); } catch (err) { toast(err.message, true); } } async function onAddNote(id) { const input = $("#note-body"); const body = input.value.trim(); if (!body) return; try { await api.addNote(id, body); input.value = ""; // Refresh notes immediately, plus list for the updated count/order. await Promise.all([loadNotes(id), refreshListOnly()]); } catch (err) { toast(err.message, true); } } async function refreshListOnly() { ideas = await api.ideas(); renderList(); } async function onDeleteNote(noteId) { try { await api.deleteNote(noteId); if (selectedId) await Promise.all([loadNotes(selectedId), refreshListOnly()]); toast("Note deleted"); } catch (err) { toast(err.message, true); } } // --- Init --- $("#idea-form").addEventListener("submit", onCreateIdea); refresh();