511 lines
21 KiB
Svelte
511 lines
21 KiB
Svelte
<script lang="ts">
|
||
import { enhance, type SubmitFunction } from '$app/forms';
|
||
import { resolve } from '$app/paths';
|
||
import { onMount, untrack } from 'svelte';
|
||
import { formatClock, formatDuration } from '#lib/format.js';
|
||
import type { PageProps } from './$types';
|
||
|
||
let { data, form }: PageProps = $props();
|
||
let promptSize = $state(1.55);
|
||
let showRail = $state(true);
|
||
let showTimes = $state(true);
|
||
let popupMessage = $state('');
|
||
let now = $state(Date.now());
|
||
let loadedAt = $state(Date.now());
|
||
let currentId = $state(untrack(() => data.live.current?.id ?? null));
|
||
let loadedRevision = $state(untrack(() => data.live.liveRevision));
|
||
let outputWindow: Window | null = null;
|
||
let progress = $derived(
|
||
data.live.totalCount ? Math.round((data.live.completedCount / data.live.totalCount) * 100) : 0
|
||
);
|
||
let currentActualSeconds = $derived(
|
||
(data.live.current?.actualDurationSeconds ?? 0) +
|
||
(data.live.current ? Math.max(0, Math.floor((now - loadedAt) / 1000)) : 0)
|
||
);
|
||
let sectionElapsedSeconds = $derived(
|
||
(data.live.sectionTimer?.elapsedSeconds ?? 0) +
|
||
(data.live.sectionTimer?.runningSinceUtc
|
||
? Math.max(0, Math.floor((now - loadedAt) / 1000))
|
||
: 0)
|
||
);
|
||
let sectionRemainingSeconds = $derived(
|
||
(data.live.sectionTimer?.plannedDurationSeconds ?? 0) - sectionElapsedSeconds
|
||
);
|
||
|
||
$effect(() => {
|
||
const nextCurrentId = data.live.current?.id ?? null;
|
||
if (nextCurrentId !== currentId || data.live.liveRevision !== loadedRevision) {
|
||
currentId = nextCurrentId;
|
||
loadedRevision = data.live.liveRevision;
|
||
loadedAt = Date.now();
|
||
}
|
||
});
|
||
|
||
onMount(() => {
|
||
const key = `sendewerk-live-${data.live.show.id}`;
|
||
try {
|
||
const settings = JSON.parse(localStorage.getItem(key) || '{}');
|
||
if (typeof settings.promptSize === 'number') promptSize = settings.promptSize;
|
||
if (typeof settings.showRail === 'boolean') showRail = settings.showRail;
|
||
if (typeof settings.showTimes === 'boolean') showTimes = settings.showTimes;
|
||
if (typeof settings.scrollY === 'number')
|
||
requestAnimationFrame(() => scrollTo(0, settings.scrollY));
|
||
} catch {
|
||
localStorage.removeItem(key);
|
||
}
|
||
const interval = setInterval(() => (now = Date.now()), 1000);
|
||
const persistScroll = () => persistSettings();
|
||
addEventListener('scroll', persistScroll, { passive: true });
|
||
return () => {
|
||
clearInterval(interval);
|
||
removeEventListener('scroll', persistScroll);
|
||
};
|
||
});
|
||
|
||
function persistSettings() {
|
||
localStorage.setItem(
|
||
`sendewerk-live-${data.live.show.id}`,
|
||
JSON.stringify({ promptSize, showRail, showTimes, scrollY })
|
||
);
|
||
}
|
||
|
||
function changePromptSize(delta: number) {
|
||
promptSize = Math.min(2.4, Math.max(1.1, promptSize + delta));
|
||
persistSettings();
|
||
}
|
||
|
||
function toggleRail() {
|
||
showRail = !showRail;
|
||
persistSettings();
|
||
}
|
||
|
||
function toggleTimes() {
|
||
showTimes = !showTimes;
|
||
persistSettings();
|
||
}
|
||
|
||
function formatCountdown(totalSeconds: number) {
|
||
const hours = Math.floor(totalSeconds / 3600);
|
||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||
const seconds = Math.floor(totalSeconds % 60);
|
||
return hours > 0
|
||
? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||
: `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
function liveAction(name: string) {
|
||
return `?show=${encodeURIComponent(data.live.show.id)}&/${name}`;
|
||
}
|
||
|
||
const enhanceCommand: SubmitFunction = ({ formElement, formData, submitter }) => {
|
||
const command = formElement.elements.namedItem('commandId');
|
||
if (command instanceof HTMLInputElement) {
|
||
command.value = crypto.randomUUID();
|
||
formData.set('commandId', command.value);
|
||
}
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = true;
|
||
return async ({ update }) => {
|
||
await update({ reset: false, invalidateAll: true });
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = false;
|
||
};
|
||
};
|
||
|
||
const enhanceOutput: SubmitFunction = ({ submitter }) => {
|
||
popupMessage = '';
|
||
outputWindow = window.open('about:blank', 'heinzlive-output');
|
||
if (!outputWindow) popupMessage = 'Popup blockiert. Bitte Popups erlauben und erneut öffnen.';
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = true;
|
||
return async ({ result, update }) => {
|
||
if (result.type === 'success') {
|
||
const response = result.data as { outputUrl: string };
|
||
if (outputWindow && !outputWindow.closed) {
|
||
outputWindow.location.href = response.outputUrl;
|
||
popupMessage = 'Präsentationsfenster verbunden.';
|
||
} else {
|
||
popupMessage = `Präsentationsfenster konnte nicht geöffnet werden: ${response.outputUrl}`;
|
||
}
|
||
} else {
|
||
outputWindow?.close();
|
||
}
|
||
await update({ reset: false, invalidateAll: false });
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = false;
|
||
};
|
||
};
|
||
</script>
|
||
|
||
<svelte:head><title>Live | {data.live.show.title} | Sendewerk</title></svelte:head>
|
||
|
||
<div class="page-wrap live-page live-controller">
|
||
<header class="live-header">
|
||
<div>
|
||
<p class="eyebrow"><span class="live-pulse"></span> Live-Steuerung</p>
|
||
<h1>{data.live.show.title}</h1>
|
||
<p class="live-state-line">
|
||
{data.live.startedAtUtc
|
||
? 'Live-Stand wird automatisch gespeichert.'
|
||
: 'Noch nicht gestartet.'}
|
||
</p>
|
||
</div>
|
||
<div class="live-header-actions">
|
||
<div class="font-control" aria-label="Prompter-Schriftgröße">
|
||
<button
|
||
type="button"
|
||
onclick={() => changePromptSize(-0.1)}
|
||
aria-label="Schrift verkleinern">A−</button
|
||
>
|
||
<button type="button" onclick={() => changePromptSize(0.1)} aria-label="Schrift vergrößern"
|
||
>A+</button
|
||
>
|
||
</div>
|
||
<button type="button" class="text-button" aria-pressed={!showTimes} onclick={toggleTimes}
|
||
>{showTimes ? 'Zeiten ausblenden' : 'Zeiten einblenden'}</button
|
||
>
|
||
<button type="button" class="text-button" aria-pressed={!showRail} onclick={toggleRail}
|
||
>{showRail ? 'Ablauf ausblenden' : 'Ablauf einblenden'}</button
|
||
>
|
||
<a href={resolve('/sendungen/[id]', { id: data.live.show.id })}>Zur Planung</a>
|
||
</div>
|
||
</header>
|
||
|
||
{#if form?.error}<div class="form-message error" role="alert">{form.error}</div>{/if}
|
||
{#if popupMessage}<div class="form-message" role="status">{popupMessage}</div>{/if}
|
||
|
||
<section class="live-toolbar" aria-label="Live-Sitzung">
|
||
<form method="POST" action={liveAction('start')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="primary-button" type="submit">
|
||
{data.live.startedAtUtc ? 'Live-Stand wiederaufnehmen' : 'Sendung starten'}
|
||
</button>
|
||
</form>
|
||
<form method="POST" action={liveAction('createOutput')} use:enhance={enhanceOutput}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<button class="secondary-button" type="submit">Ausgabe verbinden und öffnen</button>
|
||
</form>
|
||
<details class="live-output-settings">
|
||
<summary>Ausgabekanal</summary>
|
||
<div>
|
||
<form method="POST" action={liveAction('rotateOutput')} use:enhance={enhanceOutput}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<button class="text-button" type="submit">Schlüssel widerrufen und erneuern</button>
|
||
</form>
|
||
<form method="POST" action={liveAction('clearOutput')} use:enhance={enhanceCommand}>
|
||
<button class="text-button danger-text" type="submit">Ausgabekanal leeren</button>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
<div class="live-session-summary">
|
||
<strong>{data.live.completedCount} / {data.live.totalCount}</strong>
|
||
<span>{progress}% bearbeitet</span>
|
||
</div>
|
||
</section>
|
||
|
||
<div class:without-rail={!showRail} class="live-grid">
|
||
{#if data.live.current}<main
|
||
class="live-current"
|
||
style={`--prompt-size: ${promptSize}rem`}
|
||
aria-labelledby="current-title"
|
||
>
|
||
{#if data.live.sectionTimer?.sectionId === data.live.current.sectionId}
|
||
<section
|
||
class:warning={sectionRemainingSeconds <= 120 && sectionRemainingSeconds >= 0}
|
||
class:overrun={sectionRemainingSeconds < 0}
|
||
class="section-countdown"
|
||
aria-live="off"
|
||
>
|
||
<span>{data.live.sectionTimer.sectionName} · Abschnitt</span>
|
||
<strong
|
||
>{sectionRemainingSeconds < 0
|
||
? `Überzogen +${formatCountdown(Math.abs(sectionRemainingSeconds))}`
|
||
: `Noch ${formatCountdown(sectionRemainingSeconds)}`}</strong
|
||
>
|
||
</section>
|
||
{/if}
|
||
<div class="live-kicker">
|
||
<span>Aktuell · {data.live.current.sectionName}</span>
|
||
{#if showTimes}<strong
|
||
>{formatClock(currentActualSeconds)} / {formatClock(
|
||
data.live.current.durationSeconds
|
||
)}</strong
|
||
>{/if}
|
||
</div>
|
||
<h2 id="current-title">{data.live.current.title}</h2>
|
||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||
<div class="prompter-copy">{@html data.live.current.introHtml}</div>
|
||
{#if data.live.current.moderationHtml}<section class="moderation-card">
|
||
<span>Moderationsstichpunkte</span>
|
||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||
<div class="live-markdown">{@html data.live.current.moderationHtml}</div>
|
||
</section>{/if}
|
||
<details class="research-details">
|
||
<summary>Manuelle Recherche öffnen</summary>
|
||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||
<div class="live-markdown">{@html data.live.current.researchHtml}</div>
|
||
</details>
|
||
{#if data.live.current.sources.length || data.live.current.tools.length}
|
||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||
<section class="live-materials" aria-labelledby="live-materials-title">
|
||
<div>
|
||
<span>Nur Redaktion</span>
|
||
<h3 id="live-materials-title">Materialien</h3>
|
||
</div>
|
||
{#if data.live.current.sources.length}<div class="live-material-group">
|
||
<strong>Quellen</strong>
|
||
<ul>
|
||
{#each data.live.current.sources as source (source.id)}<li>
|
||
<!-- The URL is a server-validated private HTTP(S) snapshot. -->
|
||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||
<a href={source.url} target="_blank" rel="noopener noreferrer"
|
||
>{source.label}</a
|
||
>{#if source.timecodeSeconds !== null}<small
|
||
>ab {formatClock(source.timecodeSeconds)}</small
|
||
>{/if}
|
||
</li>{/each}
|
||
</ul>
|
||
</div>{/if}
|
||
{#if data.live.current.tools.length}<div class="live-material-group">
|
||
<strong>Werkzeuge</strong>
|
||
<ul>
|
||
{#each data.live.current.tools as tool (tool.id)}<li>
|
||
{#if tool.workUrl || tool.startUrl}<!-- The URL is a server-validated private HTTP(S) snapshot. -->
|
||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||
<a
|
||
href={tool.workUrl || tool.startUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer">{tool.name}</a
|
||
>{:else}<span>{tool.name}</span><small>Kein Link hinterlegt</small>{/if}
|
||
{#if tool.note}<small>{tool.note}</small>{/if}
|
||
</li>{/each}
|
||
</ul>
|
||
</div>{/if}
|
||
</section>
|
||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||
{/if}
|
||
{#if data.live.current.transitionHtml}<section class="transition-card">
|
||
<span>Überleitung</span>
|
||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||
<div class="live-markdown">{@html data.live.current.transitionHtml}</div>
|
||
</section>{/if}
|
||
|
||
<div class="live-current-actions">
|
||
<form method="POST" action={liveAction('back')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="secondary-button" type="submit">Zurück</button>
|
||
</form>
|
||
<form method="POST" action={liveAction('present')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="usageId" value={data.live.current.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="secondary-button" type="submit">
|
||
{data.live.presentationUsageId === data.live.current.id
|
||
? 'Erneut präsentieren'
|
||
: 'Präsentieren'}
|
||
</button>
|
||
</form>
|
||
{#if data.live.presentationUsageId}<form
|
||
method="POST"
|
||
action={liveAction('present')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="usageId" value="" />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="text-button" type="submit">Präsentation leeren</button>
|
||
</form>{/if}
|
||
<form method="POST" action={liveAction('skip')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="text-button" type="submit">Überspringen</button>
|
||
</form>
|
||
<form method="POST" action={liveAction('complete')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="primary-button" type="submit">Erledigen und weiter</button>
|
||
</form>
|
||
</div>
|
||
</main>{:else}<main class="live-current live-empty">
|
||
<p class="eyebrow">Kein aktives Piece</p>
|
||
<h2>{data.live.totalCount ? 'Live-Stand bereit' : 'Noch kein Piece eingeplant'}</h2>
|
||
<p>
|
||
{data.live.totalCount
|
||
? 'Sendung starten oder einen abgeschlossenen Schritt zurückgehen.'
|
||
: 'In der Planung kann zuerst ein sendefertiges Piece hinzugefügt werden.'}
|
||
</p>
|
||
{#if data.live.completedCount}<form
|
||
method="POST"
|
||
action={liveAction('back')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button class="secondary-button" type="submit">Zum letzten Piece zurück</button>
|
||
</form>{/if}
|
||
</main>{/if}
|
||
|
||
{#if showRail}<aside class="live-rail">
|
||
<section class="next-card">
|
||
<span>Als Nächstes</span>
|
||
{#if data.live.next}
|
||
<h3>{data.live.next.title}</h3>
|
||
<p>
|
||
{data.live.next.sectionName}{#if showTimes}
|
||
· {formatClock(data.live.next.durationSeconds)}{/if}
|
||
</p>
|
||
{:else}
|
||
<h3>Sendungsende</h3>
|
||
<p>Kein weiteres offenes Piece eingeplant.</p>
|
||
{/if}
|
||
</section>
|
||
|
||
<section class="live-progress">
|
||
<header>
|
||
<span>Ablauf</span><strong>{data.live.completedCount} / {data.live.totalCount}</strong>
|
||
</header>
|
||
<div class="progress-track"><span style={`width:${progress}%`}></span></div>
|
||
{#each data.live.sections as section, sectionIndex (section.id)}
|
||
<div class:active={section.id === data.live.current?.sectionId} class="rail-section">
|
||
<span>{String(sectionIndex + 1).padStart(2, '0')}</span>
|
||
<div>
|
||
<strong>{section.name}</strong>
|
||
{#if showTimes}<small>{formatDuration(section.durationSeconds)}</small>{/if}
|
||
</div>
|
||
</div>
|
||
{#each section.usages as usage, usageIndex (usage.id)}
|
||
<div class:current={usage.id === data.live.current?.id} class="live-run-item">
|
||
<div>
|
||
<strong>{usage.title}</strong><small>{usage.status}</small>
|
||
</div>
|
||
<div class="live-run-actions">
|
||
<form method="POST" action={liveAction('select')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button
|
||
class="text-button"
|
||
type="submit"
|
||
disabled={!data.live.startedAtUtc || usage.id === data.live.current?.id}
|
||
aria-label={`${usage.title} als aktuelles Piece auswählen`}>Aktuell</button
|
||
>
|
||
</form>
|
||
{#if usage.status === 'open'}
|
||
<form
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value={section.id} />
|
||
<input type="hidden" name="position" value={usageIndex - 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={usageIndex === 0 ||
|
||
section.usages[usageIndex - 1]?.status !== 'open'}
|
||
aria-label={`${usage.title} nach oben`}>↑</button
|
||
>
|
||
</form>
|
||
<form
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value={section.id} />
|
||
<input type="hidden" name="position" value={usageIndex + 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={usageIndex === section.usages.length - 1 ||
|
||
section.usages[usageIndex + 1]?.status !== 'open'}
|
||
aria-label={`${usage.title} nach unten`}>↓</button
|
||
>
|
||
</form>
|
||
{/if}
|
||
<form method="POST" action={liveAction('present')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||
<input type="hidden" name="commandId" value={data.commandId} />
|
||
<button type="submit" aria-label={`${usage.title} präsentieren`}>↗</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
{/each}
|
||
</section>
|
||
|
||
<section class="live-reserve">
|
||
<header><span>Reserve</span><strong>{data.live.reserve.length}</strong></header>
|
||
{#each data.live.reserve as usage, reserveIndex (usage.id)}
|
||
<div class="live-reserve-item">
|
||
<div>
|
||
<strong>{usage.title}</strong>{#if showTimes}<small
|
||
>{formatClock(usage.durationSeconds)}</small
|
||
>{/if}
|
||
</div>
|
||
<div class="live-run-actions">
|
||
<form method="POST" action={liveAction('moveUsage')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value="" />
|
||
<input type="hidden" name="position" value={reserveIndex - 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={reserveIndex === 0}
|
||
aria-label={`${usage.title} in Reserve nach oben`}>↑</button
|
||
>
|
||
</form>
|
||
<form method="POST" action={liveAction('moveUsage')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value="" />
|
||
<input type="hidden" name="position" value={reserveIndex + 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={reserveIndex === data.live.reserve.length - 1}
|
||
aria-label={`${usage.title} in Reserve nach unten`}>↓</button
|
||
>
|
||
</form>
|
||
{#if data.live.current || data.live.sections[0]}<form
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input
|
||
type="hidden"
|
||
name="sectionId"
|
||
value={data.live.current?.sectionId ?? data.live.sections[0]?.id}
|
||
/>
|
||
<input
|
||
type="hidden"
|
||
name="position"
|
||
value={(data.live.current?.position ?? -1) + 1}
|
||
/>
|
||
<button class="text-button" type="submit">Als Nächstes</button>
|
||
</form>{/if}
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
{#if !data.live.reserve.length}<p>Keine Pieces in Reserve.</p>{/if}
|
||
</section>
|
||
</aside>{/if}
|
||
</div>
|
||
</div>
|