diff --git a/src/features/notes/NotesApp.tsx b/src/features/notes/NotesApp.tsx
index 3f24a21..2b3db8b 100644
--- a/src/features/notes/NotesApp.tsx
+++ b/src/features/notes/NotesApp.tsx
@@ -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 (
-
+
{error} : null}
{loading ? Lade Snippets …
: null}
-
- {!loading && snippets.length === 0 ? (
-
Keine Snippets. Mit „Neu“ starten.
- ) : null}
- {snippets.map(s => (
-
- ))}
-
+ {!loading && snippets.length === 0 ? (
+ Keine Snippets. Mit „Neu“ starten.
+ ) : null}
+
+ {snippets.length > 0 ? (
+ <>
+
+ {snippets.map(s => (
+
+ ))}
+
+
+
+ {snippetColumnLeft.map(s => (
+
+ ))}
+
+
+ {snippetColumnRight.map(s => (
+
+ ))}
+
+
+ >
+ ) : null}
);
}
diff --git a/src/server/db.ts b/src/server/db.ts
index 31a7a3f..085e841 100644
--- a/src/server/db.ts
+++ b/src/server/db.ts
@@ -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;
diff --git a/src/server/db/migrate.ts b/src/server/db/migrate.ts
new file mode 100644
index 0000000..65f6638
--- /dev/null
+++ b/src/server/db/migrate.ts
@@ -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);`);
+}