snapshot
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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<SortValue>("updated_desc");
|
||||
const [snippets, setSnippets] = useState<Snippet[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [tagTreeOpen, setTagTreeOpen] = useState(false);
|
||||
const [tagTreeNodes, setTagTreeNodes] = useState<TagTreeNode[]>([]);
|
||||
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<string, string | null> = {};
|
||||
for (const n of tagTreeNodes) {
|
||||
m[n.name] = n.color;
|
||||
}
|
||||
return m;
|
||||
}, [tagTreeNodes]);
|
||||
|
||||
const treeDisplayByName = useMemo(() => {
|
||||
const m: Record<string, string> = {};
|
||||
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 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 (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-4 sm:px-5">
|
||||
<NotesToolbar
|
||||
query={query}
|
||||
onQueryChange={setQuery}
|
||||
sort={sort}
|
||||
onSortChange={setSort}
|
||||
tagTreeCount={tagTreeNodes.length}
|
||||
onOpenTagTree={() => setTagTreeOpen(true)}
|
||||
onCreateNew={() => void createNew()}
|
||||
/>
|
||||
|
||||
<TagTreeDialog
|
||||
open={tagTreeOpen}
|
||||
onOpenChange={setTagTreeOpen}
|
||||
onChanged={() => {
|
||||
void fetchTagTree();
|
||||
setSnippetListVersion(v => v + 1);
|
||||
}}
|
||||
/>
|
||||
|
||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||
{loading ? <p className="text-sm text-muted-foreground">Lade Snippets …</p> : null}
|
||||
|
||||
<div className="flex flex-col gap-2 sm:gap-2.5">
|
||||
{!loading && snippets.length === 0 ? (
|
||||
<p className="text-center text-sm text-muted-foreground">Keine Snippets. Mit „Neu“ starten.</p>
|
||||
) : null}
|
||||
{snippets.map(s => (
|
||||
<SnippetCard
|
||||
key={s.id}
|
||||
snippet={s}
|
||||
treeColorByName={treeColorByName}
|
||||
treeDisplayByName={treeDisplayByName}
|
||||
onReplace={replaceSnippet}
|
||||
onRemove={removeSnippet}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user