snapshot
This commit is contained in:
@@ -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,10 +138,13 @@ 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.length > 0 ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 sm:gap-2.5 min-[1800px]:hidden">
|
||||
{snippets.map(s => (
|
||||
<SnippetCard
|
||||
key={s.id}
|
||||
@@ -143,6 +156,34 @@ export function NotesApp() {
|
||||
/>
|
||||
))}
|
||||
</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
@@ -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;
|
||||
|
||||
@@ -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