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`);
|
if (!presetId) throw new Error(`Team "${row.resultPreset}" not found`);
|
||||||
await db.insert(schema.eventTypes).values({
|
await db.insert(schema.eventTypes).values({
|
||||||
name: row.event_name,
|
name: row.event_name,
|
||||||
preset: row.preset,
|
scoringPreset: row.preset,
|
||||||
resultPreset: presetId
|
resultPreset: presetId
|
||||||
});
|
});
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export async function getRegisteredEvents(eventId?: number) {
|
|||||||
return teamInfo.teams[0];
|
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
|
const allEvents = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.registeredEventsView)
|
.from(schema.registeredEventsView)
|
||||||
@@ -51,6 +59,7 @@ export async function getRegisteredEvents(eventId?: number) {
|
|||||||
state: events.state,
|
state: events.state,
|
||||||
completed: events.timeCompleted || 0,
|
completed: events.timeCompleted || 0,
|
||||||
resultPreset: events.resultPreset,
|
resultPreset: events.resultPreset,
|
||||||
|
scoringPreset: events.scorePreset ? await getScoringPreset(events.scorePreset) : 'UNDECIDED',
|
||||||
winner: events.winner ? await getWinnerInfo(events.winner) : 'UNDECIDED'
|
winner: events.winner ? await getWinnerInfo(events.winner) : 'UNDECIDED'
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const resultPresets = sqliteTable('resultPresets', {
|
|||||||
export const eventTypes = sqliteTable('eventTypes', {
|
export const eventTypes = sqliteTable('eventTypes', {
|
||||||
id: integer('eventTypes_id').primaryKey({ autoIncrement: true }),
|
id: integer('eventTypes_id').primaryKey({ autoIncrement: true }),
|
||||||
name: text('event_name').notNull(),
|
name: text('event_name').notNull(),
|
||||||
preset: integer('preset')
|
scoringPreset: integer('preset')
|
||||||
.references(() => scoringPresets.presetID)
|
.references(() => scoringPresets.presetID)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
resultPreset: integer('result_preset')
|
resultPreset: integer('result_preset')
|
||||||
@@ -133,6 +133,7 @@ export const registeredEventsView = sqliteView('registeredEventsView').as((qb) =
|
|||||||
state: registeredEvents.state,
|
state: registeredEvents.state,
|
||||||
timeCompleted: registeredEvents.timeCompleted,
|
timeCompleted: registeredEvents.timeCompleted,
|
||||||
winner: registeredEvents.teamWinner,
|
winner: registeredEvents.teamWinner,
|
||||||
|
scorePreset: eventTypes.scoringPreset,
|
||||||
resultPreset: eventTypes.resultPreset
|
resultPreset: eventTypes.resultPreset
|
||||||
})
|
})
|
||||||
.from(registeredEvents)
|
.from(registeredEvents)
|
||||||
|
|||||||
@@ -7,4 +7,17 @@
|
|||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<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()}
|
{@render children()}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.header {
|
||||||
|
background-color: var(--ctp-mocha-crust);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|
||||||
|
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',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ eventId }),
|
||||||
eventId: eventId
|
headers: { 'Content-type': 'application/json; charset=UTF-8' }
|
||||||
}),
|
|
||||||
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 = 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)}
|
||||||
|
{@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.firstName}
|
||||||
{player.lastName}
|
{player.lastName}
|
||||||
</div>
|
</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>
|
||||||
{/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