Release 0.5.1

This commit is contained in:
hnzio committed 2026-10-22 07:56:25 +02:00
1 parent 5a9d7735d4
commit fb24d171ae
36 files changed
+5099 -263

No files matched your search

+84 -106
View File
@@ -1,7 +1,12 @@
<script lang="ts">
import { enhance, type SubmitFunction } from '$app/forms';
import { replaceState } from '$app/navigation';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { flip } from 'svelte/animate';
import { onMount, untrack } from 'svelte';
import PieceUsageHistory from '#lib/components/PieceUsageHistory.svelte';
import UsageEditorDialog from '#lib/components/UsageEditorDialog.svelte';
import { formatClock, formatDuration } from '#lib/format.js';
import type { PageProps } from './$types';
@@ -23,9 +28,27 @@
let dropSectionId = $state('');
let dropPosition = $state(0);
let copyMessage = $state('');
let showUsedPieces = $state(untrack(() => data.filters.includeUsed));
let showPlannedPieces = $state(untrack(() => data.filters.includePlanned));
let visiblePieces = $derived(
data.pieces.filter(
(piece) =>
(showUsedPieces || piece.sentHistory.length === 0) &&
(showPlannedPieces || !piece.isPlannedInCurrentShow)
)
);
let plannedCount = $derived(
data.show.sections.reduce((total, section) => total + section.usages.length, 0)
);
onMount(() => {
const syncFiltersFromHistory = () => {
const search = new URL(window.location.href).searchParams;
showUsedPieces = search.get('includeUsed') === '1';
showPlannedPieces = search.get('includePlanned') === '1';
};
window.addEventListener('popstate', syncFiltersFromHistory);
return () => window.removeEventListener('popstate', syncFiltersFromHistory);
});
function berlinDate(value: string | null) {
if (!value) return 'Termin offen';
@@ -36,8 +59,15 @@
}).format(new Date(value));
}
function durationInMinutes(seconds: number) {
return Number((seconds / 60).toFixed(2));
function setSourceFilter(name: 'includeUsed' | 'includePlanned', checked: boolean) {
if (name === 'includeUsed') showUsedPieces = checked;
else showPlannedPieces = checked;
const url = new URL(page.url.href);
if (checked) url.searchParams.set(name, '1');
else url.searchParams.delete(name);
// This URL is cloned from the current page; only same-route filter parameters change.
// eslint-disable-next-line svelte/no-navigation-without-resolve
replaceState(url, page.state);
}
function startDrag(kind: 'piece' | 'section' | 'usage', id: string, event: DragEvent) {
@@ -142,6 +172,7 @@
affected?.classList.add('is-updating');
if (submitter instanceof HTMLButtonElement) submitter.disabled = true;
return async ({ result, update }) => {
if (result.type === 'success') formElement.closest<HTMLDialogElement>('dialog')?.close();
await update({ reset: result.type === 'success', invalidateAll: true });
affected?.classList.remove('is-updating');
if (submitter instanceof HTMLButtonElement) submitter.disabled = false;
@@ -264,9 +295,9 @@
<p class="eyebrow">Quelle</p>
<h2 id="source-title">Sendefertiger Fundus</h2>
</div>
<span>{data.pieces.length}</span>
<span>{visiblePieces.length}</span>
</header>
<form method="GET" class="builder-filter">
<form id="builder-filters" method="GET" class="builder-filter">
<label
><span class="sr-only">Fundus durchsuchen</span><input
type="search"
@@ -286,8 +317,32 @@
>
<button class="text-button" type="submit">Filtern</button>
</form>
<div class="builder-filter-options" aria-label="Fundusauswahl erweitern">
<label class="filter-checkbox">
<input
type="checkbox"
name="includeUsed"
value="1"
form="builder-filters"
checked={showUsedPieces}
onchange={(event) => setSourceFilter('includeUsed', event.currentTarget.checked)}
/>
<span>Auch verwendete Inhalte zeigen</span>
</label>
<label class="filter-checkbox">
<input
type="checkbox"
name="includePlanned"
value="1"
form="builder-filters"
checked={showPlannedPieces}
onchange={(event) => setSourceFilter('includePlanned', event.currentTarget.checked)}
/>
<span>Auch eingeplante Inhalte zeigen</span>
</label>
</div>
<div class="builder-source-list">
{#each data.pieces as piece (piece.id)}
{#each visiblePieces as piece (piece.id)}
<article
class="builder-source-card"
class:is-dragging={dragged?.kind === 'piece' && dragged.id === piece.id}
@@ -310,6 +365,8 @@
<p>
{piece.format} · {piece.topics.map((item) => item.name).join(', ') || 'Ohne Thema'}
</p>
{#if piece.isPlannedInCurrentShow}<span class="planned-label">Eingeplant</span>{/if}
<PieceUsageHistory history={piece.sentHistory} />
<small class="drag-hint">Ziehen oder Ziel auswählen</small>
</div>
<form
@@ -338,10 +395,13 @@
</article>
{/each}
</div>
{#if !data.pieces.length}<div class="empty-state">
{#if !visiblePieces.length}<div class="empty-state">
<span aria-hidden="true">⌕</span>
<h2>Nichts gefunden</h2>
<p>Filter ändern oder Pieces im Fundus sendefertig markieren.</p>
<h2>Keine verfügbaren Pieces</h2>
<p>
Suchfilter ändern, ausgeblendete Inhalte einblenden oder Pieces im Fundus sendefertig
markieren.
</p>
</div>{/if}
</section>
@@ -507,105 +567,15 @@
aria-label={`${usage.title} nach unten`}>↓</button
>
</form>
<UsageEditorDialog
{usage}
revision={data.show.revision}
sectionId={section.id}
sections={data.show.sections}
error={form?.error}
{enhanceCommand}
/>
</div>
<details class="usage-editor">
<summary>Bearbeiten und verschieben</summary>
<form
use:enhance={enhanceCommand}
method="POST"
action="?/updateUsage"
class="usage-edit-form"
>
<input type="hidden" name="revision" value={data.show.revision} /><input
type="hidden"
name="usageId"
value={usage.id}
/>
<label
>Titel<input
name="title"
required
maxlength="200"
value={usage.title}
/></label
><label
>Dauer in Minuten<input
name="durationMinutes"
type="number"
min="0"
max="1440"
step="any"
value={durationInMinutes(usage.durationSeconds)}
/></label
>
<label class="wide-field"
>Eigene Notizen<textarea name="ownNotes" rows="2"
>{usage.ownNotes}</textarea
></label
><label class="wide-field"
>Einleitung<textarea name="intro" rows="2">{usage.intro}</textarea
></label
><label class="wide-field"
>Recherche<textarea name="research" rows="2"
>{usage.research}</textarea
></label
><label class="wide-field"
>Moderation<textarea name="moderation" rows="2"
>{usage.moderation}</textarea
></label
><label class="wide-field"
>Überleitung<textarea name="transition" rows="2"
>{usage.transition}</textarea
></label
><label class="wide-field"
>Präsentation<textarea name="presentation" rows="2"
>{usage.presentation}</textarea
></label
>
<input type="hidden" name="status" value={usage.status} /><button
class="primary-button wide-field"
type="submit">Verwendung speichern</button
>
</form>
<div class="usage-command-row">
<form use:enhance={enhanceCommand} method="POST" action="?/moveUsage">
<input
type="hidden"
name="revision"
value={data.show.revision}
/><input type="hidden" name="usageId" value={usage.id} /><input
type="hidden"
name="position"
value="9999"
/><label
>Ziel<select name="sectionId"
><option value="">Reservebank</option
>{#each data.show.sections as target (target.id)}<option
value={target.id}
selected={target.id === section.id}>{target.name}</option
>{/each}</select
></label
><button class="secondary-button" type="submit">Verschieben</button>
</form>
<form
use:enhance={enhanceCommand}
method="POST"
action="?/removeUsage"
onsubmit={(event) =>
!confirm('Verwendung aus der Sendung entfernen?') &&
event.preventDefault()}
>
<input
type="hidden"
name="revision"
value={data.show.revision}
/><input type="hidden" name="usageId" value={usage.id} /><button
class="text-button danger-text"
type="submit">Entfernen</button
>
</form>
</div>
</details>
</div>
</div>
{/each}
@@ -811,6 +781,14 @@
aria-label={`${usage.title} aus Reserve entfernen`}>Entfernen</button
>
</form>
<UsageEditorDialog
{usage}
revision={data.show.revision}
sectionId=""
sections={data.show.sections}
error={form?.error}
{enhanceCommand}
/>
</div>
</div>
{/each}