From 263f63bec8cfdaf2116f907d965e9cc3b08b1817 Mon Sep 17 00:00:00 2001 From: vegard Date: Wed, 18 Mar 2026 00:20:35 +0000 Subject: [PATCH] Trait-aware frontend: samlingssider med dynamiske trait-paneler (oppgave 13.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Samlingsnoder med `metadata.traits` rendres nå som egne sider på /collection/[id]. Hvert trait-navn mappes til en dedikert Svelte-komponent som viser relevant UI. Traits uten egen komponent vises med et generisk panel. Komponenter for 9 traits: editor, chat, kanban, podcast, publishing, rss, calendar, recording, transcription. Mottak-siden viser traits som pills og lenker til samlingssiden. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/traits/CalendarTrait.svelte | 55 +++++++ .../lib/components/traits/ChatTrait.svelte | 55 +++++++ .../lib/components/traits/EditorTrait.svelte | 58 +++++++ .../lib/components/traits/GenericTrait.svelte | 29 ++++ .../lib/components/traits/KanbanTrait.svelte | 23 +++ .../lib/components/traits/PodcastTrait.svelte | 80 ++++++++++ .../components/traits/PublishingTrait.svelte | 38 +++++ .../components/traits/RecordingTrait.svelte | 17 +++ .../src/lib/components/traits/RssTrait.svelte | 49 ++++++ .../lib/components/traits/TraitPanel.svelte | 30 ++++ .../traits/TranscriptionTrait.svelte | 17 +++ frontend/src/routes/+page.svelte | 54 ++++++- .../src/routes/collection/[id]/+page.svelte | 141 ++++++++++++++++++ tasks.md | 3 +- 14 files changed, 644 insertions(+), 5 deletions(-) create mode 100644 frontend/src/lib/components/traits/CalendarTrait.svelte create mode 100644 frontend/src/lib/components/traits/ChatTrait.svelte create mode 100644 frontend/src/lib/components/traits/EditorTrait.svelte create mode 100644 frontend/src/lib/components/traits/GenericTrait.svelte create mode 100644 frontend/src/lib/components/traits/KanbanTrait.svelte create mode 100644 frontend/src/lib/components/traits/PodcastTrait.svelte create mode 100644 frontend/src/lib/components/traits/PublishingTrait.svelte create mode 100644 frontend/src/lib/components/traits/RecordingTrait.svelte create mode 100644 frontend/src/lib/components/traits/RssTrait.svelte create mode 100644 frontend/src/lib/components/traits/TraitPanel.svelte create mode 100644 frontend/src/lib/components/traits/TranscriptionTrait.svelte create mode 100644 frontend/src/routes/collection/[id]/+page.svelte diff --git a/frontend/src/lib/components/traits/CalendarTrait.svelte b/frontend/src/lib/components/traits/CalendarTrait.svelte new file mode 100644 index 0000000..c5cfb05 --- /dev/null +++ b/frontend/src/lib/components/traits/CalendarTrait.svelte @@ -0,0 +1,55 @@ + + + + {#snippet children()} + + Åpne kalender → + + {#if events.length > 0} +
    + {#each events.slice(0, 5) as ev (ev.node.id)} +
  • + {ev.when || '—'} + {ev.node.title || 'Hendelse'} +
  • + {/each} + {#if events.length > 5} +
  • +{events.length - 5} flere
  • + {/if} +
+ {/if} + {/snippet} +
diff --git a/frontend/src/lib/components/traits/ChatTrait.svelte b/frontend/src/lib/components/traits/ChatTrait.svelte new file mode 100644 index 0000000..00283a6 --- /dev/null +++ b/frontend/src/lib/components/traits/ChatTrait.svelte @@ -0,0 +1,55 @@ + + + + {#snippet children()} + {#if chatNodes.length === 0} +

Ingen samtaler knyttet til denne samlingen.

+ {:else} + + {/if} + {/snippet} +
diff --git a/frontend/src/lib/components/traits/EditorTrait.svelte b/frontend/src/lib/components/traits/EditorTrait.svelte new file mode 100644 index 0000000..e7519df --- /dev/null +++ b/frontend/src/lib/components/traits/EditorTrait.svelte @@ -0,0 +1,58 @@ + + + + {#snippet children()} +

+ Preset: {preset} + {#if config.allow_collaborators} + · Samarbeid aktivert + {/if} +

+ {#if contentNodes.length === 0} +

Ingen innholdsnoder ennå.

+ {:else} +
    + {#each contentNodes as node (node.id)} +
  • +

    {node.title || 'Uten tittel'}

    + {#if node.content} +

    {node.content.slice(0, 140)}

    + {/if} +
  • + {/each} +
+ {/if} + {/snippet} +
diff --git a/frontend/src/lib/components/traits/GenericTrait.svelte b/frontend/src/lib/components/traits/GenericTrait.svelte new file mode 100644 index 0000000..f627356 --- /dev/null +++ b/frontend/src/lib/components/traits/GenericTrait.svelte @@ -0,0 +1,29 @@ + + + + {#snippet children()} + {#if configKeys.length > 0} +
+ {#each configKeys as key (key)} +
+
{key}:
+
{JSON.stringify(config[key])}
+
+ {/each} +
+ {:else} +

Aktivert, ingen konfigurasjon.

+ {/if} + {/snippet} +
diff --git a/frontend/src/lib/components/traits/KanbanTrait.svelte b/frontend/src/lib/components/traits/KanbanTrait.svelte new file mode 100644 index 0000000..c27c8b8 --- /dev/null +++ b/frontend/src/lib/components/traits/KanbanTrait.svelte @@ -0,0 +1,23 @@ + + + + {#snippet children()} + + Åpne brett → + + {/snippet} + diff --git a/frontend/src/lib/components/traits/PodcastTrait.svelte b/frontend/src/lib/components/traits/PodcastTrait.svelte new file mode 100644 index 0000000..35d58eb --- /dev/null +++ b/frontend/src/lib/components/traits/PodcastTrait.svelte @@ -0,0 +1,80 @@ + + + + {#snippet children()} + {#if episodes.length === 0} +

Ingen episoder ennå.

+ {:else} +
    + {#each episodes as ep (ep.id)} +
  • +

    {ep.title || 'Uten tittel'}

    + {#if ep.content} +

    {ep.content.slice(0, 100)}

    + {/if} +
    + +
    +
  • + {/each} +
+ {/if} + {/snippet} +
diff --git a/frontend/src/lib/components/traits/PublishingTrait.svelte b/frontend/src/lib/components/traits/PublishingTrait.svelte new file mode 100644 index 0000000..b27be1d --- /dev/null +++ b/frontend/src/lib/components/traits/PublishingTrait.svelte @@ -0,0 +1,38 @@ + + + + {#snippet children()} +
+ {#if slug} +
+
Slug
+
{slug}
+
+ {/if} +
+
Tema
+
{theme}
+
+ {#if customDomain} +
+
Domene
+
{customDomain}
+
+ {/if} +
+ {/snippet} +
diff --git a/frontend/src/lib/components/traits/RecordingTrait.svelte b/frontend/src/lib/components/traits/RecordingTrait.svelte new file mode 100644 index 0000000..ab9bb91 --- /dev/null +++ b/frontend/src/lib/components/traits/RecordingTrait.svelte @@ -0,0 +1,17 @@ + + + + {#snippet children()} +

LiveKit-studio for opptak og sanntidslyd.

+ {/snippet} +
diff --git a/frontend/src/lib/components/traits/RssTrait.svelte b/frontend/src/lib/components/traits/RssTrait.svelte new file mode 100644 index 0000000..6db46c3 --- /dev/null +++ b/frontend/src/lib/components/traits/RssTrait.svelte @@ -0,0 +1,49 @@ + + + + {#snippet children()} +
+
+ Format: + {format.toUpperCase()} +
+
+ Maks elementer: + {maxItems} +
+ +
+ {/snippet} +
diff --git a/frontend/src/lib/components/traits/TraitPanel.svelte b/frontend/src/lib/components/traits/TraitPanel.svelte new file mode 100644 index 0000000..8c62a01 --- /dev/null +++ b/frontend/src/lib/components/traits/TraitPanel.svelte @@ -0,0 +1,30 @@ + + +
+
+ {#if icon} + + {/if} +

{label}

+
+
+ {#if children} + {@render children()} + {:else} +

Ingen innhold ennå.

+ {/if} +
+
diff --git a/frontend/src/lib/components/traits/TranscriptionTrait.svelte b/frontend/src/lib/components/traits/TranscriptionTrait.svelte new file mode 100644 index 0000000..a67540f --- /dev/null +++ b/frontend/src/lib/components/traits/TranscriptionTrait.svelte @@ -0,0 +1,17 @@ + + + + {#snippet children()} +

Automatisk tale-til-tekst via Whisper.

+ {/snippet} +
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index f9c5766..090588c 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -120,15 +120,33 @@ return null; } - /** Check if a node is a kanban board (collection with board metadata) */ + /** Check if a node is a kanban board (collection with board metadata, no traits) */ function isBoard(node: Node): boolean { if (node.nodeKind !== 'collection') return false; try { const meta = JSON.parse(node.metadata ?? '{}'); - return meta.board === true; + return meta.board === true && !meta.traits; } catch { return false; } } + /** Check if a collection has traits */ + function hasTraits(node: Node): boolean { + if (node.nodeKind !== 'collection') return false; + try { + const meta = JSON.parse(node.metadata ?? '{}'); + return meta.traits && typeof meta.traits === 'object' && Object.keys(meta.traits).length > 0; + } catch { return false; } + } + + /** Get trait names from a collection node */ + function traitNames(node: Node): string[] { + try { + const meta = JSON.parse(node.metadata ?? '{}'); + if (meta.traits && typeof meta.traits === 'object') return Object.keys(meta.traits); + } catch { /* ignore */ } + return []; + } + /** Count scheduled events for badge display */ const scheduledCount = $derived.by(() => { if (!connected) return 0; @@ -366,7 +384,37 @@