Compare commits

...

4 Commits

Author SHA1 Message Date
62af341a1e very important changes 2026-07-03 11:10:01 +01:00
6019d67145 amended todo so that I have a plan 2026-07-03 08:07:17 +01:00
b7e060441c added unit to scoring page 2026-07-01 17:54:52 +01:00
3b964c4d9c player scores working yayyyy 2026-07-01 17:52:32 +01:00
10 changed files with 276 additions and 99 deletions

43
TODO.md
View File

@@ -1,17 +1,36 @@
# DATABASE # UI - Medium priority
record scores
load event scores from db into scoring view
# VIEWS
ledger view
player view - all registered events and scores for those events player view - all registered events and scores for those events
team view - all registered players and all points gained from them team view - all registered players and all points gained from them - Do i need this?
make event view nicer
# UI # Login stuff - HIGH PRIORITY
inserting into ledger disableable register button on the login page for end users
animations for all leaderboards
make a way to toggle on and off the register page make a way to toggle on and off the register page
protect the register endpoint?? (low prio)
# OPTIONAL # Ledger - HIGH PRIORITY
ledger view
inserting manually into ledger
# Database - HIGH PRIORITY
make events score based off of highest score instead of avg
load results from the excel sheet i made
export whole contest results to csv
# OPTIONAL (low priority)
chat thing chat thing
protect all the endpoints?????? (pain in the ass)
> probably need just in case, seems possible with one endpoint/interface
## Non-seeded data
manual registering of teams
live editing of event/score presets
manual registering of events
manual registering of players
result amendments
## Easy shit
animations for all leaderboards
Ongoing since... or ongoing for... on the main page
displaying averages of loaded scores when there's more than one (easy, low prio)
fix the layout on event view to match the homepage view
fix the marquee animation thing and make it global

View File

