Release 0.6.0
This commit is contained in:
1 parent
fd47910d09
commit
b80dded55d
40 files changed
+4080
-856
No files matched your search
@@ -0,0 +1,249 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import AppIcon from './AppIcon.svelte';
|
||||
|
||||
type ShareShow = {
|
||||
id: string;
|
||||
title: string;
|
||||
startsAtUtc: string | null;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type ShareItem = {
|
||||
id: string;
|
||||
showId: string;
|
||||
showTitle: string;
|
||||
showStatus: string;
|
||||
showStartsAtUtc: string | null;
|
||||
scope: 'rundown' | 'moderation' | 'research';
|
||||
isPasswordProtected: boolean;
|
||||
expiresAtUtc: string | null;
|
||||
revokedAtUtc: string | null;
|
||||
createdAtUtc: string;
|
||||
};
|
||||
|
||||
let {
|
||||
shows,
|
||||
shares,
|
||||
fixedShowId = '',
|
||||
form = null
|
||||
}: {
|
||||
shows: ShareShow[];
|
||||
shares: ShareItem[];
|
||||
fixedShowId?: string;
|
||||
form?: { intent?: string; error?: string; success?: string; shareUrl?: string } | null;
|
||||
} = $props();
|
||||
|
||||
let visibleShows = $derived(
|
||||
fixedShowId
|
||||
? shows.filter((show) => show.id === fixedShowId)
|
||||
: shows.filter((show) => shares.some((share) => share.showId === show.id))
|
||||
);
|
||||
|
||||
function germanDate(value: string | null, fallback = 'Termin offen') {
|
||||
return value
|
||||
? new Intl.DateTimeFormat('de-DE', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
}).format(new Date(value))
|
||||
: fallback;
|
||||
}
|
||||
|
||||
function berlinInput(value: string | null) {
|
||||
if (!value) return '';
|
||||
const parts = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone: 'Europe/Berlin',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h23'
|
||||
}).formatToParts(new Date(value));
|
||||
const read = (type: Intl.DateTimeFormatPartTypes) =>
|
||||
parts.find((part) => part.type === type)?.value ?? '';
|
||||
return `${read('year')}-${read('month')}-${read('day')}T${read('hour')}:${read('minute')}`;
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === 'draft') return 'Entwurf';
|
||||
if (status === 'ready') return 'Bereit';
|
||||
if (status === 'live') return 'Live';
|
||||
return 'Abgeschlossen';
|
||||
}
|
||||
|
||||
function scopeLabel(scope: ShareItem['scope']) {
|
||||
if (scope === 'rundown') return 'Nur Ablauf';
|
||||
if (scope === 'moderation') return 'Ablauf und Moderation';
|
||||
return 'Vollständig mit Recherche';
|
||||
}
|
||||
|
||||
function shareState(share: ShareItem) {
|
||||
if (share.revokedAtUtc) return 'Widerrufen';
|
||||
if (share.expiresAtUtc && new Date(share.expiresAtUtc) <= new Date()) return 'Abgelaufen';
|
||||
return 'Aktiv';
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="quiet-panel share-management"
|
||||
aria-labelledby={`share-title-${fixedShowId || 'all'}`}
|
||||
>
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Gastzugänge</p>
|
||||
<h2 id={`share-title-${fixedShowId || 'all'}`}>Lesefreigaben</h2>
|
||||
</div>
|
||||
<span class="mail-mode">Aktualisiert sich bis zum Abschluss</span>
|
||||
</div>
|
||||
<p class="share-intro">
|
||||
Der Gast sieht nur diese Sendung und den gewählten Informationsumfang. Der vollständige Link
|
||||
wird aus Sicherheitsgründen nur direkt nach dem Anlegen angezeigt.
|
||||
</p>
|
||||
{#if form?.intent?.toLowerCase().includes('share') && form.error}<div
|
||||
class="form-message error"
|
||||
role="alert"
|
||||
>
|
||||
{form.error}
|
||||
</div>{/if}
|
||||
{#if form?.intent?.toLowerCase().includes('share') && form.success}<div
|
||||
class="form-message success"
|
||||
role="status"
|
||||
>
|
||||
{form.success}
|
||||
</div>{/if}
|
||||
{#if form?.shareUrl}<div class="form-message success share-result" role="status">
|
||||
<strong>Neuer Leselink</strong>
|
||||
<!-- The server returns only a same-origin share path created by this action. -->
|
||||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||||
<a href={form.shareUrl}>{form.shareUrl}</a>
|
||||
</div>{/if}
|
||||
|
||||
<form class="share-create-form" method="POST" action="?/createShare">
|
||||
{#if fixedShowId}
|
||||
<input type="hidden" name="showId" value={fixedShowId} />
|
||||
{:else}
|
||||
<label
|
||||
>Sendung<select name="showId" required>
|
||||
<option value="">Sendung wählen</option>
|
||||
{#each shows as show (show.id)}<option value={show.id}
|
||||
>{show.title} · {statusLabel(show.status)}</option
|
||||
>{/each}
|
||||
</select></label
|
||||
>
|
||||
{/if}
|
||||
<label
|
||||
>Informationsumfang<select name="scope">
|
||||
<option value="rundown">Nur Ablauf</option>
|
||||
<option value="moderation">Ablauf und Moderation</option>
|
||||
<option value="research">Vollständig mit Recherche</option>
|
||||
</select></label
|
||||
>
|
||||
<label
|
||||
>Optionales Passwort<input
|
||||
name="password"
|
||||
type="password"
|
||||
minlength="12"
|
||||
maxlength="256"
|
||||
autocomplete="new-password"
|
||||
/></label
|
||||
>
|
||||
<label>Optionaler Ablauf<input name="expiresAt" type="datetime-local" /></label>
|
||||
<button class="primary-button" type="submit" disabled={!shows.length}>Leselink anlegen</button>
|
||||
</form>
|
||||
|
||||
{#if visibleShows.length}
|
||||
<div class="share-show-list">
|
||||
{#each visibleShows as show (show.id)}
|
||||
<article class="share-show-card">
|
||||
<header>
|
||||
<div>
|
||||
<a href={resolve('/sendungen/[id]', { id: show.id })}>{show.title}</a>
|
||||
<small>{germanDate(show.startsAtUtc)} Uhr</small>
|
||||
</div>
|
||||
<span class={`show-state ${show.status}`}>{statusLabel(show.status)}</span>
|
||||
</header>
|
||||
<div class="share-list">
|
||||
{#each shares.filter((share) => share.showId === show.id) as share (share.id)}
|
||||
<details class="share-row">
|
||||
<summary>
|
||||
<span class="share-row-icon"><AppIcon name="link" /></span>
|
||||
<span>
|
||||
<strong>{scopeLabel(share.scope)}</strong>
|
||||
<small
|
||||
>{share.isPasswordProtected ? 'Mit Passwort' : 'Ohne Passwort'} · {germanDate(
|
||||
share.expiresAtUtc,
|
||||
'ohne Ablauf'
|
||||
)}</small
|
||||
>
|
||||
</span>
|
||||
<span class:locked={shareState(share) !== 'Aktiv'}>{shareState(share)}</span>
|
||||
<AppIcon name="pen" />
|
||||
</summary>
|
||||
<div class="share-edit-area">
|
||||
{#if share.revokedAtUtc}
|
||||
<p>Diese Freigabe ist widerrufen und kann nur noch gelöscht werden.</p>
|
||||
{:else}
|
||||
<form class="share-edit-form" method="POST" action="?/updateShare">
|
||||
<input type="hidden" name="shareId" value={share.id} />
|
||||
<label
|
||||
>Informationsumfang<select name="scope" value={share.scope}>
|
||||
<option value="rundown">Nur Ablauf</option>
|
||||
<option value="moderation">Ablauf und Moderation</option>
|
||||
<option value="research">Vollständig mit Recherche</option>
|
||||
</select></label
|
||||
>
|
||||
<label
|
||||
>Passwort<select name="passwordMode">
|
||||
<option value="keep">Unverändert</option>
|
||||
<option value="remove">Kein Passwort</option>
|
||||
<option value="replace">Neues Passwort setzen</option>
|
||||
</select></label
|
||||
>
|
||||
<label
|
||||
>Neues Passwort<input
|
||||
name="password"
|
||||
type="password"
|
||||
minlength="12"
|
||||
maxlength="256"
|
||||
autocomplete="new-password"
|
||||
/></label
|
||||
>
|
||||
<label
|
||||
>Ablauf<input
|
||||
name="expiresAt"
|
||||
type="datetime-local"
|
||||
value={berlinInput(share.expiresAtUtc)}
|
||||
/><small>Leer lassen, um den Ablauf zu entfernen.</small></label
|
||||
>
|
||||
<button class="secondary-button" type="submit">Freigabe speichern</button>
|
||||
</form>
|
||||
{/if}
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteShare"
|
||||
onsubmit={(event) => {
|
||||
if (
|
||||
!confirm('Freigabe endgültig löschen? Der Leselink wird sofort ungültig.')
|
||||
)
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="shareId" value={share.id} />
|
||||
<button class="text-button danger-text" type="submit">Freigabe löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
{/each}
|
||||
{#if !shares.some((share) => share.showId === show.id)}
|
||||
<p class="quiet-copy">Für diese Sendung gibt es noch keine Lesefreigabe.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="quiet-copy">Noch keine Lesefreigaben angelegt.</p>
|
||||
{/if}
|
||||
</section>
|
||||
Reference in new issue
Block a user