synops/frontend/src/lib/components/PublishDialog.svelte
vegard e8a1a80652 Valider fase 22: STDB-migrering fullført, ingen rester i aktiv kode
Validering av fase 22 (SpacetimeDB-migrering) bekrefter:

1. WebSocket-sanntid fungerer:
   - maskinrommet lytter på PG NOTIFY-kanaler (node_changed, edge_changed,
     access_changed, mixer_channel_changed)
   - Enrichment av events med fulle rader fra PG
   - Broadcast via tokio::broadcast til WebSocket-klienter
   - Tilgangskontroll filtrerer events per bruker
   - Frontend kobler til /ws med JWT, mottar initial_sync + inkrementelle events

2. PG LISTEN/NOTIFY-triggere verifisert i database:
   - 4 notify-funksjoner: notify_node_change, notify_edge_change,
     notify_access_change, notify_mixer_channel_change
   - 4 triggere: nodes_notify, edges_notify, node_access_notify,
     mixer_channels_notify

3. Ingen STDB-rester i aktiv kode/konfig:
   - maskinrommet/src/: rent
   - Cargo.toml: ingen spacetimedb-avhengigheter
   - docker-compose.yml: ingen spacetimedb-tjeneste
   - Caddyfile: ingen spacetimedb-proxy
   - Eneste funn: frontend/src/lib/spacetime/ katalognavn —
     omdøpt til frontend/src/lib/realtime/ (32 filer oppdatert)
   - Historiske referanser i docs/arkiv og scripts/synops.md er OK
2026-03-18 16:31:16 +00:00

194 lines
5.5 KiB
Svelte

<script lang="ts">
import type { Node } from '$lib/realtime';
import { createEdge } from '$lib/api';
import PresentationEditor from './PresentationEditor.svelte';
interface Props {
/** The content node to publish */
node: Node;
/** The target publishing collection */
collection: Node;
/** Publishing trait config */
pubConfig: Record<string, unknown>;
/** Auth token */
accessToken: string;
/** Close callback */
onclose: () => void;
}
let { node, collection, pubConfig, accessToken, onclose }: Props = $props();
const collectionSlug = $derived((pubConfig.slug as string) ?? '');
const theme = $derived((pubConfig.theme as string) ?? 'default');
/** Auto-generate slug from title */
function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[æ]/g, 'ae')
.replace(/[ø]/g, 'oe')
.replace(/[å]/g, 'aa')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80);
}
let articleSlug = $state(slugify(node.title ?? 'uten-tittel'));
let isPublishing = $state(false);
let error = $state<string | null>(null);
let showPreview = $state(false);
const shortId = $derived(node.id.slice(0, 8));
const publicUrl = $derived(
collectionSlug ? `/api/pub/${collectionSlug}/${shortId}` : ''
);
const previewUrl = $derived(
collectionSlug ? `/api/pub/${collectionSlug}/preview/${theme}` : ''
);
async function handlePublish() {
if (isPublishing) return;
isPublishing = true;
error = null;
try {
await createEdge(accessToken, {
source_id: node.id,
target_id: collection.id,
edge_type: 'belongs_to',
metadata: {}
});
onclose();
} catch (err) {
console.error('Publiseringsfeil:', err);
error = err instanceof Error ? err.message : 'Ukjent feil ved publisering';
} finally {
isPublishing = false;
}
}
</script>
<!-- Backdrop -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
onkeydown={(e) => { if (e.key === 'Escape') onclose(); }}
>
<!-- Dialog -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="w-full max-w-lg rounded-xl bg-white shadow-2xl"
onclick={(e) => e.stopPropagation()}
role="dialog"
aria-label="Publiser artikkel"
>
<!-- Header -->
<div class="flex items-center justify-between border-b border-gray-100 px-5 py-4">
<h2 class="text-base font-semibold text-gray-900">Publiser artikkel</h2>
<button
onclick={onclose}
class="rounded-lg p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
aria-label="Lukk"
>
&times;
</button>
</div>
<!-- Body -->
<div class="space-y-4 px-5 py-4">
{#if error}
<div class="rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700">
{error}
</div>
{/if}
<!-- Article info -->
<div>
<h3 class="text-sm font-medium text-gray-900">{node.title || 'Uten tittel'}</h3>
{#if node.content}
<p class="mt-1 text-xs text-gray-500">{node.content.slice(0, 200)}{node.content.length > 200 ? '...' : ''}</p>
{/if}
</div>
<!-- Target collection -->
<div>
<dt class="text-xs font-medium text-gray-500">Publiseres i</dt>
<dd class="mt-0.5 text-sm text-gray-900">{collection.title ?? 'Samling'}</dd>
</div>
<!-- Slug editor -->
<div>
<label for="article-slug" class="block text-xs font-medium text-gray-500">
Artikkel-slug
</label>
<div class="mt-1 flex items-center gap-2">
<input
id="article-slug"
type="text"
bind:value={articleSlug}
class="block w-full rounded-lg border border-gray-300 px-3 py-1.5 text-sm font-mono text-gray-900 focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 focus:outline-none"
placeholder="artikkel-slug"
/>
</div>
{#if collectionSlug}
<p class="mt-1 text-xs text-gray-400">
URL: synops.no/pub/{collectionSlug}/{shortId}
</p>
{/if}
</div>
<!-- Presentasjonselementer -->
<details class="rounded-lg border border-gray-200" open>
<summary class="cursor-pointer px-3 py-2 text-xs font-medium text-gray-700">
Presentasjonselementer (tittel, ingress, bilde)
</summary>
<div class="border-t border-gray-100 px-3 py-3">
<PresentationEditor articleId={node.id} {accessToken} />
</div>
</details>
<!-- Theme info -->
<div class="flex items-center gap-4 text-xs text-gray-500">
<span>Tema: <span class="font-medium text-gray-700">{theme}</span></span>
{#if previewUrl}
<button
onclick={() => { showPreview = !showPreview; }}
class="text-emerald-600 hover:underline"
>
{showPreview ? 'Skjul forhåndsvisning' : 'Vis forhåndsvisning'}
</button>
{/if}
</div>
<!-- Preview iframe -->
{#if showPreview && previewUrl}
<div class="overflow-hidden rounded-lg border border-gray-200">
<iframe
src={previewUrl}
title="Tema-forhåndsvisning"
class="h-[300px] w-full border-0"
sandbox="allow-same-origin"
></iframe>
</div>
{/if}
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 border-t border-gray-100 px-5 py-4">
<button
onclick={onclose}
class="rounded-lg px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100"
disabled={isPublishing}
>
Avbryt
</button>
<button
onclick={handlePublish}
class="rounded-lg bg-emerald-600 px-4 py-2 text-sm font-medium text-white hover:bg-emerald-700 disabled:opacity-50"
disabled={isPublishing}
>
{isPublishing ? 'Publiserer...' : 'Publiser'}
</button>
</div>
</div>
</div>