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">
import { onMount, onDestroy } from 'svelte';
import type { PageProps } from './$types';
import { flip } from 'svelte/animate';
let { params }: PageProps = $props();
function ordinal(n: number) {
@@ -28,65 +30,238 @@
}
let eventId = params.eventId;
let eventEndpoint: EventSource;
async function getEventData() {
let response = await fetch('/api/registeredEvents', {
type Player = { firstName: string; lastName: string; teamColor: string; [key: string]: any };
type Bracket = { name: string; items: Player[]; [key: string]: any };
let event = $state<any>(null);
let brackets = $state<Bracket[]>([]);
let loading = $state(true);
let dropTarget = $state<{ bi: number; pi: number } | null>(null);
let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
// Drag state
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;
}
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: eventId
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
body: JSON.stringify({ eventId }),
headers: { 'Content-type': 'application/json; charset=UTF-8' }
});
return response.json();
}
const data = await response.json();
console.log(data);
event = data[0];
brackets = data[0].registeredPlayers.map((b: any) => ({
...b,
items: [...b.items]
}));
loading = false;
let eventDataPromise = getEventData();
onMount(() => {
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>
{#await eventDataPromise}
{#if loading}
<div>loading</div>
{:then eventData}
{@const event = eventData[0]}
{:else}
<div class="flex justify-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">
{event.name} - {event.division} - scoring
</div>
<button onclick={() => (sortByScore = !sortByScore)}>
{sortByScore ? 'Sort: Score' : 'Sort: Manual'}
</button>
<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="brackets-name text-bold">
<span class="brackets-name-text">{bracket.name}</span>
<div class=""></div>
<span class="brackets-name-text" role="listbox" aria-label={bracket.name}
>{bracket.name}</span
>
</div>
{#each bracket.items as player}
<div class="" style="color:{player.teamColor}">
<div class="player-name-wrap" use:marquee>
{#each bracket.items as player, pi (player.id)}
{@const playerScores = committedScores[player.id] ?? []}
{@const validScores = playerScores.filter((s): s is number => s !== null)}
{@const avgScore =
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>
{/each}
</div>
{/each}
</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>
{/await}
{/if}
<style>
.drop-target {
outline: 2px dotted currentColor;
}
.scoring-player-card {
margin: 5px;
background: color-mix(in srgb, var(--player-color) 10%, transparent);
}
</style>