// --- API endpoints (relies on window.api from common.js) --- const ideasApi = { list: () => api.req("GET", "/api/ideas"), create: (title, description) => api.req("POST", "/api/ideas", { title, description }), remove: (id) => api.req("DELETE", `/api/ideas/${id}`), setVisibility: (id, visibility) => api.req("PUT", `/api/ideas/${id}/visibility`, { visibility }), enableShare: (id) => api.req("POST", `/api/ideas/${id}/share`), disableShare: (id) => api.req("DELETE", `/api/ideas/${id}/share`), 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}`), }; let me = null; let ideas = []; let selectedId = null; // --- Auth bootstrap --- async function init() { $("#idea-form").addEventListener("submit", onCreateIdea); try { me = await api.req("GET", "/api/me"); showApp(); await refresh(); } catch (e) { if (e.status === 401) showLogin(); else toast(e.message, true); } } async function showLogin() { $("#app-main").hidden = true; $("#login-gate").hidden = false; const params = new URLSearchParams(location.search); if (params.get("auth_error")) { const el = $("#login-error"); el.hidden = false; el.textContent = params.get("auth_error") === "state" ? "Sign-in session expired or was tampered with. Please try again." : "Could not complete sign-in. Please try again."; } try { const { providers } = await api.req("GET", "/api/auth/providers"); const box = $("#provider-buttons"); if (!providers.length) { $("#login-note").hidden = false; return; } box.innerHTML = providers.map(providerButton).join(""); box.querySelectorAll(".provider-btn").forEach((btn) => { btn.addEventListener("click", () => { location.href = `/auth/${btn.dataset.provider}`; }); }); } catch (e) { toast(e.message, true); } } function showApp() { $("#login-gate").hidden = true; $("#app-main").hidden = false; renderUserChip(); } function renderUserChip() { const nav = $("#nav-right"); // Remove any existing chip first. nav.querySelector(".user-chip")?.remove(); const avatar = me.avatar_url ? `` : `${escapeHtml( (me.name || "?")[0].toUpperCase() )}`; const chip = document.createElement("div"); chip.className = "user-chip"; chip.innerHTML = `${avatar}${escapeHtml( me.name )}βŽ‹`; chip.querySelector(".logout").addEventListener("click", () => { location.href = "/auth/logout"; }); nav.appendChild(chip); } // --- Rendering: idea list --- function renderList() { const list = $("#ideas-list"); $("#idea-count").textContent = ideas.length; if (!ideas.length) { 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" } ${ idea.visibility === "public" ? "🌐 public" : "πŸ”’ private" } updated ${relativeTime(idea.updated_at)}
` ) .join(""); list .querySelectorAll(".idea-item") .forEach((el) => el.addEventListener("click", () => selectIdea(Number(el.dataset.id))) ); } // --- Rendering: detail + privacy + notes --- 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; } const shareUrl = idea.share_token ? `${location.origin}/share/${idea.share_token}` : ""; panel.innerHTML = `

${escapeHtml(idea.title)}

${ idea.description ? `

${escapeHtml(idea.description)}

` : "" }
Created ${fullTime(idea.created_at)}
${ idea.visibility === "public" ? "🌐 Public" : "πŸ”’ Private" }

πŸ”— Shareable read-only link

${ idea.share_token ? `` : `` }
${ idea.share_token ? `
Anyone with this link can view the idea and its notes (read-only) β€” no account needed.
` : `
Generate a secret link to share this idea read-only, even while it stays private.
` }

Notes

Loading…
`; $("#del-idea").addEventListener("click", () => onDeleteIdea(idea.id)); $("#vis-toggle").addEventListener("change", (e) => onToggleVisibility(idea.id, e.target.checked) ); $("#share-enable")?.addEventListener("click", () => onEnableShare(idea.id)); $("#share-disable")?.addEventListener("click", () => onDisableShare(idea.id)); $("#share-copy")?.addEventListener("click", async () => { await navigator.clipboard.writeText(shareUrl).catch(() => {}); toast("Link copied to clipboard πŸ”—"); }); 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); } // --- Actions --- async function refresh() { try { ideas = await ideasApi.list(); renderList(); renderDetail(); } catch (e) { toast(e.message, true); } } async function refreshListOnly() { ideas = await ideasApi.list(); renderList(); } function selectIdea(id) { selectedId = id; renderList(); renderDetail(); } async function loadNotes(id) { try { const notes = await ideasApi.notes(id); if (selectedId === id) renderNotesInto($("#notes-container"), notes, onDeleteNote); } 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 ideasApi.create(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 ideasApi.remove(id); if (selectedId === id) selectedId = null; await refresh(); toast("Idea deleted"); } catch (err) { toast(err.message, true); } } async function onToggleVisibility(id, makePublic) { try { const updated = await ideasApi.setVisibility(id, makePublic ? "public" : "private"); mergeIdea(updated); renderList(); renderDetail(); toast(makePublic ? "Now public on Explore 🌐" : "Set to private πŸ”’"); } catch (err) { toast(err.message, true); renderDetail(); } } async function onEnableShare(id) { try { const updated = await ideasApi.enableShare(id); mergeIdea(updated); renderDetail(); toast("Share link created πŸ”—"); } catch (err) { toast(err.message, true); } } async function onDisableShare(id) { try { const updated = await ideasApi.disableShare(id); mergeIdea(updated); renderDetail(); toast("Share link disabled"); } catch (err) { toast(err.message, true); } } async function onAddNote(id) { const input = $("#note-body"); const body = input.value.trim(); if (!body) return; try { await ideasApi.addNote(id, body); input.value = ""; await Promise.all([loadNotes(id), refreshListOnly()]); } catch (err) { toast(err.message, true); } } async function onDeleteNote(noteId) { try { await ideasApi.deleteNote(noteId); if (selectedId) await Promise.all([loadNotes(selectedId), refreshListOnly()]); toast("Note deleted"); } catch (err) { toast(err.message, true); } } // Replace an idea in local state with a fresh copy (preserving note_count). function mergeIdea(updated) { const idx = ideas.findIndex((i) => i.id === updated.id); if (idx !== -1) ideas[idx] = { ...ideas[idx], ...updated }; } init();