This commit is contained in:
Stefan ATZLESBERGER
2026-04-16 16:44:06 +02:00
parent e5f77debc2
commit 24b37821dc
3 changed files with 134 additions and 64 deletions
+57 -16
View File
@@ -85,6 +85,16 @@ export function NotesApp() {
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 {
@@ -105,7 +115,7 @@ export function NotesApp() {
};
return (
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-4 sm:px-5">
<div className="mx-auto flex w-full max-w-6xl flex-col gap-4 px-4 py-4 sm:px-5 xl:max-w-7xl min-[1800px]:max-w-none min-[1800px]:px-6 min-[2200px]:px-10">
<NotesToolbar
query={query}
onQueryChange={setQuery}
@@ -128,21 +138,52 @@ export function NotesApp() {
{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>
{!loading && snippets.length === 0 ? (
<p className="text-center text-sm text-muted-foreground">Keine Snippets. Mit „Neu“ starten.</p>
) : null}
{snippets.length > 0 ? (
<>
<div className="flex flex-col gap-2 sm:gap-2.5 min-[1800px]:hidden">
{snippets.map(s => (
<SnippetCard
key={s.id}
snippet={s}
treeColorByName={treeColorByName}
treeDisplayByName={treeDisplayByName}
onReplace={replaceSnippet}
onRemove={removeSnippet}
/>
))}
</div>
<div className="hidden min-[1800px]:flex min-[1800px]:flex-row min-[1800px]:items-start min-[1800px]:gap-x-8">
<div className="flex min-w-0 flex-1 flex-col gap-4">
{snippetColumnLeft.map(s => (
<SnippetCard
key={s.id}
snippet={s}
treeColorByName={treeColorByName}
treeDisplayByName={treeDisplayByName}
onReplace={replaceSnippet}
onRemove={removeSnippet}
/>
))}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-4">
{snippetColumnRight.map(s => (
<SnippetCard
key={s.id}
snippet={s}
treeColorByName={treeColorByName}
treeDisplayByName={treeDisplayByName}
onReplace={replaceSnippet}
onRemove={removeSnippet}
/>
))}
</div>
</div>
</>
) : null}
</div>
);
}
+2 -48
View File
@@ -2,55 +2,9 @@ import { Database } from "bun:sqlite";
import { mkdirSync } from "fs";
import path from "path";
let dbInstance: Database | null = null;
import { migrate } from "./db/migrate";
function migrate(database: Database) {
database.run(`
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
body TEXT NOT NULL DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
`);
database.run(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE
);
`);
database.run(`
CREATE TABLE IF NOT EXISTS snippet_tags (
snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (snippet_id, tag_id)
);
`);
database.run(`
CREATE TABLE IF NOT EXISTS tag_tree_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
parent_id INTEGER REFERENCES tag_tree_nodes(id) ON DELETE CASCADE,
color TEXT,
label_prefix TEXT
);
`);
try {
database.run(`ALTER TABLE tag_tree_nodes ADD COLUMN label_prefix TEXT`);
} catch {
/* Spalte existiert bereits */
}
database.run(`CREATE INDEX IF NOT EXISTS idx_tag_tree_parent ON tag_tree_nodes(parent_id);`);
database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_updated ON snippets(updated_at);`);
try {
database.run(`ALTER TABLE snippets ADD COLUMN deadline_at TEXT`);
} catch {
/* Spalte existiert bereits */
}
database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_deadline ON snippets(deadline_at);`);
}
let dbInstance: Database | null = null;
export function getDb(): Database {
if (dbInstance) return dbInstance;
+75
View File
@@ -0,0 +1,75 @@
import type { Database } from "bun:sqlite";
/**
* Einmaliges Kernschema — so, als würde eine leere DB zum ersten Mal angelegt.
* `CREATE TABLE IF NOT EXISTS` ändert bestehende Tabellen nicht; fehlende Spalten
* werden danach idempotent per `ensureColumn` nachgezogen (bestehende Zeilen bleiben).
*/
function assertSqlIdent(name: string, label: string): void {
if (!/^[a-z][a-z0-9_]*$/i.test(name)) {
throw new Error(`${label} ist kein gültiger SQL-Identifier: ${name}`);
}
}
function tableHasColumn(database: Database, table: string, column: string): boolean {
assertSqlIdent(table, "Tabellenname");
assertSqlIdent(column, "Spaltenname");
const cols = database.query(`PRAGMA table_info(${table})`).all() as { name: string }[];
return cols.some(c => c.name === column);
}
/** Fügt eine Spalte nur hinzu, wenn sie noch fehlt (keine Exceptions als Steuerung). */
function ensureColumn(database: Database, table: string, column: string, sqlType: string): void {
if (tableHasColumn(database, table, column)) return;
assertSqlIdent(table, "Tabellenname");
assertSqlIdent(column, "Spaltenname");
database.run(`ALTER TABLE ${table} ADD COLUMN ${column} ${sqlType}`);
}
export function migrate(database: Database): void {
database.run(`
CREATE TABLE IF NOT EXISTS snippets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
body TEXT NOT NULL DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
deadline_at TEXT
);
`);
database.run(`
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE
);
`);
database.run(`
CREATE TABLE IF NOT EXISTS snippet_tags (
snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE,
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (snippet_id, tag_id)
);
`);
database.run(`
CREATE TABLE IF NOT EXISTS tag_tree_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE COLLATE NOCASE,
parent_id INTEGER REFERENCES tag_tree_nodes(id) ON DELETE CASCADE,
color TEXT,
label_prefix TEXT
);
`);
/* Bestehende Installationen: Tabellen schon da, einzelne Spalten fehlen → ALTER nur bei Bedarf */
// ensureColumn(database, "snippets", "deadline_at", "TEXT");
// ensureColumn(database, "tag_tree_nodes", "label_prefix", "TEXT");
database.run(`CREATE INDEX IF NOT EXISTS idx_tag_tree_parent ON tag_tree_nodes(parent_id);`);
database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_updated ON snippets(updated_at);`);
database.run(`CREATE INDEX IF NOT EXISTS idx_snippets_deadline ON snippets(deadline_at);`);
}