// app/notes/NotesList.tsx // Client Component. Owns the notes UI state (filter, add-form, edit, // optimistic CRUD). All server calls go through /api/notes/* JSON routes. "use client"; import { useMemo, useState } from "react"; import type { YsterbaardNote } from "@/lib/types"; type Filter = "all" | "open" | "done"; interface Props { initialNotes: YsterbaardNote[]; } export default function NotesList({ initialNotes }: Props) { const [notes, setNotes] = useState(initialNotes); const [filter, setFilter] = useState("all"); const [adding, setAdding] = useState(false); const [editingId, setEditingId] = useState(null); const [error, setError] = useState(null); // Sort: open first (created desc), then done (done_at desc). const sorted = useMemo(() => { const open = notes .filter((n) => !n.is_done) .slice() .sort((a, b) => (a.created < b.created ? 1 : -1)); const done = notes .filter((n) => n.is_done) .slice() .sort((a, b) => { const ad = a.done_at ?? a.updated ?? a.created; const bd = b.done_at ?? b.updated ?? b.created; return ad < bd ? 1 : -1; }); return [...open, ...done]; }, [notes]); const filtered = useMemo(() => { if (filter === "open") return sorted.filter((n) => !n.is_done); if (filter === "done") return sorted.filter((n) => n.is_done); return sorted; }, [sorted, filter]); function refresh(updated: YsterbaardNote) { setNotes((prev) => prev.map((n) => (n.id === updated.id ? updated : n))); } async function toggleDone(n: YsterbaardNote) { const nextDone = !n.is_done; // Optimistic setNotes((prev) => prev.map((x) => x.id === n.id ? { ...x, is_done: nextDone, done_at: nextDone ? new Date().toISOString() : null } : x, ), ); try { const res = await fetch(`/api/notes/${encodeURIComponent(n.id)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ is_done: nextDone }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const updated = (await res.json()) as YsterbaardNote; refresh(updated); } catch (e) { setError(`Could not update note: ${e instanceof Error ? e.message : "unknown"}`); // Rollback setNotes((prev) => prev.map((x) => x.id === n.id ? { ...x, is_done: !nextDone, done_at: !nextDone ? new Date().toISOString() : null } : x, ), ); } } async function deleteNote(n: YsterbaardNote) { if (!confirm(`Delete "${n.title}"?`)) return; const snapshot = notes; setNotes((prev) => prev.filter((x) => x.id !== n.id)); try { const res = await fetch(`/api/notes/${encodeURIComponent(n.id)}`, { method: "DELETE" }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } catch (e) { setError(`Could not delete note: ${e instanceof Error ? e.message : "unknown"}`); setNotes(snapshot); } } async function createNote(payload: { title: string; content: string }) { const res = await fetch("/api/notes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const created = (await res.json()) as YsterbaardNote; setNotes((prev) => [created, ...prev]); } async function updateNote(id: string, payload: { title?: string; content?: string }) { const res = await fetch(`/api/notes/${encodeURIComponent(id)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const updated = (await res.json()) as YsterbaardNote; refresh(updated); } return (
{error && (
{error}
)}
{(["all", "open", "done"] as Filter[]).map((f) => ( ))}
{!adding && ( )}
{adding && ( setAdding(false)} onSubmit={async (payload) => { try { await createNote(payload); setAdding(false); } catch (e) { setError(`Could not create note: ${e instanceof Error ? e.message : "unknown"}`); } }} /> )} {filtered.length === 0 ? ( ) : (
    {filtered.map((n) => editingId === n.id ? ( setEditingId(null)} onSave={async (payload) => { try { await updateNote(n.id, payload); setEditingId(null); } catch (e) { setError(`Could not save note: ${e instanceof Error ? e.message : "unknown"}`); } }} /> ) : ( toggleDone(n)} onEdit={() => setEditingId(n.id)} onDelete={() => deleteNote(n)} /> ), )}
)}
); } function AddForm({ onCancel, onSubmit, }: { onCancel: () => void; onSubmit: (payload: { title: string; content: string }) => Promise; }) { const [title, setTitle] = useState(""); const [content, setContent] = useState(""); const [busy, setBusy] = useState(false); return (
setTitle(e.target.value)} className="w-full rounded-md border border-border bg-surface-warm px-3 py-2 text-sm font-semibold text-fg placeholder:text-muted" data-od-id="notes-add-title" />