import { useCallback, useEffect, useMemo, useState } from "react"; import { formatTreeTagDisplay } from "@/lib/treeTagDisplay"; import type { Snippet } from "@/types/snippet"; import type { TagTreeNode } from "@/types/tagTree"; import { SnippetCard } from "@/features/snippets/SnippetCard"; import { TagTreeDialog } from "@/features/tag-tree/TagTreeDialog"; import { NotesToolbar } from "@/features/notes/NotesToolbar"; import type { SortValue } from "@/features/notes/sortOptions"; export function NotesApp() { const [query, setQuery] = useState(""); const [debouncedQuery, setDebouncedQuery] = useState(""); const [sort, setSort] = useState("updated_desc"); const [snippets, setSnippets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [tagTreeOpen, setTagTreeOpen] = useState(false); const [tagTreeNodes, setTagTreeNodes] = useState([]); const [snippetListVersion, setSnippetListVersion] = useState(0); useEffect(() => { const t = window.setTimeout(() => setDebouncedQuery(query), 300); return () => window.clearTimeout(t); }, [query]); useEffect(() => { let cancelled = false; void (async () => { setLoading(true); setError(null); try { const params = new URLSearchParams(); if (debouncedQuery.trim()) params.set("q", debouncedQuery.trim()); params.set("sort", sort); const res = await fetch(`/api/snippets?${params.toString()}`); if (!res.ok) throw new Error(await res.text()); const data = (await res.json()) as Snippet[]; if (!cancelled) setSnippets(data); } catch (e) { if (!cancelled) setError(e instanceof Error ? e.message : "Laden fehlgeschlagen"); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [debouncedQuery, sort, snippetListVersion]); const fetchTagTree = useCallback(async () => { try { const res = await fetch("/api/tag-tree"); if (!res.ok) return; setTagTreeNodes((await res.json()) as TagTreeNode[]); } catch { /* ignore */ } }, []); useEffect(() => { void fetchTagTree(); }, [fetchTagTree]); const treeColorByName = useMemo(() => { const m: Record = {}; for (const n of tagTreeNodes) { m[n.name] = n.color; } return m; }, [tagTreeNodes]); const treeDisplayByName = useMemo(() => { const m: Record = {}; for (const n of tagTreeNodes) { m[n.name] = formatTreeTagDisplay(n.name, n.label_prefix ?? null); } return m; }, [tagTreeNodes]); const replaceSnippet = useCallback((s: Snippet) => { setSnippets(prev => prev.map(x => (x.id === s.id ? s : x))); }, []); const removeSnippet = useCallback((id: number) => { setSnippets(prev => prev.filter(x => x.id !== id)); }, []); const { snippetColumnLeft, snippetColumnRight } = useMemo(() => { const left: Snippet[] = []; const right: Snippet[] = []; snippets.forEach((s, i) => { if (i % 2 === 0) left.push(s); else right.push(s); }); return { snippetColumnLeft: left, snippetColumnRight: right }; }, [snippets]); const createNew = async () => { setError(null); try { const res = await fetch("/api/snippets", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: null, body: "", tags: [] }), }); if (!res.ok) { const err = (await res.json().catch(() => null)) as { error?: string } | null; throw new Error(err?.error ?? res.statusText); } const created = (await res.json()) as Snippet; setSnippets(prev => [created, ...prev]); } catch (e) { setError(e instanceof Error ? e.message : "Neues Snippet fehlgeschlagen"); } }; return (
setTagTreeOpen(true)} onCreateNew={() => void createNew()} /> { void fetchTagTree(); setSnippetListVersion(v => v + 1); }} /> {error ?

{error}

: null} {loading ?

Lade Snippets …

: null} {!loading && snippets.length === 0 ? (

Keine Snippets. Mit „Neu“ starten.

) : null} {snippets.length > 0 ? ( <>
{snippets.map(s => ( ))}
{snippetColumnLeft.map(s => ( ))}
{snippetColumnRight.map(s => ( ))}
) : null}
); }