Release 0.5.2
This commit is contained in:
1 parent
fb24d171ae
commit
25da870601
10 files changed
+1308
-298
No files matched your search
+519
-292
@@ -2,10 +2,13 @@
|
||||
import { enhance, type SubmitFunction } from '$app/forms';
|
||||
import { beforeNavigate, goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { onDestroy, untrack } from 'svelte';
|
||||
import { onDestroy, tick, untrack } from 'svelte';
|
||||
import EditorialMarkdownField from '#lib/components/EditorialMarkdownField.svelte';
|
||||
import MarkdownPreview from '#lib/components/MarkdownPreview.svelte';
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
type ContextPanel = 'sources' | 'classification' | 'presentation';
|
||||
|
||||
let { data, form }: PageProps = $props();
|
||||
let draft = $state(untrack(() => ({ ...data.piece })));
|
||||
let durationMinutes = $state(untrack(() => Number((data.piece.durationSeconds / 60).toFixed(2))));
|
||||
@@ -14,6 +17,10 @@
|
||||
let rendered = $state(untrack(() => ({ ...data.rendered })));
|
||||
let saveState = $state<'idle' | 'dirty' | 'saving' | 'saved' | 'error' | 'conflict'>('idle');
|
||||
let saveMessage = $state('Alle Änderungen gespeichert');
|
||||
let classificationEditing = $state(false);
|
||||
let presentationEditing = $state(false);
|
||||
let mobileContext = $state<ContextPanel | null>(null);
|
||||
let sourceImageUnavailable = $state(false);
|
||||
let editorForm: HTMLFormElement;
|
||||
let previewForm: HTMLFormElement;
|
||||
let saveButton: HTMLButtonElement;
|
||||
@@ -26,8 +33,27 @@
|
||||
saveState === 'error' ||
|
||||
saveState === 'conflict'
|
||||
);
|
||||
let featuredSource = $derived(
|
||||
draft.presentationSourcePosition === null
|
||||
? (sources[0] ?? null)
|
||||
: (sources[draft.presentationSourcePosition] ?? sources[0] ?? null)
|
||||
);
|
||||
let sourcePreviewImage = $derived(
|
||||
draft.presentationImageUrl
|
||||
? resolve('/fundus/[id]/praesentation/preview-image', { id: data.piece.id })
|
||||
: data.piece.media[0]
|
||||
? `/media/${data.piece.media[0].id}`
|
||||
: ''
|
||||
);
|
||||
let pendingHref: string | null = null;
|
||||
let allowNavigation = false;
|
||||
let contextTrigger: HTMLElement | null = null;
|
||||
|
||||
const statusLabels = {
|
||||
inbox: 'Eingang',
|
||||
preparing: 'In Vorbereitung',
|
||||
ready: 'Sendefertig'
|
||||
};
|
||||
|
||||
onDestroy(() => clearTimeout(timer));
|
||||
beforeNavigate((navigation) => {
|
||||
@@ -87,11 +113,29 @@
|
||||
draft.presentationMetadataFetchedAtUtc = null;
|
||||
draft.presentationProvider = '';
|
||||
draft.presentationProviderId = '';
|
||||
sourceImageUnavailable = false;
|
||||
if (!draft.presentationTitleManual) draft.presentationTitle = '';
|
||||
if (!draft.presentationDescriptionManual) draft.presentationDescription = '';
|
||||
if (!draft.presentationImageManual) draft.presentationImageUrl = '';
|
||||
}
|
||||
|
||||
async function openContext(event: MouseEvent, panel: ContextPanel) {
|
||||
contextTrigger = event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
|
||||
mobileContext = panel;
|
||||
await tick();
|
||||
document.querySelector<HTMLElement>(`[data-context-close="${panel}"]`)?.focus();
|
||||
}
|
||||
|
||||
function closeContext() {
|
||||
mobileContext = null;
|
||||
contextTrigger?.focus();
|
||||
contextTrigger = null;
|
||||
}
|
||||
|
||||
function handleEscape(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && mobileContext) closeContext();
|
||||
}
|
||||
|
||||
const enhanceSave: SubmitFunction = () => {
|
||||
const submittedGeneration = generation;
|
||||
saving = true;
|
||||
@@ -132,22 +176,9 @@
|
||||
</script>
|
||||
|
||||
<svelte:head><title>{draft.title} | Fundus | Sendewerk</title></svelte:head>
|
||||
<svelte:window onkeydown={handleEscape} />
|
||||
|
||||
<div class="page-wrap editor-page">
|
||||
<header class="editor-header">
|
||||
<div>
|
||||
<a class="back-link" href={resolve('/fundus')}>← Fundus</a>
|
||||
<h1>{draft.title}</h1>
|
||||
</div>
|
||||
<div
|
||||
class:problem={saveState === 'error' || saveState === 'conflict'}
|
||||
class="save-indicator"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<i></i><span>{saveMessage}</span>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-wrap editor-page worksheet-page">
|
||||
{#if form?.error}<div class="form-message error" role="alert">{form.error}</div>{/if}
|
||||
|
||||
<form
|
||||
@@ -155,297 +186,493 @@
|
||||
use:enhance={enhanceSave}
|
||||
method="POST"
|
||||
action="?/save"
|
||||
class="piece-editor"
|
||||
class="piece-editor worksheet-editor"
|
||||
oninput={scheduleSave}
|
||||
onchange={scheduleSave}
|
||||
>
|
||||
<input type="hidden" name="revision" value={revision} />
|
||||
<section class="editor-section editor-basics">
|
||||
<div class="section-title">
|
||||
<p class="eyebrow">Grundlagen</p>
|
||||
<h2>Worum geht es?</h2>
|
||||
<header class="worksheet-header">
|
||||
<div class="worksheet-heading">
|
||||
<a class="back-link" href={resolve('/fundus')}>← Fundus</a>
|
||||
<h1 class="sr-only">{draft.title}</h1>
|
||||
<label class="worksheet-title">
|
||||
<span class="sr-only">Titel</span>
|
||||
<textarea
|
||||
name="title"
|
||||
required
|
||||
rows="1"
|
||||
maxlength="200"
|
||||
aria-label="Titel"
|
||||
bind:value={draft.title}></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="editor-fields two-columns">
|
||||
<label class="wide-field"
|
||||
>Titel<input name="title" required maxlength="200" bind:value={draft.title} /></label
|
||||
>
|
||||
<label
|
||||
>Status<select name="status" bind:value={draft.status}
|
||||
><option value="inbox">Eingang</option><option value="preparing">In Vorbereitung</option
|
||||
><option value="ready">Sendefertig</option></select
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Format<select name="formatId" bind:value={draft.formatId}
|
||||
><option value={null}>Ohne Format</option
|
||||
>{#each data.taxonomy.formats as item (item.id)}<option value={item.id}
|
||||
>{item.name}</option
|
||||
>{/each}</select
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Dauer in Minuten<input
|
||||
name="durationMinutes"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1440"
|
||||
step="any"
|
||||
bind:value={durationMinutes}
|
||||
/></label
|
||||
>
|
||||
<label class="wide-field"
|
||||
>Kurzbeschreibung<textarea
|
||||
name="summary"
|
||||
rows="2"
|
||||
maxlength="2000"
|
||||
bind:value={draft.summary}></textarea></label
|
||||
>
|
||||
<div
|
||||
class:problem={saveState === 'error' || saveState === 'conflict'}
|
||||
class="save-indicator"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<i></i><span>{saveMessage}</span>
|
||||
</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<section class="editor-section taxonomy-editor">
|
||||
<div class="section-title">
|
||||
<p class="eyebrow">Einordnung</p>
|
||||
<h2>Themen und Abschnitte</h2>
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>Themen</legend>
|
||||
<div class="choice-grid">
|
||||
{#each data.taxonomy.topics as item (item.id)}<label
|
||||
><input
|
||||
type="checkbox"
|
||||
name="topicIds"
|
||||
value={item.id}
|
||||
bind:group={draft.topicIds}
|
||||
/>{item.name}</label
|
||||
>{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Vorgeschlagene Abschnittskategorien</legend>
|
||||
<div class="choice-grid">
|
||||
{#each data.taxonomy.categories as item (item.id)}<label
|
||||
><input
|
||||
type="checkbox"
|
||||
name="categoryIds"
|
||||
value={item.id}
|
||||
bind:group={draft.categoryIds}
|
||||
/><i style={`--choice-color:${item.color}`}></i>{item.name}</label
|
||||
>{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
<nav class="mobile-editor-tools" aria-label="Piece-Werkzeuge">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={mobileContext === 'sources'}
|
||||
onclick={(event) => openContext(event, 'sources')}>Quellen</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={mobileContext === 'classification'}
|
||||
onclick={(event) => openContext(event, 'classification')}>Einordnung</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={mobileContext === 'presentation'}
|
||||
onclick={(event) => openContext(event, 'presentation')}>Präsentation</button
|
||||
>
|
||||
<a href="#piece-media">Bilder</a>
|
||||
</nav>
|
||||
|
||||
<section class="editor-section markdown-section">
|
||||
<div class="section-title">
|
||||
<p class="eyebrow">Redaktion</p>
|
||||
<h2>Getrennte Arbeitsbereiche</h2>
|
||||
<small>Markdown wird unterstützt und vor der Anzeige bereinigt.</small>
|
||||
</div>
|
||||
<div class="markdown-grid">
|
||||
<label
|
||||
><span>Einleitung</span><textarea
|
||||
name="intro"
|
||||
rows="7"
|
||||
maxlength="50000"
|
||||
bind:value={draft.intro}></textarea>
|
||||
<MarkdownPreview html={rendered.intro} /></label
|
||||
<div class="editor-workspace">
|
||||
<main class="editorial-worksheet" aria-label="Redaktionelles Arbeitsblatt">
|
||||
<section
|
||||
class:has-image={sourcePreviewImage && !sourceImageUnavailable}
|
||||
class="source-feature"
|
||||
>
|
||||
<label class="private-field"
|
||||
><span>Private Recherche</span><textarea
|
||||
name="researchText"
|
||||
rows="7"
|
||||
maxlength="50000"
|
||||
bind:value={draft.researchText}></textarea>
|
||||
<MarkdownPreview html={rendered.research} /></label
|
||||
>
|
||||
<label
|
||||
><span>Moderationsstichpunkte</span><textarea
|
||||
name="moderationNotes"
|
||||
rows="7"
|
||||
maxlength="50000"
|
||||
bind:value={draft.moderationNotes}></textarea>
|
||||
<MarkdownPreview html={rendered.moderation} /></label
|
||||
>
|
||||
<label
|
||||
><span>Überleitung</span><textarea
|
||||
name="transitionText"
|
||||
rows="7"
|
||||
maxlength="50000"
|
||||
bind:value={draft.transitionText}></textarea>
|
||||
<MarkdownPreview html={rendered.transition} /></label
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="editor-section presentation-editor">
|
||||
<div class="section-title split-section-title">
|
||||
<div>
|
||||
<p class="eyebrow">Präsentation</p>
|
||||
<h2>Darstellung in heinzlive</h2>
|
||||
<small>Nur die ausdrücklich ausgewählte Quelle wird präsentiert.</small>
|
||||
</div>
|
||||
<a
|
||||
class="secondary-button"
|
||||
href={resolve('/fundus/[id]/praesentation', { id: data.piece.id })}
|
||||
target="_blank"
|
||||
rel="noopener">Präsentation testen</a
|
||||
>
|
||||
</div>
|
||||
<div class="presentation-mode-picker" role="group" aria-label="Darstellung">
|
||||
{#each [['text', 'Text'], ['link', 'Linkvorschau'], ['video', 'Video']] as mode (mode[0])}
|
||||
<label
|
||||
><input
|
||||
type="radio"
|
||||
name="presentationMode"
|
||||
value={mode[0]}
|
||||
bind:group={draft.presentationMode}
|
||||
/>{mode[1]}</label
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{#if draft.presentationMode === 'text'}
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationSourcePosition"
|
||||
value={draft.presentationSourcePosition ?? ''}
|
||||
/>
|
||||
<input type="hidden" name="presentationTitle" value={draft.presentationTitle} />
|
||||
<input type="hidden" name="presentationDescription" value={draft.presentationDescription} />
|
||||
<input type="hidden" name="presentationImageUrl" value={draft.presentationImageUrl} />
|
||||
<label class="presentation-text-field"
|
||||
><span>Präsentationstext</span><textarea
|
||||
name="presentationText"
|
||||
rows="7"
|
||||
maxlength="50000"
|
||||
bind:value={draft.presentationText}></textarea>
|
||||
<MarkdownPreview html={rendered.presentation} /></label
|
||||
>
|
||||
{:else}
|
||||
<input type="hidden" name="presentationText" value={draft.presentationText} />
|
||||
<div class="presentation-settings">
|
||||
<label class="wide-field"
|
||||
>Quelle<select
|
||||
name="presentationSourcePosition"
|
||||
bind:value={draft.presentationSourcePosition}
|
||||
onchange={presentationSourceChanged}
|
||||
>
|
||||
<option value={null}>Quelle auswählen</option>
|
||||
{#each sources as source, index (index)}
|
||||
{#if source.url}<option value={index}>{source.label || source.url}</option>{/if}
|
||||
{/each}
|
||||
</select></label
|
||||
>
|
||||
<label
|
||||
>Titel<input
|
||||
name="presentationTitle"
|
||||
maxlength="300"
|
||||
bind:value={draft.presentationTitle}
|
||||
oninput={() => (draft.presentationTitleManual = true)}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Bild-URL<input
|
||||
name="presentationImageUrl"
|
||||
type="url"
|
||||
maxlength="2000"
|
||||
bind:value={draft.presentationImageUrl}
|
||||
oninput={() => (draft.presentationImageManual = true)}
|
||||
/></label
|
||||
>
|
||||
<label class="wide-field"
|
||||
>Anreißer<textarea
|
||||
name="presentationDescription"
|
||||
rows="3"
|
||||
maxlength="1000"
|
||||
bind:value={draft.presentationDescription}
|
||||
oninput={() => (draft.presentationDescriptionManual = true)}></textarea></label
|
||||
>
|
||||
<div class="presentation-preview-actions wide-field">
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
onclick={() => previewForm.requestSubmit()}
|
||||
disabled={actionsBlocked || draft.presentationSourcePosition === null}
|
||||
>Vorschau laden</button
|
||||
>
|
||||
<small>
|
||||
{draft.presentationMetadataFetchedAtUtc
|
||||
? `Zuletzt geladen: ${new Date(draft.presentationMetadataFetchedAtUtc).toLocaleString('de-DE')}`
|
||||
: 'Noch keine Metadaten geladen.'}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationTitleManual"
|
||||
value={draft.presentationTitleManual ? '1' : '0'}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationDescriptionManual"
|
||||
value={draft.presentationDescriptionManual ? '1' : '0'}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationImageManual"
|
||||
value={draft.presentationImageManual ? '1' : '0'}
|
||||
/>
|
||||
<input type="hidden" name="presentationSiteName" value={draft.presentationSiteName} />
|
||||
<input type="hidden" name="presentationProvider" value={draft.presentationProvider} />
|
||||
<input type="hidden" name="presentationProviderId" value={draft.presentationProviderId} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationMetadataFetchedAtUtc"
|
||||
value={draft.presentationMetadataFetchedAtUtc ?? ''}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="editor-section source-editor">
|
||||
<div class="section-title split-section-title">
|
||||
<div>
|
||||
<p class="eyebrow">Belege</p>
|
||||
<h2>Quellen</h2>
|
||||
</div>
|
||||
<button class="secondary-button" type="button" onclick={addSource}>Quelle hinzufügen</button
|
||||
>
|
||||
</div>
|
||||
{#if sources.length}<div class="source-list">
|
||||
{#each sources as source, index (index)}<div class="source-row">
|
||||
<label
|
||||
>Name<input name="sourceLabel" maxlength="200" bind:value={source.label} /></label
|
||||
><label
|
||||
>URL<input
|
||||
name="sourceUrl"
|
||||
type="url"
|
||||
maxlength="2000"
|
||||
bind:value={source.url}
|
||||
/></label
|
||||
><label
|
||||
>Zeitmarke in Sekunden<input
|
||||
name="sourceTimecode"
|
||||
type="number"
|
||||
min="0"
|
||||
max="86400"
|
||||
bind:value={source.timecodeSeconds}
|
||||
/></label
|
||||
><button
|
||||
class="text-button danger-text"
|
||||
type="button"
|
||||
onclick={() => removeSource(index)}>Entfernen</button
|
||||
{#if sourcePreviewImage && !sourceImageUnavailable}
|
||||
<img src={sourcePreviewImage} alt="" onerror={() => (sourceImageUnavailable = true)} />
|
||||
{/if}
|
||||
<div>
|
||||
<p class="eyebrow">Fundstück</p>
|
||||
{#if featuredSource}
|
||||
<h2>{draft.presentationTitle || featuredSource.label || 'Quelle'}</h2>
|
||||
{#if draft.presentationDescription}<p>{draft.presentationDescription}</p>{/if}
|
||||
<small>{featuredSource.label || featuredSource.url}</small>
|
||||
<!-- The URL is a server-validated external HTTP(S) source. -->
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={featuredSource.url} target="_blank" rel="noopener noreferrer"
|
||||
>Original öffnen ↗</a
|
||||
>
|
||||
</div>{/each}
|
||||
</div>{:else}<p class="quiet-copy">Noch keine Quellen hinterlegt.</p>{/if}
|
||||
</section>
|
||||
{:else}
|
||||
<h2>Noch keine Quelle</h2>
|
||||
<p>Eine Quelle lässt sich ergänzen, ohne den Schreibstand zu verlassen.</p>
|
||||
<button
|
||||
class="text-button"
|
||||
type="button"
|
||||
onclick={(event) => openContext(event, 'sources')}>Quelle ergänzen</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="writing-sheet">
|
||||
<header class="writing-sheet-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Arbeitsstand</p>
|
||||
<h2>Gedanken und Sendungstext</h2>
|
||||
</div>
|
||||
<small>Markdown wird beim Speichern sicher formatiert.</small>
|
||||
</header>
|
||||
|
||||
<EditorialMarkdownField
|
||||
name="researchText"
|
||||
label="Eigene Gedanken & Recherche"
|
||||
inputLabel="Private Recherche"
|
||||
hint="Nur intern"
|
||||
emptyText="Beobachtungen, Links und offene Fragen festhalten"
|
||||
bind:value={draft.researchText}
|
||||
html={rendered.research}
|
||||
privateField
|
||||
rows={9}
|
||||
/>
|
||||
|
||||
<div class="script-heading">
|
||||
<p class="eyebrow">Für die Sendung</p>
|
||||
<h2>Sendungstext</h2>
|
||||
<p>Einleitung, Moderation und Übergang bleiben getrennt gespeichert.</p>
|
||||
</div>
|
||||
<div class="script-sections">
|
||||
<EditorialMarkdownField
|
||||
name="intro"
|
||||
label="Einleitung"
|
||||
emptyText="Wie beginnt dieses Thema in der Sendung?"
|
||||
bind:value={draft.intro}
|
||||
html={rendered.intro}
|
||||
/>
|
||||
<EditorialMarkdownField
|
||||
name="moderationNotes"
|
||||
label="Moderationsstichpunkte"
|
||||
emptyText="Die wichtigsten Punkte für die Moderation notieren"
|
||||
bind:value={draft.moderationNotes}
|
||||
html={rendered.moderation}
|
||||
/>
|
||||
<EditorialMarkdownField
|
||||
name="transitionText"
|
||||
label="Überleitung"
|
||||
emptyText="Den Übergang zum nächsten Thema vorbereiten"
|
||||
bind:value={draft.transitionText}
|
||||
html={rendered.transition}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<aside
|
||||
class:context-open={mobileContext}
|
||||
class="editor-context"
|
||||
aria-label="Werkzeuge zum Piece"
|
||||
>
|
||||
<section
|
||||
class:mobile-active={mobileContext === 'sources'}
|
||||
class="context-card source-editor"
|
||||
aria-labelledby="source-context-title"
|
||||
>
|
||||
<header class="context-card-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Fundstück</p>
|
||||
<h2 id="source-context-title">Quellen</h2>
|
||||
</div>
|
||||
<button
|
||||
class="context-close"
|
||||
data-context-close="sources"
|
||||
type="button"
|
||||
aria-label="Quellen schließen"
|
||||
onclick={closeContext}>×</button
|
||||
>
|
||||
</header>
|
||||
{#if sources.length}
|
||||
<div class="source-list">
|
||||
{#each sources as source, index (index)}
|
||||
<div class="source-row">
|
||||
<label
|
||||
>Name<input
|
||||
name="sourceLabel"
|
||||
maxlength="200"
|
||||
bind:value={source.label}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>URL<input
|
||||
name="sourceUrl"
|
||||
type="url"
|
||||
maxlength="2000"
|
||||
bind:value={source.url}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Zeitmarke in Sekunden<input
|
||||
name="sourceTimecode"
|
||||
type="number"
|
||||
min="0"
|
||||
max="86400"
|
||||
bind:value={source.timecodeSeconds}
|
||||
/></label
|
||||
>
|
||||
<button
|
||||
class="text-button danger-text"
|
||||
type="button"
|
||||
onclick={() => removeSource(index)}>Entfernen</button
|
||||
>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="context-empty">Noch keine Quelle hinterlegt.</p>
|
||||
{/if}
|
||||
<button class="secondary-button context-wide-action" type="button" onclick={addSource}
|
||||
>Quelle hinzufügen</button
|
||||
>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class:mobile-active={mobileContext === 'classification'}
|
||||
class="context-card classification-editor"
|
||||
aria-labelledby="classification-context-title"
|
||||
>
|
||||
<header class="context-card-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Überblick</p>
|
||||
<h2 id="classification-context-title">Einordnung</h2>
|
||||
</div>
|
||||
<button
|
||||
class="context-close"
|
||||
data-context-close="classification"
|
||||
type="button"
|
||||
aria-label="Einordnung schließen"
|
||||
onclick={closeContext}>×</button
|
||||
>
|
||||
</header>
|
||||
<div class="classification-summary">
|
||||
<strong>{statusLabels[draft.status as keyof typeof statusLabels]}</strong>
|
||||
<span>{durationMinutes || 0} Min.</span>
|
||||
<span
|
||||
>{data.taxonomy.formats.find((item) => item.id === draft.formatId)?.name ??
|
||||
'Ohne Format'}</span
|
||||
>
|
||||
</div>
|
||||
{#if draft.summary}<p class="context-summary-copy">{draft.summary}</p>{/if}
|
||||
<div class="context-tags">
|
||||
{#each data.taxonomy.topics.filter( (item) => draft.topicIds.includes(item.id) ) as item (item.id)}<span
|
||||
>{item.name}</span
|
||||
>{/each}
|
||||
{#each data.taxonomy.categories.filter( (item) => draft.categoryIds.includes(item.id) ) as item (item.id)}<span
|
||||
>{item.name}</span
|
||||
>{/each}
|
||||
</div>
|
||||
<button
|
||||
class="text-button context-edit-toggle"
|
||||
type="button"
|
||||
aria-expanded={classificationEditing}
|
||||
onclick={() => (classificationEditing = !classificationEditing)}
|
||||
>{classificationEditing ? 'Einordnung schließen' : 'Einordnung bearbeiten'}</button
|
||||
>
|
||||
<div class="context-fields" hidden={!classificationEditing}>
|
||||
<label
|
||||
>Status<select name="status" bind:value={draft.status}
|
||||
><option value="inbox">Eingang</option><option value="preparing"
|
||||
>In Vorbereitung</option
|
||||
><option value="ready">Sendefertig</option></select
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Dauer in Minuten<input
|
||||
name="durationMinutes"
|
||||
type="number"
|
||||
min="0"
|
||||
max="1440"
|
||||
step="any"
|
||||
bind:value={durationMinutes}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Format<select name="formatId" bind:value={draft.formatId}
|
||||
><option value={null}>Ohne Format</option
|
||||
>{#each data.taxonomy.formats as item (item.id)}<option value={item.id}
|
||||
>{item.name}</option
|
||||
>{/each}</select
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Kurzbeschreibung<textarea
|
||||
name="summary"
|
||||
rows="3"
|
||||
maxlength="2000"
|
||||
bind:value={draft.summary}></textarea></label
|
||||
>
|
||||
<fieldset>
|
||||
<legend>Themen</legend>
|
||||
<div class="choice-grid">
|
||||
{#each data.taxonomy.topics as item (item.id)}<label
|
||||
><input
|
||||
type="checkbox"
|
||||
name="topicIds"
|
||||
value={item.id}
|
||||
bind:group={draft.topicIds}
|
||||
/>{item.name}</label
|
||||
>{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Vorgeschlagene Abschnittskategorien</legend>
|
||||
<div class="choice-grid">
|
||||
{#each data.taxonomy.categories as item (item.id)}<label
|
||||
><input
|
||||
type="checkbox"
|
||||
name="categoryIds"
|
||||
value={item.id}
|
||||
bind:group={draft.categoryIds}
|
||||
/><i style={`--choice-color:${item.color}`}></i>{item.name}</label
|
||||
>{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class:mobile-active={mobileContext === 'presentation'}
|
||||
class="context-card presentation-editor"
|
||||
aria-labelledby="presentation-context-title"
|
||||
>
|
||||
<header class="context-card-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Publikum</p>
|
||||
<h2 id="presentation-context-title">Präsentation</h2>
|
||||
</div>
|
||||
<button
|
||||
class="context-close"
|
||||
data-context-close="presentation"
|
||||
type="button"
|
||||
aria-label="Präsentation schließen"
|
||||
onclick={closeContext}>×</button
|
||||
>
|
||||
</header>
|
||||
<div class="audience-preview">
|
||||
<span
|
||||
>{draft.presentationMode === 'text'
|
||||
? 'Text'
|
||||
: draft.presentationMode === 'link'
|
||||
? 'Linkvorschau'
|
||||
: 'Video'}</span
|
||||
>
|
||||
<strong>{draft.presentationTitle || draft.title}</strong>
|
||||
{#if draft.presentationMode === 'text' && rendered.presentation}<MarkdownPreview
|
||||
html={rendered.presentation}
|
||||
/>{:else if draft.presentationDescription}<p>
|
||||
{draft.presentationDescription}
|
||||
</p>{:else}<p>Noch keine Vorschauangaben.</p>{/if}
|
||||
</div>
|
||||
<div class="presentation-card-actions">
|
||||
<button
|
||||
class="text-button"
|
||||
type="button"
|
||||
aria-expanded={presentationEditing}
|
||||
onclick={() => (presentationEditing = !presentationEditing)}
|
||||
>{presentationEditing ? 'Einstellungen schließen' : 'Präsentation bearbeiten'}</button
|
||||
>
|
||||
<a
|
||||
class="secondary-button"
|
||||
href={resolve('/fundus/[id]/praesentation', { id: data.piece.id })}
|
||||
target="_blank"
|
||||
rel="noopener">Präsentation testen</a
|
||||
>
|
||||
</div>
|
||||
<div class="presentation-controls" hidden={!presentationEditing}>
|
||||
<div class="presentation-mode-picker" role="group" aria-label="Darstellung">
|
||||
{#each [['text', 'Text'], ['link', 'Linkvorschau'], ['video', 'Video']] as mode (mode[0])}<label
|
||||
><input
|
||||
type="radio"
|
||||
name="presentationMode"
|
||||
value={mode[0]}
|
||||
bind:group={draft.presentationMode}
|
||||
/>{mode[1]}</label
|
||||
>{/each}
|
||||
</div>
|
||||
{#if draft.presentationMode === 'text'}
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationSourcePosition"
|
||||
value={draft.presentationSourcePosition ?? ''}
|
||||
/>
|
||||
<input type="hidden" name="presentationTitle" value={draft.presentationTitle} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationDescription"
|
||||
value={draft.presentationDescription}
|
||||
/>
|
||||
<input type="hidden" name="presentationImageUrl" value={draft.presentationImageUrl} />
|
||||
<EditorialMarkdownField
|
||||
name="presentationText"
|
||||
label="Präsentationstext"
|
||||
emptyText="Text für die Publikumsausgabe schreiben"
|
||||
bind:value={draft.presentationText}
|
||||
html={rendered.presentation}
|
||||
rows={8}
|
||||
/>
|
||||
{:else}
|
||||
<input type="hidden" name="presentationText" value={draft.presentationText} />
|
||||
<div class="presentation-settings">
|
||||
<label
|
||||
>Quelle<select
|
||||
name="presentationSourcePosition"
|
||||
bind:value={draft.presentationSourcePosition}
|
||||
onchange={presentationSourceChanged}
|
||||
><option value={null}>Quelle auswählen</option
|
||||
>{#each sources as source, index (index)}{#if source.url}<option value={index}
|
||||
>{source.label || source.url}</option
|
||||
>{/if}{/each}</select
|
||||
></label
|
||||
>
|
||||
<label
|
||||
>Titel<input
|
||||
name="presentationTitle"
|
||||
maxlength="300"
|
||||
bind:value={draft.presentationTitle}
|
||||
oninput={() => (draft.presentationTitleManual = true)}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Bild-URL<input
|
||||
name="presentationImageUrl"
|
||||
type="url"
|
||||
maxlength="2000"
|
||||
bind:value={draft.presentationImageUrl}
|
||||
oninput={() => (draft.presentationImageManual = true)}
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Anreißer<textarea
|
||||
name="presentationDescription"
|
||||
rows="3"
|
||||
maxlength="1000"
|
||||
bind:value={draft.presentationDescription}
|
||||
oninput={() => (draft.presentationDescriptionManual = true)}></textarea></label
|
||||
>
|
||||
<div class="presentation-preview-actions">
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="button"
|
||||
onclick={() => previewForm.requestSubmit()}
|
||||
disabled={actionsBlocked || draft.presentationSourcePosition === null}
|
||||
>Vorschau laden</button
|
||||
><small
|
||||
>{draft.presentationMetadataFetchedAtUtc
|
||||
? `Zuletzt geladen: ${new Date(draft.presentationMetadataFetchedAtUtc).toLocaleString('de-DE')}`
|
||||
: 'Noch keine Metadaten geladen.'}</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationTitleManual"
|
||||
value={draft.presentationTitleManual ? '1' : '0'}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationDescriptionManual"
|
||||
value={draft.presentationDescriptionManual ? '1' : '0'}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationImageManual"
|
||||
value={draft.presentationImageManual ? '1' : '0'}
|
||||
/>
|
||||
<input type="hidden" name="presentationSiteName" value={draft.presentationSiteName} />
|
||||
<input type="hidden" name="presentationProvider" value={draft.presentationProvider} />
|
||||
<input type="hidden" name="presentationProviderId" value={draft.presentationProviderId} />
|
||||
<input
|
||||
type="hidden"
|
||||
name="presentationMetadataFetchedAtUtc"
|
||||
value={draft.presentationMetadataFetchedAtUtc ?? ''}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="context-card asset-summary">
|
||||
<div>
|
||||
<p class="eyebrow">Material</p>
|
||||
<h2>Bilder</h2>
|
||||
<p>
|
||||
{data.piece.media.length
|
||||
? `${data.piece.media.length} gespeichert`
|
||||
: 'Noch keine Bilder'}
|
||||
</p>
|
||||
</div>
|
||||
<a class="text-link" href="#piece-media">Bilder verwalten ↓</a>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="editor-savebar">
|
||||
<span class:problem={saveState === 'error' || saveState === 'conflict'}>{saveMessage}</span
|
||||
><button bind:this={saveButton} class="primary-button" type="submit" disabled={saving}
|
||||
<span class:problem={saveState === 'error' || saveState === 'conflict'}>{saveMessage}</span>
|
||||
<button bind:this={saveButton} class="primary-button" type="submit" disabled={saving}
|
||||
>Jetzt speichern</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form
|
||||
bind:this={previewForm}
|
||||
id="preview-load-form"
|
||||
@@ -456,7 +683,7 @@
|
||||
<input type="hidden" name="revision" value={revision} />
|
||||
</form>
|
||||
|
||||
<section class="editor-section media-editor">
|
||||
<section id="piece-media" class="editor-section media-editor supporting-editor-section">
|
||||
<div class="section-title">
|
||||
<p class="eyebrow">Private Dateien</p>
|
||||
<h2>Bilder</h2>
|
||||
|
||||
Reference in new issue
Block a user