synops/frontend/src/lib/components/PublishDialog.svelte
vegard 63630eb55a Fullfører oppgave 14.16: Presentasjonselementer som noder
Publisert tittel, ingress, OG-bilde og undertittel er nå egne noder
koblet til artikler via title/subtitle/summary/og_image-edges.
Rendering bruker presentasjonselementer med fallback til artikkelfelt.

Backend:
- Ny query: GET /query/presentation_elements?article_id=...
- render_article_to_cas henter presentasjonselementer via edges
- fetch_article + fetch_index_articles bruker pres.elementer
- Batch-henting for forsideartikler (én SQL-spørring)
- ArticleData utvides med subtitle + og_image
- Alle fire temaer viser subtitle og OG-bilde
- SEO og_image-tag fylles fra presentasjonselement

Frontend:
- PresentationEditor.svelte: opprett/rediger tittel, undertittel,
  ingress, OG-bilde med variantvelger (editorial/ai/social/rss)
- Integrert i PublishDialog via <details>-seksjon
- API-klient: fetchPresentationElements(), deleteNode()

Grunnlag for A/B-testing (oppgave 14.17): edge-metadata støtter
ab_status/impressions/clicks/ctr, best_of() prioriterer winner > testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 02:55:23 +00:00

194 lines
5.5 KiB
Svelte

<script lang="ts">
import type { Node } from '$lib/spacetime';
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>