snapshot
This commit is contained in:
@@ -85,6 +85,16 @@ export function NotesApp() {
|
|||||||
setSnippets(prev => prev.filter(x => x.id !== id));
|
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 () => {
|
const createNew = async () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
@@ -105,7 +115,7 @@ export function NotesApp() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<NotesToolbar
|
||||||
query={query}
|
query={query}
|
||||||
onQueryChange={setQuery}
|
onQueryChange={setQuery}
|
||||||
@@ -128,10 +138,13 @@ export function NotesApp() {
|
|||||||
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
{error ? <p className="text-sm text-destructive">{error}</p> : null}
|
||||||
{loading ? <p className="text-sm text-muted-foreground">Lade Snippets …</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 ? (
|
{!loading && snippets.length === 0 ? (
|
||||||
<p className="text-center text-sm text-muted-foreground">Keine Snippets. Mit „Neu“ starten.</p>
|
<p className="text-center text-sm text-muted-foreground">Keine Snippets. Mit „Neu“ starten.</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{snippets.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2 sm:gap-2.5 min-[1800px]:hidden">
|
||||||
{snippets.map(s => (
|
{snippets.map(s => (
|
||||||
<SnippetCard
|
<SnippetCard
|
||||||
key={s.id}
|
key={s.id}
|
||||||
@@ -143,6 +156,34 @@ export function NotesApp() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-48
@@ -2,55 +2,9 @@ import { Database } from "bun:sqlite";
|
|||||||
import { mkdirSync } from "fs";
|
import { mkdirSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
let dbInstance: Database | null = null;
|
import { migrate } from "./db/migrate";
|
||||||
|
|
||||||
function migrate(database: Database) {
|
let dbInstance: Database | null = null;
|
||||||
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);`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDb(): Database {
|
export function getDb(): Database {
|
||||||
if (dbInstance) return dbInstance;
|
if (dbInstance) return dbInstance;
|
||||||
|
|||||||
@@ -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);`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user