Files
nextup/web/src/routes/display/+page.svelte

300 lines
7.7 KiB
Svelte

<script lang="ts">
import { eventTarget, wsState } from "$lib/ws.svelte";
import { PersistedState } from "runed";
import { onMount } from "svelte";
import { fade } from "svelte/transition";
interface CallEntry {
num: string;
ticket: {
status: "called" | "completed" | "no-show";
room?: string;
};
}
let withSound = false;
let callAudioURI = "";
let persisted: PersistedState<CallEntry[]> = new PersistedState("calls", []);
let calls = $state<CallEntry[]>($state.snapshot(persisted.current));
let newCalls = $state<CallEntry[]>([]);
let firstLoad = true;
let connected = $state(false);
onMount(() => {
withSound = location.search.includes("sound=true");
eventTarget.addEventListener("display", (e) => {
const detail = (e as CustomEvent).detail;
if(detail.motd) {
const [speed, ...text] = detail.motd.split(" ");
marqueeText = text.join(" ");
marqueeSpeed = parseInt(speed);
updateMarqueeSpeed();
}
const _newCalls = detail.tickets.filter(
(call: CallEntry) => !calls.find((c) => c.num === call.num)
);
calls = detail.tickets ?? [];
persisted.current = calls;
connected = true;
if (!firstLoad) {
if (_newCalls.length > 0 && withSound && callAudioURI) {
const audio = new Audio(callAudioURI);
audio.play();
}
newCalls.push(..._newCalls);
setTimeout(() => {
newCalls = newCalls.filter((c) => _newCalls.includes(c));
}, 10000);
}
firstLoad = false;
});
eventTarget.dispatchEvent(
new CustomEvent("send", { detail: { type: "display" } })
);
if(withSound) {
eventTarget.addEventListener("call-audio", (e) => {
const detail = (e as CustomEvent).detail;
callAudioURI = detail.dataUri;
});
eventTarget.dispatchEvent(
new CustomEvent("send", { detail: { type: "call-audio" } })
);
}
});
function updateMarqueeSpeed() {
requestAnimationFrame(() => {
const marqueeContent = document.querySelectorAll(".marquee-content") as NodeListOf<HTMLElement>;
const speed = marqueeSpeed; // px per second
const width = marqueeContent[0].offsetWidth;
marqueeContent[0].style.animationDuration = `${width / speed}s`;
marqueeContent[1].style.animationDuration = `${width / speed}s`;
});
}
const ENTRIES: Record<string, number> = { A: 1, B: 2, C: 1, D: 2, E: 1 };
const ENTRIES_PER_TABLE = 10;
let marqueeSpeed = 50;
let marqueeText = $state("");
</script>
{#if newCalls.length > 0}
<div
class="fixed top-0 left-0 w-full h-full backdrop-blur-md z-50"
transition:fade
>
<div
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-9xl font-bold bg-background text-foreground p-4 rounded-md flex flex-col gap-10"
>
{#each newCalls as call (call.num)}
<span class="mx-4 flex items-center gap-10">
{call.num}
<svg
xmlns="http://www.w3.org/2000/svg"
width="80"
height="80"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-right-icon lucide-arrow-right"
><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg
>
{call.ticket.room}
</span>
{/each}
</div>
</div>
{/if}
<div class="marquee">
<div class="marquee-content">{marqueeText}</div>
<div class="marquee-content">{marqueeText}</div>
</div>
<div class="p-4 flex justify-around gap-4 text-5xl">
<!-- <h1 class="text-6xl font-bold">Aufruf</h1> -->
<table class="self-start">
<tbody>
<tr>
<th class="pr-4 font-bold">Warte Nr.</th>
<th class="pr-4 font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-right-icon lucide-arrow-right"
><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg
>
</th>
<th class="pr-4 font-bold">Raum</th>
</tr>
{#each calls.slice(0, ENTRIES_PER_TABLE) as call (call.num)}
<tr>
<td class="call">{call.num}</td>
<td class="call">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-right-icon lucide-arrow-right"
><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg
>
</td>
<td class="call"
>{call.ticket.status === "no-show"
? "Empfang " + ENTRIES[call.num[0]]
: call.ticket.room}</td
>
</tr>
{/each}
</tbody>
</table>
<div
class="fixed h-full w-1 top-15 left-1/2 -translate-x-1/2 {wsState.closed
? 'bg-red-800'
: !wsState.connected
? 'bg-amber-800'
: 'bg-black'}"
></div>
<table class="self-start">
<tbody>
<tr>
<th class="pr-4 font-bold">Warte Nr.</th>
<th class="pr-4 font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-right-icon lucide-arrow-right"
><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg
>
</th>
<th class="pr-4 font-bold">Raum</th>
</tr>
{#each calls.slice(ENTRIES_PER_TABLE) as call (call.num)}
<tr>
<td class="call">{call.num}</td>
<td class="call">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-arrow-right-icon lucide-arrow-right"
><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></svg
>
</td>
<td class="call"
>{call.ticket.status === "no-show"
? "Empfang " + ENTRIES[call.num[0]]
: call.ticket.room}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
<style>
.call {
animation: flash 5s ease-in-out;
animation-delay: 10s;
}
@keyframes flash {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
20% {
opacity: 0;
}
30% {
opacity: 1;
}
40% {
opacity: 0;
}
50% {
opacity: 1;
}
60% {
opacity: 0;
}
70% {
opacity: 1;
}
80% {
opacity: 0;
}
100% {
opacity: 1;
}
}
th,
td {
padding: 0.5rem 1rem;
text-align: center;
background-color: var(--accent);
border: 2px solid var(--background);
}
.marquee {
display: flex;
overflow: hidden;
width: 100vw;
white-space: nowrap;
font-size: 2.5rem;
gap: 4rem;
}
.marquee-content {
animation-iteration-count: infinite;
animation-name: marquee;
animation-timing-function: linear;
}
@keyframes marquee {
from {
transform: translateX(0%);
}
to {
transform: translateX(-100%);
}
}
</style>