// --- 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
`;
$("#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();