wow a scoring page! sure hope that the endpoint exists... oh

This commit is contained in:
2026-05-30 17:29:18 +01:00
parent 10010631f5
commit d1abc83074

View File

@@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import { flip } from 'svelte/animate';
let { params }: PageProps = $props(); let { params }: PageProps = $props();
function ordinal(n: number) { function ordinal(n: number) {
@@ -28,65 +30,238 @@
} }
let eventId = params.eventId; let eventId = params.eventId;
let eventEndpoint: EventSource; let eventEndpoint: EventSource;
async function getEventData() { type Player = { firstName: string; lastName: string; teamColor: string; [key: string]: any };
let response = await fetch('/api/registeredEvents', { type Bracket = { name: string; items: Player[]; [key: string]: any };
method: 'POST',
body: JSON.stringify({ let event = $state<any>(null);
eventId: eventId let brackets = $state<Bracket[]>([]);
}), let loading = $state(true);
headers: { let dropTarget = $state<{ bi: number; pi: number } | null>(null);
'Content-type': 'application/json; charset=UTF-8' let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
}
}); // Drag state
return response.json(); let dragSrc = $state<{ bi: number; pi: number } | null>(null);
let sortByScore = $state(true);
let numResults = $derived(event?.resultPresets[0]?.numberOfResults ?? 2);
let useAverage = $derived(event?.resultPresets[0]?.averageResults ?? true);
let pendingScores = $state<Record<string, string[]>>({});
let committedScores = $state<Record<string, (number | null)[]>>({});
function average(scores: (number | null)[]): number {
const valid = scores.filter((s): s is number => s !== null);
if (valid.length === 0) return Infinity;
return valid.reduce((a, b) => a + b, 0) / valid.length;
} }
let eventDataPromise = getEventData(); function best(scores: (number | null)[]): number {
const valid = scores.filter((s): s is number => s !== null);
if (valid.length === 0) return Infinity;
return Math.min(...valid);
}
let displayBrackets = $derived(
brackets.map((bracket) => ({
...bracket,
items: sortByScore
? [...bracket.items].sort((a, b) => {
const sa = useAverage
? average(committedScores[a.id] ?? [])
: best(committedScores[a.id] ?? []);
const sb = useAverage
? average(committedScores[b.id] ?? [])
: best(committedScores[b.id] ?? []);
return sa - sb;
})
: bracket.items
}))
);
onMount(async () => {
const response = await fetch('/api/registeredEvents', {
method: 'POST',
body: JSON.stringify({ eventId }),
headers: { 'Content-type': 'application/json; charset=UTF-8' }
});
const data = await response.json();
console.log(data);
event = data[0];
brackets = data[0].registeredPlayers.map((b: any) => ({
...b,
items: [...b.items]
}));
loading = false;
onMount(() => {
eventEndpoint = new EventSource('/api/registeredEvents'); eventEndpoint = new EventSource('/api/registeredEvents');
// eventEndpoint.onmessage = (e) => {
// const eventData = JSON.parse(e.data);
// console.log(eventData);
// };
}); });
onDestroy(() => {
eventEndpoint?.close();
});
function onDragStart(bi: number, pi: number) {
dragSrc = { bi, pi };
}
function onDragOver(e: DragEvent, bi: number, pi: number) {
if (dragSrc?.bi === bi) {
e.preventDefault();
dropTarget = { bi, pi };
}
}
function onDrop(bi: number, pi: number) {
if (!dragSrc || dragSrc.bi !== bi) return;
const [src] = brackets[dragSrc.bi].items.splice(dragSrc.pi, 1);
brackets[bi].items.splice(pi, 0, src);
brackets = [...brackets];
dragSrc = null;
dropTarget = null;
}
function onDragEnd() {
dragSrc = null;
dropTarget = null;
}
async function submitResults() {
submitStatus = 'submitting';
try {
const res = await fetch('/api/eventResults', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventId,
brackets: brackets.map((b) => ({
name: b.name,
players: b.items.map((p, i) => ({ ...p, position: i + 1 }))
}))
})
});
submitStatus = res.ok ? 'done' : 'error';
} catch {
submitStatus = 'error';
}
}
</script> </script>
{#await eventDataPromise} {#if loading}
<div>loading</div> <div>loading</div>
{:then eventData} {:else}
{@const event = eventData[0]}
<div class="flex justify-center"> <div class="flex justify-center">
<div class="w-full flex-col px-[5vw] text-center"> <div class="w-full flex-col px-[5vw] text-center">
{console.log(event)}
<div class="align-text-middle h-10 w-full bg-red-500"> <div class="align-text-middle h-10 w-full bg-red-500">
{event.name} - {event.division} - scoring {event.name} - {event.division} - scoring
</div> </div>
<button onclick={() => (sortByScore = !sortByScore)}>
{sortByScore ? 'Sort: Score' : 'Sort: Manual'}
</button>
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
{#each event.registeredPlayers as bracket, bi} <!-- {#each some as some} -->
<!-- {/each} -->
{#each displayBrackets as bracket, bi}
<div class="w-full min-w-0"> <div class="w-full min-w-0">
<div class="brackets-name text-bold"> <div class="brackets-name text-bold">
<span class="brackets-name-text">{bracket.name}</span> <span class="brackets-name-text" role="listbox" aria-label={bracket.name}
<div class=""></div> >{bracket.name}</span
>
</div> </div>
{#each bracket.items as player} {#each bracket.items as player, pi (player.id)}
<div class="" style="color:{player.teamColor}"> {@const playerScores = committedScores[player.id] ?? []}
<div class="player-name-wrap" use:marquee> {@const validScores = playerScores.filter((s): s is number => s !== null)}
{player.firstName} {@const avgScore =
{player.lastName} validScores.length > 0
? validScores.reduce((a, b) => a + b, 0) / validScores.length
: 0}
<div
animate:flip={{ duration: 300 }}
class="scoring-player-card"
style="--player-color:{player.teamColor}"
>
<div
class="player-name-wrap cursor-grab active:cursor-grabbing"
class:opacity-50={dragSrc?.bi === bi && dragSrc?.pi === pi}
style="color:{player.teamColor}"
role="option"
aria-selected={false}
draggable="true"
tabindex="0"
ondragstart={() => onDragStart(bi, pi)}
class:drop-target={dropTarget?.bi === bi && dropTarget?.pi === pi}
ondragover={(e) => onDragOver(e, bi, pi)}
ondrop={() => onDrop(bi, pi)}
ondragend={onDragEnd}
>
<div use:marquee>
{player.firstName}
{player.lastName}
</div>
<div class="result-input-containers flex flex-col">
{#each Array.from({ length: numResults }, (_, i) => i) as run}
<input
type="number"
placeholder="run {run + 1}"
value={pendingScores[player.id]?.[run] ?? ''}
oninput={(e) => {
const current = [
...(pendingScores[player.id] ?? Array(numResults).fill(''))
];
current[run] = e.currentTarget.value;
pendingScores[player.id] = current;
}}
onblur={(e) => {
const val = parseFloat(e.currentTarget.value);
const current = [
...(committedScores[player.id] ?? Array(numResults).fill(null))
];
current[run] = isNaN(val) ? null : val;
committedScores[player.id] = current;
}}
/>
{/each}
{#if event.resultPresets[0].averageResults == 1}{/if}
</div>
</div>
<div class="text-sm opacity-60">
avg: {avgScore.toFixed(2)}
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
{/each} {/each}
</div> </div>
<button
class="mt-4 rounded px-6 py-2 text-white"
class:bg-green-600={submitStatus === 'idle'}
class:bg-gray-400={submitStatus === 'submitting'}
class:bg-blue-600={submitStatus === 'done'}
class:bg-red-600={submitStatus === 'error'}
disabled={submitStatus === 'submitting' || submitStatus === 'done'}
onclick={submitResults}
>
{#if submitStatus === 'idle'}Submit Results
{:else if submitStatus === 'submitting'}Submitting…
{:else if submitStatus === 'done'}Submitted ✓
{:else}Error — Retry{/if}
</button>
</div> </div>
</div> </div>
{/await} {/if}
<style> <style>
.drop-target {
outline: 2px dotted currentColor;
}
.scoring-player-card {
margin: 5px;
background: color-mix(in srgb, var(--player-color) 10%, transparent);
}
</style> </style>