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
record scores
load event scores from db into scoring view
# VIEWS
ledger view
# UI - Medium priority
player view - all registered events and scores for those events
team view - all registered players and all points gained from them
make event view nicer
team view - all registered players and all points gained from them - Do i need this?
# UI
inserting into ledger
animations for all leaderboards
# Login stuff - HIGH PRIORITY
disableable register button on the login page for end users
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
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 { eq } from 'drizzle-orm';
import { sql, eq, and } from 'drizzle-orm';
import * as schema from '$lib/server/db/schema';
import { globalEmitter } from './globalEmitter';
@@ -74,7 +74,6 @@ export async function startEvent(eventId: number) {
.from(schema.registeredEventsView)
.where(eq(schema.registeredEventsView.eventId, eventId));
let requestedEvent = event[0];
console.log(requestedEvent);
if (requestedEvent.state != 0) {
console.log('not startable');
return false;
@@ -84,26 +83,26 @@ export async function startEvent(eventId: number) {
.set({ state: 1 })
.where(eq(schema.registeredEvents.id, requestedEvent.eventId))
.returning();
console.log(replacedEvent);
globalEmitter.emit('eventUpdate');
return true;
}
}
// 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
.select()
.from(schema.registeredEventPlayersView)
// Filter by event ID
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
.orderBy(
schema.registeredEventPlayersView.bracket,
schema.registeredEventPlayersView.placement,
sql`CASE WHEN ${schema.registeredEventPlayersView.placement} = 0 THEN 999999 ELSE ${schema.registeredEventPlayersView.placement} END ASC`,
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,
firstName: players.firstName,
lastName: players.lastName,
@@ -114,8 +113,14 @@ export async function getAllRegisteredEventPlayers(eventId: number) {
eventName: players.eventName,
teamId: players.teamId,
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) {
const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id), playerId);
return playerInfo;
const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id, playerId));
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) {
@@ -155,7 +192,10 @@ export async function getRegisteredEventsWithPlayers(eventId?: number) {
for (let registeredEvent in registeredEventList) {
let event = registeredEventList[registeredEvent];
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
const bracketOrder = brackets.brackets.map((category) => {

View File

@@ -55,6 +55,19 @@ export async function POST({ request }: any) {
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
let newPlayerPlacement = await db
.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">
import { onMount, onDestroy } from 'svelte';
import type { PageProps } from './$types';
let { params }: PageProps = $props();
let { params, data }: PageProps = $props();
function ordinal(n: number) {
const s = ['th', 'st', 'nd', 'rd'];
@@ -75,12 +75,21 @@
<div>loading</div>
{:then eventData}
{@const event = eventData[0]}
{console.log(event)}
<div class="flex justify-center">
<div class="w-full flex-col px-[5vw] text-center">
<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"
>
{event.name} - {event.division}
{#if event.state == 1}- ONGOING
{:else if event.state == 2}- FINISHED
{/if}
</div>
{#each event.registeredPlayers as bracket, bi}
{#if bi > 0}
@@ -92,7 +101,7 @@
<div class="bracket-vertical-sep"></div>
</div>
{#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">
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
<text
@@ -114,6 +123,13 @@
</div>
{#if player.placement !== 0}
<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}
<div class="player-placement-gap"></div>
{/if}
@@ -123,10 +139,23 @@
{/each}
</div>
</div>
{#if data.user}
<div class="mt-10 flex w-full justify-center">
<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>
{/if}
{/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' }
});
const data = await response.json();
console.log(data);
console.log(data[0]);
event = data[0];
brackets = data[0].registeredPlayers.map((b: any) => ({
...b,
@@ -155,11 +155,16 @@
loading = false;
eventEndpoint = new EventSource('/api/registeredEvents');
eventEndpoint.onmessage = (e) => {
const data = JSON.parse(e.data)[eventId - 1];
console.log(data);
event = data;
brackets = data.registeredPlayers.map((b: any) => ({
eventEndpoint.onmessage = async (e) => {
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('sone', data[0]);
event = data[0];
brackets = data[0].registeredPlayers.map((b: any) => ({
...b,
items: [...b.items]
}));
@@ -266,11 +271,6 @@
>Start event</button
>
{/if}
<!-- <button onclick={() => (sortByScore = !sortByScore)}> -->
<!-- {sortByScore ? 'Sort: Score' : 'Sort: Manual'} -->
<!-- </button> -->
<div class="flex flex-row justify-center">
<div class="flex w-50 min-w-0 flex-col">
<div class="brackets-name text-bold">=</div>
@@ -333,6 +333,7 @@
{player.lastName}
</div>
<div class="result-input-containers flex flex-col">
{#if event.state == 1}
{#each Array.from({ length: numResults }, (_, i) => i) as run}
<input
type="number"
@@ -357,13 +358,20 @@
}}
/>
{/each}
</div>
</div>
{#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>
{/each}
</div>

View File

@@ -382,5 +382,5 @@
margin-top: 2px;
}
.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 = async () => {
return await getAllInitialInfo();
// export const load: PageServerLoad = async ({ params }) => {
// return {
// 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">
import type { PageProps } from './$types';
import { getPlayerData } from './data.remote';
let { data, params }: PageProps = $props();
let { data }: PageProps = $props();
console.log(data);
let some = await getPlayerData(params.playerId);
console.log(some)
let playerId = params.playerId;
console.log(parseInt(playerId));
function marquee(node: HTMLElement) {
function measure() {
const inner = node.querySelector<HTMLElement>('.marquee-inner');
if (!inner) return;
const overflow = inner.scrollWidth - node.clientWidth;
if (overflow > 2) {
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>
<div class="my-3 flex flex-col justify-center">
<div class="player-box w-full" style="--c:red">
<!-- <span>{console.log(getPlayerInfo(playerId))}</span> -->
{#await data.playerInfo}
<div>Loading...</div>
{: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>
<div class=" goldman player-name-wrap text-5xl" use:marquee>
{playerInfo.firstName}
{playerInfo.lastName}
</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;
});