server/web/src/lib/blocks/NotesBlock.svelte
vegard 7e83292abe Notater: auto-save scratchpad med debounce
- Migrering 0004: notes-tabell (nodes i kunnskapsgrafen)
- REST API: GET/PATCH notat
- PG-adapter med 500ms debounce og 10s polling
- NotesBlock: tittel + fritekst med auto-lagring og status
- Seed: notater for begge workspaces, kalenderside med 2-1 layout

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 03:03:41 +01:00

164 lines
3 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import { createNote } from '$lib/notes/create.svelte';
import type { NoteConnection } from '$lib/notes/types';
let { props = {} }: { props?: Record<string, unknown> } = $props();
const noteId = props.noteId as string | undefined;
let conn = $state<NoteConnection | null>(null);
let title = $state('');
let content = $state('');
let initialized = $state(false);
// Synk fra server ved første lasting
$effect(() => {
if (conn?.note && !initialized) {
title = conn.note.title;
content = conn.note.content;
initialized = true;
}
});
function handleTitleInput() {
conn?.save({ title });
}
function handleContentInput() {
conn?.save({ content });
}
let updatedAt = $derived.by(() => {
if (!conn?.note?.updated_at) return '';
const d = new Date(conn.note.updated_at);
return d.toLocaleString('nb-NO', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
});
onMount(() => {
if (noteId) {
conn = createNote(noteId);
}
return () => conn?.destroy();
});
</script>
{#if !noteId}
<div class="no-note"><p>Ingen notat konfigurert for denne blokken.</p></div>
{:else if conn?.loading}
<div class="no-note"><p>Laster...</p></div>
{:else}
<div class="notes-wrapper">
<input
type="text"
class="note-title"
placeholder="Tittel..."
bind:value={title}
oninput={handleTitleInput}
/>
<textarea
class="note-content"
placeholder="Skriv her..."
bind:value={content}
oninput={handleContentInput}
></textarea>
<div class="note-footer">
{#if conn?.saving}
<span class="status saving">Lagrer...</span>
{:else if updatedAt}
<span class="status saved">Lagret {updatedAt}</span>
{/if}
</div>
</div>
{#if conn?.error}
<div class="error">{conn.error}</div>
{/if}
{/if}
<style>
.notes-wrapper {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
gap: 0.5rem;
}
.note-title {
background: transparent;
border: none;
border-bottom: 1px solid #2d3148;
color: #e1e4e8;
font-size: 1rem;
font-weight: 600;
font-family: inherit;
padding: 0.25rem 0;
}
.note-title:focus {
outline: none;
border-bottom-color: #3b82f6;
}
.note-title::placeholder {
color: #8b92a5;
}
.note-content {
flex: 1;
background: transparent;
border: none;
color: #e1e4e8;
font-size: 0.85rem;
font-family: inherit;
line-height: 1.6;
resize: none;
padding: 0;
min-height: 100px;
}
.note-content:focus {
outline: none;
}
.note-content::placeholder {
color: #8b92a5;
}
.note-footer {
display: flex;
justify-content: flex-end;
padding-top: 0.25rem;
border-top: 1px solid #2d3148;
}
.status {
font-size: 0.7rem;
color: #8b92a5;
}
.status.saving {
color: #f59e0b;
}
.no-note {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #8b92a5;
font-size: 0.85rem;
}
.error {
font-size: 0.75rem;
color: #f87171;
padding: 0.25rem 0;
}
</style>