Files
score-system/src/routes/event/[eventId]/+page.svelte
2026-07-07 22:31:54 +01:00

137 lines
4.2 KiB
Svelte

<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import type { PageProps } from './$types';
let { params, data }: PageProps = $props();
function ordinal(n: number) {
const s = ['th', 'st', 'nd', 'rd'];
const v = n % 100;
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
}
function marquee(node: HTMLElement) {
function measure() {
const inner = node.querySelector<HTMLElement>('.marquee-inner');
if (!inner) return;
const overflow = inner.scrollWidth - node.clientWidth;
if (overflow > 2) {
node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
inner.classList.add('scrolling');
} else {
inner.classList.remove('scrolling');
}
}
measure();
const ro = new ResizeObserver(measure);
ro.observe(node);
return { destroy: () => ro.disconnect() };
}
let eventId = params.eventId;
let eventEndpoint: EventSource;
let eventData = $state<any>(data.event);
onMount(() => {
eventEndpoint = new EventSource('/api/registeredEvents');
eventEndpoint.onmessage = (e) => {
const allEvents = JSON.parse(e.data);
const updated = allEvents.find((ev: any) => ev.id == eventId);
if (updated) {
if (updated.registeredPlayers) {
updated.registeredPlayers.forEach((bracket: any) => {
bracket.items.sort((a: any, b: any) => {
if (a.placement === 0 && b.placement === 0) return 0;
if (a.placement === 0) return 1;
if (b.placement === 0) return -1;
return a.placement - b.placement;
});
});
}
eventData = updated;
}
};
});
onDestroy(() => {
eventEndpoint?.close();
});
</script>
{#if eventData}
<div class="page-container">
<div class="event-card" class:ongoing-event={eventData.state == 1} class:completed-event={eventData.state == 2}
style={eventData.state == 2 ? `--event-color: ${eventData.winner?.color ?? 'currentColor'}` : ''}>
<div class="event-header">
<span class="event-name goldman">{eventData.name}</span>
<span class="event-division">{eventData.division}</span>
{#if eventData.state == 1}
<span class="event-status goldman">ONGOING</span>
{:else if eventData.state == 2}
<span class="event-status goldman">FINISHED</span>
{/if}
</div>
<div class="brackets px-[5vw]">
{#each eventData.registeredPlayers as bracket, bi}
{#if bi > 0}
<div class="bracket-sep" aria-hidden="true"></div>
{/if}
<div class="bracket-row">
<div class="brackets-name flex items-center">
<span class="brackets-name-text align-text-middle">{bracket.name}</span>
<div class="bracket-vertical-sep"></div>
</div>
{#each bracket.items as player}
<div class="player-box" style="--c:{player.teamColor}">
<div class="player-ghost" aria-hidden="true">
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
<text
x="0"
y="0.7"
font-size="1"
dominant-baseline="auto"
textLength="100"
lengthAdjust="spacingAndGlyphs"
font-family="'Black Ops One',system-ui">{player.firstName}</text
>
</svg>
</div>
<div class="player-name-wrap" use:marquee>
<a href="/stats/player/{player.id}" class="marquee-inner">
{player.firstName}
{player.lastName}
</a>
</div>
{#if player.placement !== 0}
<div class="player-placement goldman">{ordinal(player.placement)}</div>
<div class="flex flex-col justify-center">
{#each player.playerScores as score}
<div class="mx-1 my-0.5 rounded border px-1 text-xs" style="border-color:{player.teamColor}">
Run {score.resultIndex + 1}: {score.result}{eventData.resultPresets[0].unit}
</div>
{/each}
</div>
{:else}
<div class="player-placement-gap"></div>
{/if}
</div>
{/each}
</div>
{/each}
</div>
</div>
{#if data.user}
<div class="mt-10 flex w-full justify-center">
<a
class="flex justify-center rounded border-2 border-solid border-white bg-ctp-surface2 p-4"
href="/event/scoring/{eventId}">Score This Event</a
>
</div>
{/if}
</div>
{:else}
<div class="page-container"><div>loading</div></div>
{/if}