wow a scoring page! sure hope that the endpoint exists... oh
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user