262 lines
9.2 KiB
Svelte
262 lines
9.2 KiB
Svelte
<script lang="ts">
|
||
import { goto, replaceState } from '$app/navigation';
|
||
import { resolve } from '$app/paths';
|
||
import { page } from '$app/state';
|
||
import { onMount, untrack } from 'svelte';
|
||
import PieceUsageHistory from '#lib/components/PieceUsageHistory.svelte';
|
||
import { formatClock } from '#lib/format.js';
|
||
import type { PageProps } from './$types';
|
||
|
||
let { data, form }: PageProps = $props();
|
||
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.isPlannedInActiveShow)
|
||
)
|
||
);
|
||
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 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);
|
||
}
|
||
|
||
async function resetFilters() {
|
||
await goto(resolve('/fundus'), { invalidateAll: true });
|
||
showUsedPieces = false;
|
||
showPlannedPieces = false;
|
||
}
|
||
const statusLabel = {
|
||
ready: 'Sendefertig',
|
||
preparing: 'In Vorbereitung',
|
||
inbox: 'Eingang',
|
||
archived: 'Archiviert'
|
||
};
|
||
</script>
|
||
|
||
<svelte:head><title>Fundus | Sendewerk</title></svelte:head>
|
||
|
||
<div class="page-wrap">
|
||
<header class="page-heading split-heading">
|
||
<div>
|
||
<h1>Fundus</h1>
|
||
<p class="page-intro">Vorbereitete Inhalte für kommende Sendungen.</p>
|
||
</div>
|
||
<div class="fundus-heading-actions">
|
||
<div class="heading-stat" aria-label={`${visiblePieces.length} gefundene Inhalte`}>
|
||
<strong>{visiblePieces.length}</strong><span>Inhalte</span>
|
||
</div>
|
||
<details class="template-picker">
|
||
<summary><span aria-hidden="true">+</span> Neues Piece</summary>
|
||
<div class="template-popover">
|
||
<div class="template-grid">
|
||
{#each data.templates as template (template.id)}
|
||
<form method="POST" action="?/create">
|
||
<input type="hidden" name="template" value={template.id} />
|
||
<button type="submit"
|
||
><strong>{template.name}</strong><span>{template.description}</span></button
|
||
>
|
||
</form>
|
||
{/each}
|
||
</div>
|
||
{#if data.templates.some((template) => template.id !== 'blank')}<details
|
||
class="template-management"
|
||
>
|
||
<summary>Vorlagen verwalten</summary>
|
||
{#each data.templates.filter((template) => template.id !== 'blank') as template (template.id)}
|
||
<form
|
||
method="POST"
|
||
action="?/deleteTemplate"
|
||
onsubmit={(event) =>
|
||
!confirm(`Vorlage „${template.name}“ löschen?`) && event.preventDefault()}
|
||
>
|
||
<input type="hidden" name="templateId" value={template.id} />
|
||
<input type="hidden" name="revision" value={template.revision} />
|
||
<span>{template.name}</span>
|
||
<button class="text-button danger-text" type="submit">Löschen</button>
|
||
</form>
|
||
{/each}
|
||
</details>{/if}
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</header>
|
||
{#if form?.error}<div class="form-message error" role="alert">{form.error}</div>{/if}
|
||
{#if form?.success}<div class="form-message success" role="status">{form.success}</div>{/if}
|
||
|
||
<form
|
||
id="library-filters"
|
||
class="library-filters"
|
||
method="GET"
|
||
aria-label="Fundus durchsuchen und filtern"
|
||
>
|
||
<div class="fundus-search-row">
|
||
<label class="search-field"
|
||
><span aria-hidden="true">⌕</span><span class="sr-only">Fundus durchsuchen</span><input
|
||
name="q"
|
||
type="search"
|
||
value={data.filters.query}
|
||
placeholder="Titel, Quelle oder Thema suchen"
|
||
/></label
|
||
>
|
||
<button class="secondary-button" type="submit">Suchen</button>
|
||
<details class="filter-picker">
|
||
<summary><span aria-hidden="true">≡</span> Weitere Filter</summary>
|
||
<div class="filter-panel">
|
||
<label
|
||
><span>Status</span><select name="status"
|
||
><option value="">Alle</option><option
|
||
value="ready"
|
||
selected={data.filters.status === 'ready'}>Sendefertig</option
|
||
><option value="preparing" selected={data.filters.status === 'preparing'}
|
||
>In Arbeit</option
|
||
></select
|
||
></label
|
||
>
|
||
<label
|
||
><span>Format</span><select name="format"
|
||
><option value="">Alle</option>{#each data.taxonomy.formats as item (item.id)}<option
|
||
value={item.id}
|
||
selected={data.filters.formatId === item.id}>{item.name}</option
|
||
>{/each}</select
|
||
></label
|
||
>
|
||
<label
|
||
><span>Thema</span><select name="topic"
|
||
><option value="">Alle</option>{#each data.taxonomy.topics as item (item.id)}<option
|
||
value={item.id}
|
||
selected={data.filters.topicId === item.id}>{item.name}</option
|
||
>{/each}</select
|
||
></label
|
||
>
|
||
<label
|
||
><span>Abschnitt</span><select name="category"
|
||
><option value="">Alle</option
|
||
>{#each data.taxonomy.categories as item (item.id)}<option
|
||
value={item.id}
|
||
selected={data.filters.categoryId === item.id}>{item.name}</option
|
||
>{/each}</select
|
||
></label
|
||
>
|
||
<div class="library-filter-options" aria-label="Fundusauswahl erweitern">
|
||
<label class="filter-checkbox">
|
||
<input
|
||
type="checkbox"
|
||
name="includeUsed"
|
||
value="1"
|
||
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"
|
||
checked={showPlannedPieces}
|
||
onchange={(event) => setSourceFilter('includePlanned', event.currentTarget.checked)}
|
||
/>
|
||
<span>Auch eingeplante Inhalte zeigen</span>
|
||
</label>
|
||
</div>
|
||
<div class="filter-actions">
|
||
<button class="secondary-button" type="submit">Filter anwenden</button>
|
||
<button class="text-button" type="button" onclick={resetFilters}>Zurücksetzen</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</form>
|
||
|
||
{#if visiblePieces.length}
|
||
<div class="piece-grid">
|
||
{#each visiblePieces as piece (piece.id)}
|
||
<article
|
||
class="piece-card"
|
||
style={`--category: ${piece.categories[0]?.color ?? '#8b8f98'}`}
|
||
>
|
||
{#if piece.mediaId}<img
|
||
class="piece-thumbnail"
|
||
src={`/media/${piece.mediaId}`}
|
||
alt=""
|
||
/>{:else}<span class="piece-thumbnail piece-thumbnail-placeholder" aria-hidden="true">
|
||
<svg viewBox="0 0 24 24" fill="none">
|
||
<path d="M4.5 6.5h15v11h-15zM7.5 14l3-3 2.2 2.2 1.8-1.7 2.5 2.5" />
|
||
<circle cx="15.8" cy="9.2" r="1.2" />
|
||
</svg>
|
||
</span>{/if}
|
||
<div class="piece-primary">
|
||
<h2><a href={resolve('/fundus/[id]', { id: piece.id })}>{piece.title}</a></h2>
|
||
<p>{piece.summary || 'Noch keine Kurzbeschreibung.'}</p>
|
||
<PieceUsageHistory history={piece.sentHistory} />
|
||
</div>
|
||
<div class="piece-classification">
|
||
<span class="category-label"
|
||
><i></i>{piece.categories.map((item) => item.name).join(', ') || 'Offen'}</span
|
||
>
|
||
<div class="piece-tags">
|
||
<span>{piece.format}</span>{#each piece.topics as topic (topic.id)}<span
|
||
>{topic.name}</span
|
||
>{/each}
|
||
</div>
|
||
</div>
|
||
<div class="piece-state">
|
||
<span class:ready={piece.status === 'ready'} class="status-label"
|
||
>{statusLabel[piece.status]}</span
|
||
><span class="piece-facts"
|
||
><span><b>{formatClock(piece.durationSeconds)}</b> geplant</span><span
|
||
>{piece.sourceCount} {piece.sourceCount === 1 ? 'Quelle' : 'Quellen'}</span
|
||
></span
|
||
>
|
||
</div>
|
||
<details class="piece-actions">
|
||
<summary aria-label={`Aktionen für ${piece.title}`}
|
||
><span aria-hidden="true">•••</span></summary
|
||
>
|
||
<div class="piece-actions-popover">
|
||
<form method="POST" action="?/duplicate">
|
||
<input type="hidden" name="pieceId" value={piece.id} /><button
|
||
class="text-button"
|
||
type="submit">Duplizieren</button
|
||
>
|
||
</form>
|
||
<form method="POST" action="?/archive">
|
||
<input type="hidden" name="pieceId" value={piece.id} /><input
|
||
type="hidden"
|
||
name="revision"
|
||
value={piece.revision}
|
||
/><button class="text-button danger-text" type="submit">Archivieren</button>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
</article>
|
||
{/each}
|
||
</div>
|
||
{:else}
|
||
<div class="empty-state">
|
||
<span aria-hidden="true">⌕</span>
|
||
<h2>Kein passendes Piece</h2>
|
||
<p>Andere Filter wählen oder ein neues Piece anlegen.</p>
|
||
<a class="secondary-button" href={resolve('/fundus')}>Filter zurücksetzen</a>
|
||
</div>
|
||
{/if}
|
||
</div>
|