Compare commits
3 Commits
685fba8c71
...
d1abc83074
| Author | SHA1 | Date | |
|---|---|---|---|
| d1abc83074 | |||
| 10010631f5 | |||
| a44ee668c8 |
@@ -132,7 +132,7 @@ async function seed() {
|
||||
if (!presetId) throw new Error(`Team "${row.resultPreset}" not found`);
|
||||
await db.insert(schema.eventTypes).values({
|
||||
name: row.event_name,
|
||||
preset: row.preset,
|
||||
scoringPreset: row.preset,
|
||||
resultPreset: presetId
|
||||
});
|
||||
console.log(
|
||||
|
||||
@@ -38,6 +38,14 @@ export async function getRegisteredEvents(eventId?: number) {
|
||||
return teamInfo.teams[0];
|
||||
}
|
||||
|
||||
async function getScoringPreset(presetId: number) {
|
||||
const presets = await db
|
||||
.select()
|
||||
.from(schema.scoringPresets)
|
||||
.where(presetId ? eq(schema.scoringPresets.presetID, presetId) : undefined);
|
||||
return presets;
|
||||
}
|
||||
|
||||
const allEvents = await db
|
||||
.select()
|
||||
.from(schema.registeredEventsView)
|
||||
@@ -51,6 +59,7 @@ export async function getRegisteredEvents(eventId?: number) {
|
||||
state: events.state,
|
||||
completed: events.timeCompleted || 0,
|
||||
resultPreset: events.resultPreset,
|
||||
scoringPreset: events.scorePreset ? await getScoringPreset(events.scorePreset) : 'UNDECIDED',
|
||||
winner: events.winner ? await getWinnerInfo(events.winner) : 'UNDECIDED'
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -46,7 +46,7 @@ export const resultPresets = sqliteTable('resultPresets', {
|
||||
export const eventTypes = sqliteTable('eventTypes', {
|
||||
id: integer('eventTypes_id').primaryKey({ autoIncrement: true }),
|
||||
name: text('event_name').notNull(),
|
||||
preset: integer('preset')
|
||||
scoringPreset: integer('preset')
|
||||
.references(() => scoringPresets.presetID)
|
||||
.notNull(),
|
||||
resultPreset: integer('result_preset')
|
||||
@@ -133,6 +133,7 @@ export const registeredEventsView = sqliteView('registeredEventsView').as((qb) =
|
||||
state: registeredEvents.state,
|
||||
timeCompleted: registeredEvents.timeCompleted,
|
||||
winner: registeredEvents.teamWinner,
|
||||
scorePreset: eventTypes.scoringPreset,
|
||||
resultPreset: eventTypes.resultPreset
|
||||
})
|
||||
.from(registeredEvents)
|
||||
|
||||
@@ -7,4 +7,17 @@
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
|
||||
<div class="header goldman align-center flex h-15 w-full">
|
||||
<a
|
||||
class="align-text-middle mx-3 my-1 h-auto content-center rounded-sm border-2 border-solid border-red-500 px-2"
|
||||
href="/">home</a
|
||||
>
|
||||
</div>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<style>
|
||||
.header {
|
||||
background-color: var(--ctp-mocha-crust);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user