@@ -1,5 +1,5 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { eq } from 'drizzle-orm'; import { sql, eq, and } from 'drizzle-orm';
import * as schema from '$lib/server/db/schema'; import * as schema from '$lib/server/db/schema';
import { globalEmitter } from './globalEmitter'; import { globalEmitter } from './globalEmitter';
@@ -74,7 +74,6 @@ export async function startEvent(eventId: number) {
.from(schema.registeredEventsView) .from(schema.registeredEventsView)
.where(eq(schema.registeredEventsView.eventId, eventId)); .where(eq(schema.registeredEventsView.eventId, eventId));
let requestedEvent = event[0]; let requestedEvent = event[0];
console.log(requestedEvent);
if (requestedEvent.state != 0) { if (requestedEvent.state != 0) {
console.log('not startable'); console.log('not startable');
return false; return false;
@@ -84,26 +83,26 @@ export async function startEvent(eventId: number) {
.set({ state: 1 }) .set({ state: 1 })
.where(eq(schema.registeredEvents.id, requestedEvent.eventId)) .where(eq(schema.registeredEvents.id, requestedEvent.eventId))
.returning(); .returning();
console.log(replacedEvent);
globalEmitter.emit('eventUpdate'); globalEmitter.emit('eventUpdate');
return true; return true;
} }
} }
// Fetch all players registered for a specific event // Fetch all players registered for a specific event
export async function getAllRegisteredEventPlayers(eventId: number) { export async function getAllRegisteredEventPlayers(eventId: number, getScores?: boolean) {
const eventPlayers = await db const eventPlayers = await db
.select() .select()
.from(schema.registeredEventPlayersView) .from(schema.registeredEventPlayersView)
// Filter by event ID
.where(eq(schema.registeredEventPlayersView.eventId, eventId)) .where(eq(schema.registeredEventPlayersView.eventId, eventId))
.orderBy( .orderBy(
schema.registeredEventPlayersView.bracket, schema.registeredEventPlayersView.bracket,
schema.registeredEventPlayersView.placement, sql`CASE WHEN ${schema.registeredEventPlayersView.placement} = 0 THEN 999999 ELSE ${schema.registeredEventPlayersView.placement} END ASC`,
schema.registeredEventPlayersView.teamName schema.registeredEventPlayersView.teamName
); );
return {
eventPlayers: eventPlayers.map((players) => ({ // 1. Wrap the map in Promise.all and await it
const resolvedPlayers = await Promise.all(
eventPlayers.map(async (players) => ({
id: players.playerId, id: players.playerId,
firstName: players.firstName, firstName: players.firstName,
lastName: players.lastName, lastName: players.lastName,
@@ -114,8 +113,14 @@ export async function getAllRegisteredEventPlayers(eventId: number) {
eventName: players.eventName, eventName: players.eventName,
teamId: players.teamId, teamId: players.teamId,
teamName: players.teamName, teamName: players.teamName,
teamColor: players.teamColor teamColor: players.teamColor,
playerScores: getScores == true ? await getPlayerScores(players.playerId, eventId) : undefined
})) }))
);
// 2. Return the fully resolved data
return {
eventPlayers: resolvedPlayers
}; };
} }
@@ -128,8 +133,40 @@ export async function getAllBrackets() {
} }
export async function getPlayerInfo(playerId: number) { export async function getPlayerInfo(playerId: number) {
const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id), playerId); const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id, playerId));
return playerInfo; const teamInfo = await db
.select()
.from(schema.teams)
.where(eq(schema.teams.id, playerInfo[0].team));
return { ...playerInfo[0], teamInfo: teamInfo[0] };
}
export async function getPlayerScores(playerId: number, eventId?: number) {
const playerRegistrations = await getPlayerRegistrations(playerId, eventId);
let scoresObject: any[] = [];
for (let registration in playerRegistrations) {
let currentReg = playerRegistrations[registration];
let scores = await db
.select()
.from(schema.registeredResults)
.where(eq(schema.registeredResults.registeredPlayerId, currentReg.registeredPlayerId));
scoresObject.push(...scores);
}
return scoresObject;
}
export async function getPlayerRegistrations(playerId: number, eventId?: number) {
const playerRegistrations = await db
.select()
.from(schema.registeredEventPlayersView)
.where(
and(
eq(schema.registeredEventPlayersView.playerId, playerId),
eventId ? eq(schema.registeredEventPlayersView.eventId, eventId) : undefined
)
);
return playerRegistrations;
} }
export async function getResultPreset(presetId?: number) { export async function getResultPreset(presetId?: number) {
@@ -155,7 +192,10 @@ export async function getRegisteredEventsWithPlayers(eventId?: number) {
for (let registeredEvent in registeredEventList) { for (let registeredEvent in registeredEventList) {
let event = registeredEventList[registeredEvent]; let event = registeredEventList[registeredEvent];
let resultPreset = await getResultPreset(event.resultPreset); let resultPreset = await getResultPreset(event.resultPreset);
let registeredPlayers = await getAllRegisteredEventPlayers(event.id); let registeredPlayers = await getAllRegisteredEventPlayers(
event.id,
eventId != undefined ? true : undefined
);
// Group players by bracket category for the frontend // Group players by bracket category for the frontend
const bracketOrder = brackets.brackets.map((category) => { const bracketOrder = brackets.brackets.map((category) => {

View File

@@ -55,6 +55,19 @@ export async function POST({ request }: any) {
teamScores.set(currentPlayerTeam, currentTeamScore + score); teamScores.set(currentPlayerTeam, currentTeamScore + score);
} }
for (let result in currentPlayer.scores) {
let currentResult = currentPlayer.scores[result];
let newScoreEntry = await db
.insert(schema.registeredResults)
.values({
registeredPlayerId: currentPlayer.registeredPlayerId,
resultIndex: parseInt(result),
result: currentResult
})
.returning();
console.log(newScoreEntry);
}
// Update player placement in the database // Update player placement in the database
let newPlayerPlacement = await db let newPlayerPlacement = await db
.update(schema.registeredPlayers) .update(schema.registeredPlayers)

View File

@@ -0,0 +1,14 @@
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) return { user: null };
const [row] = await db
.select({ role: scorers.role })
.from(scorers)
.where(eq(scorers.id, locals.user.id));
return { user: { ...locals.user, role: row?.role ?? 'scorer' } };
};

View File

@@ -1,7 +1,7 @@
<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';
let { params }: PageProps = $props(); let { params, data }: PageProps = $props();
function ordinal(n: number) { function ordinal(n: number) {
const s = ['th', 'st', 'nd', 'rd']; const s = ['th', 'st', 'nd', 'rd'];
@@ -75,12 +75,21 @@
<div>loading</div> <div>loading</div>
{:then eventData} {:then eventData}
{@const event = eventData[0]} {@const event = eventData[0]}
{console.log(event)}
<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">
<div <div
style:background-color={event.state === 1
? 'color-mix(in srgb, #fe640b 18%, transparent)'
: event.state === 2
? 'color-mix(in srgb, #a6e3a1 18%, transparent)'
: ''}
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1" class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
> >
{event.name} - {event.division} {event.name} - {event.division}
{#if event.state == 1}- ONGOING
{:else if event.state == 2}- FINISHED
{/if}
</div> </div>
{#each event.registeredPlayers as bracket, bi} {#each event.registeredPlayers as bracket, bi}
{#if bi > 0} {#if bi > 0}
@@ -92,7 +101,7 @@
<div class="bracket-vertical-sep"></div> <div class="bracket-vertical-sep"></div>
</div> </div>
{#each bracket.items as player} {#each bracket.items as player}
<div class="player-box h-30" style="--c:{player.teamColor}"> <div class="player-box" style="--c:{player.teamColor}">
<div class="player-ghost" aria-hidden="true"> <div class="player-ghost" aria-hidden="true">
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg"> <svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
<text <text
@@ -114,6 +123,13 @@
</div> </div>
{#if player.placement !== 0} {#if player.placement !== 0}
<div class="player-placement goldman">{ordinal(player.placement)}</div> <div class="player-placement goldman">{ordinal(player.placement)}</div>
<div class="resultContainer flex justify-center">
{#each player.playerScores as score}
<div class="mx-5 my-1 rounded border-2" style="border-color:{player.teamColor}">
Run {score.resultIndex + 1}: {score.result}{event.resultPresets[0].unit}
</div>
{/each}
</div>
{:else} {:else}
<div class="player-placement-gap"></div> <div class="player-placement-gap"></div>
{/if} {/if}
@@ -123,10 +139,23 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="mt-10 flex w-full justify-center"> {#if data.user}
<a <div class="mt-10 flex w-full justify-center">
class="flex justify-center rounded border-2 border-solid border-white bg-ctp-surface2 p-4" <a
href="/event/scoring/{eventId}">Score This Event</a class="flex justify-center rounded border-2 border-solid border-white bg-ctp-surface2 p-4"
> href="/event/scoring/{eventId}">Score This Event</a
</div> >
</div>
{/if}
{/await} {/await}
<style>
.resultContainer {
flex-direction: column;
}
@media (max-width: 479px) {
.resultContainer {
flex-direction: column;
}
}
</style>

View File

@@ -126,7 +126,7 @@
headers: { 'Content-type': 'application/json; charset=UTF-8' } headers: { 'Content-type': 'application/json; charset=UTF-8' }
}); });
const data = await response.json(); const data = await response.json();
console.log(data); console.log(data[0]);
event = data[0]; event = data[0];
brackets = data[0].registeredPlayers.map((b: any) => ({ brackets = data[0].registeredPlayers.map((b: any) => ({
...b, ...b,
@@ -155,11 +155,16 @@
loading = false; loading = false;
eventEndpoint = new EventSource('/api/registeredEvents'); eventEndpoint = new EventSource('/api/registeredEvents');
eventEndpoint.onmessage = (e) => { eventEndpoint.onmessage = async (e) => {
const data = JSON.parse(e.data)[eventId - 1]; const response = await fetch('/api/registeredEvents', {
console.log(data); method: 'POST',
event = data; body: JSON.stringify({ eventId }),
brackets = data.registeredPlayers.map((b: any) => ({ headers: { 'Content-type': 'application/json; charset=UTF-8' }
});
const data = await response.json();
console.log('sone', data[0]);
event = data[0];
brackets = data[0].registeredPlayers.map((b: any) => ({
...b, ...b,
items: [...b.items] items: [...b.items]
})); }));
@@ -266,11 +271,6 @@
>Start event</button >Start event</button
> >
{/if} {/if}
<!-- <button onclick={() => (sortByScore = !sortByScore)}> -->
<!-- {sortByScore ? 'Sort: Score' : 'Sort: Manual'} -->
<!-- </button> -->
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<div class="flex w-50 min-w-0 flex-col"> <div class="flex w-50 min-w-0 flex-col">
<div class="brackets-name text-bold">=</div> <div class="brackets-name text-bold">=</div>
@@ -333,37 +333,45 @@
{player.lastName} {player.lastName}
</div> </div>
<div class="result-input-containers flex flex-col"> <div class="result-input-containers flex flex-col">
{#each Array.from({ length: numResults }, (_, i) => i) as run} {#if event.state == 1}
<input {#each Array.from({ length: numResults }, (_, i) => i) as run}
type="number" <input
placeholder="run {run + 1}" type="number"
class="text-black" placeholder="run {run + 1}"
disabled={event.state != 1} class="text-black"
value={pendingScores[player.id]?.[run] ?? ''} disabled={event.state != 1}
oninput={(e) => { value={pendingScores[player.id]?.[run] ?? ''}
const current = [ oninput={(e) => {
...(pendingScores[player.id] ?? Array(numResults).fill('')) const current = [
]; ...(pendingScores[player.id] ?? Array(numResults).fill(''))
current[run] = e.currentTarget.value; ];
pendingScores[player.id] = current; current[run] = e.currentTarget.value;
}} pendingScores[player.id] = current;
onblur={(e) => { }}
const val = parseFloat(e.currentTarget.value); onblur={(e) => {
const current = [ const val = parseFloat(e.currentTarget.value);
...(committedScores[player.id] ?? Array(numResults).fill(null)) const current = [
]; ...(committedScores[player.id] ?? Array(numResults).fill(null))
current[run] = isNaN(val) ? null : val; ];
committedScores[player.id] = current; current[run] = isNaN(val) ? null : val;
}} committedScores[player.id] = current;
/> }}
{/each} />
{/each}
{#if event.resultPresets[0].averageResults == 1}
<div class="text-sm opacity-60">
avg: {avgScore.toFixed(2)}
</div>
{/if}
{:else if player.playerScores.length > 0}
{#each player.playerScores as score}
<div class="border-red border-2 px-2">
{score.result}{event.resultPresets[0].unit}
</div>
{/each}
{/if}
</div> </div>
</div> </div>
{#if event.resultPresets[0].averageResults == 1}
<div class="text-sm opacity-60">
avg: {avgScore.toFixed(2)}
</div>
{/if}
</div> </div>
{/each} {/each}
</div> </div>

View File

@@ -382,5 +382,5 @@
margin-top: 2px; margin-top: 2px;
} }
.player-placement-gap { .player-placement-gap {
height: 20px; height: 50px;
} }

View File

@@ -1,7 +1,14 @@
import { getAllInitialInfo } from '$lib/server/databaseManager'; import { getPlayerInfo } from '$lib/server/databaseManager';
import type { PageServerLoad } from './$types';
// Provide initial data for the home page // export const load: PageServerLoad = async ({ params }) => {
export const load = async () => { // return {
return await getAllInitialInfo(); // data: await getPlayerInfo(parseInt(params.playerId))
// };
// };
export const load: PageServerLoad = async ({ params }) => {
return {
playerInfo: getPlayerInfo(parseInt(params.playerId))
};
}; };

View File

@@ -1,20 +1,75 @@
<script lang="ts"> <script lang="ts">
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import { getPlayerData } from './data.remote';
let { data, params }: PageProps = $props(); let { data }: PageProps = $props();
console.log(data); function marquee(node: HTMLElement) {
function measure() {
let some = await getPlayerData(params.playerId); const inner = node.querySelector<HTMLElement>('.marquee-inner');
console.log(some) if (!inner) return;
const overflow = inner.scrollWidth - node.clientWidth;
let playerId = params.playerId; if (overflow > 2) {
console.log(parseInt(playerId)); node.style.setProperty('--scroll-dist', `-${overflow + 6}px`);
inner.classList.add('scrolling');
} else {
inner.classList.remove('scrolling');
}
}
measure();
const ro = new ResizeObserver(measure);
ro.observe(node);
return { destroy: () => ro.disconnect() };
}
</script> </script>
<div class="my-3 flex flex-col justify-center"> {#await data.playerInfo}
<div class="player-box w-full" style="--c:red"> <div>Loading...</div>
<!-- <span>{console.log(getPlayerInfo(playerId))}</span> --> {:then playerInfo}
{console.log(playerInfo)}
<div class="mt-5 flex justify-center">
<div
class="player-single-box aspect-square w-full max-w-[95vw] justify-center md:aspect-2/1 lg:aspect-2/1"
style="--c:{playerInfo.teamInfo.color}"
>
<div class="player-ghost" aria-hidden="true">
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
<text
x="0"
y="0.7"
font-size="1"
dominant-baseline="auto"
textLength="100"
lengthAdjust="spacingAndGlyphs"
font-family="'Black Ops One',system-ui">{playerInfo.firstName}</text
>
</svg>
</div>
<div class=" goldman player-name-wrap text-5xl" use:marquee>
{playerInfo.firstName}
{playerInfo.lastName}
</div>
</div>
</div> </div>
</div> {/await}
<style>
.player-single-box {
flex: 1 1 0;
position: relative;
overflow: hidden;
border-radius: 8px;
border: 1.5px solid var(--c);
color: var(--c);
background: color-mix(in srgb, var(--c) 10%, transparent);
padding: 5px 7px 5px;
display: flex;
flex-direction: column;
}
.player-name-wrap {
position: relative;
z-index: 1;
text-decoration: none;
overflow: hidden;
white-space: nowrap;
}
</style>

View File

@@ -1,8 +0,0 @@
import { query } from '$app/server';
import { getPlayerInfo } from '$lib/server/databaseManager';
export const getPlayerData = query(async (playerId: number) => {
const playerInfo = await getPlayerInfo(playerId);
return playerInfo;
});