diff --git a/migrations/0004_notes.sql b/migrations/0004_notes.sql
new file mode 100644
index 0000000..1ec30e8
--- /dev/null
+++ b/migrations/0004_notes.sql
@@ -0,0 +1,21 @@
+-- Sidelinja: Notater/Scratchpad
+-- Avhenger av: 0001_initial_schema.sql (nodes, workspaces)
+
+BEGIN;
+
+CREATE TABLE notes (
+ id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE,
+ parent_id UUID NOT NULL REFERENCES nodes(id),
+ title TEXT NOT NULL DEFAULT '',
+ content TEXT NOT NULL DEFAULT '',
+ created_by TEXT REFERENCES users(authentik_id),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
+);
+
+CREATE TRIGGER trg_notes_updated_at BEFORE UPDATE ON notes
+ FOR EACH ROW EXECUTE FUNCTION set_updated_at();
+
+COMMIT;
+
+ALTER TYPE node_type ADD VALUE IF NOT EXISTS 'note';
diff --git a/migrations/seed_dev.sql b/migrations/seed_dev.sql
index b84dfde..b2e217c 100644
--- a/migrations/seed_dev.sql
+++ b/migrations/seed_dev.sql
@@ -66,6 +66,12 @@ INSERT INTO nodes (id, workspace_id, node_type) VALUES
INSERT INTO calendars (id, parent_id, name, color) VALUES
('b0000000-0000-0000-0000-000000000030', 'b0000000-0000-0000-0000-000000000010', 'Foreningskalender', '#f59e0b');
+-- Notat for Liberalistene
+INSERT INTO nodes (id, workspace_id, node_type) VALUES
+ ('b0000000-0000-0000-0000-000000000040', 'b0000000-0000-0000-0000-000000000001', 'note');
+INSERT INTO notes (id, parent_id, title, content) VALUES
+ ('b0000000-0000-0000-0000-000000000040', 'b0000000-0000-0000-0000-000000000010', 'Møtenotater', '');
+
-- Kanban-brett for Liberalistene
INSERT INTO nodes (id, workspace_id, node_type) VALUES
('b0000000-0000-0000-0000-000000000020', 'b0000000-0000-0000-0000-000000000001', 'kanban_board');
@@ -95,9 +101,10 @@ UPDATE workspaces SET settings = jsonb_set(
"slug": "kalender",
"title": "Kalender",
"icon": "📅",
- "layout": "single",
+ "layout": "2-1",
"blocks": [
- {"id": "cal-lib-1", "type": "calendar", "title": "Foreningskalender", "props": {"calendarId": "b0000000-0000-0000-0000-000000000030"}}
+ {"id": "cal-lib-1", "type": "calendar", "title": "Foreningskalender", "props": {"calendarId": "b0000000-0000-0000-0000-000000000030"}},
+ {"id": "notes-lib-1", "type": "notes", "title": "Møtenotater", "props": {"noteId": "b0000000-0000-0000-0000-000000000040"}}
]
},
{
@@ -122,6 +129,12 @@ INSERT INTO nodes (id, workspace_id, node_type) VALUES
INSERT INTO calendars (id, parent_id, name, color) VALUES
('a0000000-0000-0000-0000-000000000030', 'a0000000-0000-0000-0000-000000000010', 'Redaksjonskalender', '#3b82f6');
+-- Notat for Sidelinja
+INSERT INTO nodes (id, workspace_id, node_type) VALUES
+ ('a0000000-0000-0000-0000-000000000040', 'a0000000-0000-0000-0000-000000000001', 'note');
+INSERT INTO notes (id, parent_id, title, content) VALUES
+ ('a0000000-0000-0000-0000-000000000040', 'a0000000-0000-0000-0000-000000000010', 'Show notes', '');
+
-- Kanban-brett for redaksjonen
INSERT INTO nodes (id, workspace_id, node_type) VALUES
('a0000000-0000-0000-0000-000000000020', 'a0000000-0000-0000-0000-000000000001', 'kanban_board');
@@ -152,9 +165,10 @@ UPDATE workspaces SET settings = jsonb_set(
"slug": "kalender",
"title": "Kalender",
"icon": "📅",
- "layout": "single",
+ "layout": "2-1",
"blocks": [
- {"id": "cal-1", "type": "calendar", "title": "Redaksjonskalender", "props": {"calendarId": "a0000000-0000-0000-0000-000000000030"}}
+ {"id": "cal-1", "type": "calendar", "title": "Redaksjonskalender", "props": {"calendarId": "a0000000-0000-0000-0000-000000000030"}},
+ {"id": "notes-1", "type": "notes", "title": "Show notes", "props": {"noteId": "a0000000-0000-0000-0000-000000000040"}}
]
},
{
diff --git a/web/src/lib/blocks/NotesBlock.svelte b/web/src/lib/blocks/NotesBlock.svelte
index 008efa1..be31e98 100644
--- a/web/src/lib/blocks/NotesBlock.svelte
+++ b/web/src/lib/blocks/NotesBlock.svelte
@@ -1,25 +1,164 @@
-
-
📝
-
Notes
-
Kommer snart
-
+{#if !noteId}
+ Ingen notat konfigurert for denne blokken.
+{:else if conn?.loading}
+
+{:else}
+
+
+
+
+
+
+ {#if conn?.error}
+ {conn.error}
+ {/if}
+{/if}
diff --git a/web/src/lib/notes/create.svelte.ts b/web/src/lib/notes/create.svelte.ts
new file mode 100644
index 0000000..3d08c99
--- /dev/null
+++ b/web/src/lib/notes/create.svelte.ts
@@ -0,0 +1,6 @@
+import type { NoteConnection } from './types';
+import { createPgNote } from './pg.svelte';
+
+export function createNote(noteId: string): NoteConnection {
+ return createPgNote(noteId);
+}
diff --git a/web/src/lib/notes/index.ts b/web/src/lib/notes/index.ts
new file mode 100644
index 0000000..7890a83
--- /dev/null
+++ b/web/src/lib/notes/index.ts
@@ -0,0 +1,2 @@
+export type { Note, NoteConnection } from './types';
+export { createNote } from './create.svelte';
diff --git a/web/src/lib/notes/pg.svelte.ts b/web/src/lib/notes/pg.svelte.ts
new file mode 100644
index 0000000..2448850
--- /dev/null
+++ b/web/src/lib/notes/pg.svelte.ts
@@ -0,0 +1,73 @@
+import type { Note, NoteConnection } from './types';
+
+export function createPgNote(noteId: string): NoteConnection {
+ let _note = $state(null);
+ let _error = $state('');
+ let _loading = $state(true);
+ let _saving = $state(false);
+ let _saveTimeout: ReturnType | null = null;
+ let _interval: ReturnType | null = null;
+
+ async function fetchNote() {
+ try {
+ const res = await fetch(`/api/notes/${noteId}`);
+ if (!res.ok) {
+ _error = `Feil: ${res.status}`;
+ return;
+ }
+ const data = await res.json();
+ // Ikke overskriv lokale endringer mens bruker skriver
+ if (!_saving) {
+ _note = data;
+ }
+ _error = '';
+ } catch (e) {
+ _error = e instanceof Error ? e.message : 'Ukjent feil';
+ } finally {
+ _loading = false;
+ }
+ }
+
+ fetchNote();
+ _interval = setInterval(fetchNote, 10000); // Sjeldnere polling for notater
+
+ return {
+ get note() { return _note; },
+ get error() { return _error; },
+ get loading() { return _loading; },
+ get saving() { return _saving; },
+
+ async save(updates) {
+ _saving = true;
+
+ // Debounce: vent 500ms etter siste endring
+ if (_saveTimeout) clearTimeout(_saveTimeout);
+
+ _saveTimeout = setTimeout(async () => {
+ try {
+ const res = await fetch(`/api/notes/${noteId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(updates)
+ });
+ if (!res.ok) {
+ _error = `Lagring feilet: ${res.status}`;
+ } else {
+ const data = await res.json();
+ _note = data;
+ _error = '';
+ }
+ } catch (e) {
+ _error = e instanceof Error ? e.message : 'Lagring feilet';
+ } finally {
+ _saving = false;
+ }
+ }, 500);
+ },
+
+ destroy() {
+ if (_interval) clearInterval(_interval);
+ if (_saveTimeout) clearTimeout(_saveTimeout);
+ }
+ };
+}
diff --git a/web/src/lib/notes/types.ts b/web/src/lib/notes/types.ts
new file mode 100644
index 0000000..9b82998
--- /dev/null
+++ b/web/src/lib/notes/types.ts
@@ -0,0 +1,15 @@
+export interface Note {
+ id: string;
+ title: string;
+ content: string;
+ updated_at: string;
+}
+
+export interface NoteConnection {
+ readonly note: Note | null;
+ readonly error: string;
+ readonly loading: boolean;
+ readonly saving: boolean;
+ save(updates: { title?: string; content?: string }): Promise;
+ destroy(): void;
+}
diff --git a/web/src/routes/api/notes/[noteId]/+server.ts b/web/src/routes/api/notes/[noteId]/+server.ts
new file mode 100644
index 0000000..f4e21c9
--- /dev/null
+++ b/web/src/routes/api/notes/[noteId]/+server.ts
@@ -0,0 +1,42 @@
+import { json, error } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { sql } from '$lib/server/db';
+
+/** GET /api/notes/:noteId — Hent notat */
+export const GET: RequestHandler = async ({ params, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const [note] = await sql`
+ SELECT n.id, n.title, n.content, n.updated_at
+ FROM notes n
+ JOIN nodes nd ON nd.id = n.id
+ WHERE n.id = ${params.noteId} AND nd.workspace_id = ${locals.workspace.id}
+ `;
+ if (!note) error(404, 'Notat ikke funnet');
+
+ return json(note);
+};
+
+/** PATCH /api/notes/:noteId — Oppdater notat */
+export const PATCH: RequestHandler = async ({ params, request, locals }) => {
+ if (!locals.workspace || !locals.user) error(401);
+
+ const updates = await request.json();
+
+ const [note] = await sql`
+ SELECT n.id FROM notes n
+ JOIN nodes nd ON nd.id = n.id
+ WHERE n.id = ${params.noteId} AND nd.workspace_id = ${locals.workspace.id}
+ `;
+ if (!note) error(404, 'Notat ikke funnet');
+
+ const [updated] = await sql`
+ UPDATE notes SET
+ title = COALESCE(${updates.title ?? null}, title),
+ content = CASE WHEN ${updates.content !== undefined} THEN ${updates.content ?? ''} ELSE content END
+ WHERE id = ${params.noteId}
+ RETURNING id, title, content, updated_at
+ `;
+
+ return json(updated);
+};