From 201821d53ce1540b0a9713adcf8600d93ecdc7aa Mon Sep 17 00:00:00 2001 From: voidarc Date: Mon, 29 Jun 2026 15:11:53 +0100 Subject: [PATCH] had opencode clean up all my nonsense comments too --- eslint.config.js | 4 -- scripts/seed.ts | 35 ++++++++-------- src/app.d.ts | 3 +- src/hooks.server.ts | 2 +- src/lib/index.ts | 2 +- src/lib/server/auth.ts | 6 +-- src/lib/server/databaseManager.ts | 28 ++++--------- src/lib/server/db/schema.ts | 2 +- src/lib/ui/Table.svelte | 11 +---- src/lib/ui/fitText.ts | 4 +- src/routes/+page.server.ts | 2 +- src/routes/+page.svelte | 25 ++++------- src/routes/api/eventResults/+server.ts | 23 +++------- src/routes/api/registeredEvents/+server.ts | 14 +------ src/routes/api/registeredPlayers/+server.ts | 3 -- src/routes/api/teams/+server.ts | 7 +--- src/routes/event/[eventId]/+page.svelte | 9 +--- .../event/scoring/[eventId]/+page.svelte | 11 ++--- src/routes/layout.css | 42 ++++++++----------- src/routes/signup/+page.server.ts | 2 +- svelte.config.js | 5 +-- 21 files changed, 77 insertions(+), 163 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 0014edd..1959b68 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -20,8 +20,6 @@ export default defineConfig( { languageOptions: { globals: { ...globals.browser, ...globals.node } }, rules: { - // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. - // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors 'no-undef': 'off' } }, @@ -37,8 +35,6 @@ export default defineConfig( } }, { - // Override or add rule settings here, such as: - // 'svelte/button-has-type': 'error' rules: {} } ); diff --git a/scripts/seed.ts b/scripts/seed.ts index 61df5f1..174967d 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -28,7 +28,7 @@ function readCSV(filename: string): Record[] { async function seed() { console.log('Resetting database...'); - // Disable foreign keys globally during setup to prevent structural mismatches + // Temporarily disable FK checks during reset await client.execute('PRAGMA foreign_keys = OFF'); await db.delete(schema.scoreLedger); @@ -55,28 +55,28 @@ async function seed() { .insert(schema.scorers) .values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash }); - // --- 1. Teams --- + // Seed teams const teamsCSV = readCSV('teams.csv'); for (const row of teamsCSV) { await db.insert(schema.teams).values({ name: row.team_name, color: row.color }); console.log(` → Team: ${row.team_name} (${row.color})`); } - // --- 2. Divisions --- + // Seed divisions const divisionsCSV = readCSV('divisions.csv'); for (const row of divisionsCSV) { await db.insert(schema.divisions).values({ name: row.div_name }); console.log(` → Division: ${row.div_name}`); } - // --- 2.5 Brackets (Added Section) --- + // Seed brackets const bracketsCSV = readCSV('brackets.csv'); for (const row of bracketsCSV) { await db.insert(schema.brackets).values({ name: row.bracket_name }); console.log(` → Bracket: ${row.bracket_name}`); } - // --- 2.75 resultPresets --- + // Seed result presets const resultPresetsCSV = readCSV('resultPresets.csv'); for (const row of resultPresetsCSV) { await db.insert(schema.resultPresets).values({ @@ -91,7 +91,7 @@ async function seed() { ); } - // --- 3. Scoring Presets --- + // Seed scoring presets const scoringPresetsCSV = readCSV('scoringPresets.csv'); for (const row of scoringPresetsCSV) { await db.insert(schema.scoringPresets).values({ @@ -102,19 +102,19 @@ async function seed() { console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`); } - // Maps for dynamic relational lookups + // Build lookup maps for relational seeding const dbTeams = await db.select().from(schema.teams); const dbDivisions = await db.select().from(schema.divisions); const dbResults = await db.select().from(schema.resultPresets); - const dbBrackets = await db.select().from(schema.brackets); // Look up newly seeded brackets + const dbBrackets = await db.select().from(schema.brackets); const teamMap = new Map(dbTeams.map((t) => [t.name, t.id])); const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id])); const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name])); - const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id])); // Map names to IDs - const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id])); // Map names to IDs + const bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id])); + const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id])); - // --- 4. Players --- + // Seed players const playersCSV = readCSV('players.csv'); for (const row of playersCSV) { const teamId = teamMap.get(row.team); @@ -132,7 +132,7 @@ async function seed() { ); } - // --- 5. Event Types --- + // Seed event types const eventTypesCSV = readCSV('eventTypes.csv'); for (const row of eventTypesCSV) { const presetId = resultPresetMap.get(row.resultPreset); @@ -150,7 +150,7 @@ async function seed() { const dbEventTypes = await db.select().from(schema.eventTypes); const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id])); - // --- 6. Registered Events --- + // Seed registered events const eventNameMap = new Map(); const registeredEventsCSV = readCSV('registeredEvents.csv'); @@ -176,11 +176,11 @@ async function seed() { ` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}, winner: ${teamId}, ${row.winner}` ); - // Map the textual event name (e.g., "100m Sprint") to the generated DB ID + // Map event name|division to the generated event ID eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id); } - // --- 7. Registered Players --- + // Seed registered players (linking players to events) const dbPlayers = await db.select().from(schema.players); const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p])); @@ -190,7 +190,6 @@ async function seed() { const divisionName = divisionNameMap.get(player?.division ?? -1); const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`); - // Dynamic look up of the bracket row's primary key ID using the CSV text const bracketId = bracketMap.get(row.bracket); if (!player) throw new Error(`Player "${row.player_registered}" not found`); @@ -203,7 +202,7 @@ async function seed() { await db.insert(schema.registeredPlayers).values({ playerID: player.id, registeredEventID: actualEventId, - bracket: bracketId, // Using the real relational ID instead of raw value + bracket: bracketId, placement: row.player_placement || 0 }); console.log( @@ -211,7 +210,7 @@ async function seed() { ); } - // Re-enable Foreign Key constraints now that the data is built cleanly + // Re-enable FK checks await client.execute('PRAGMA foreign_keys = ON'); console.log('\n✅ Seeding complete!'); diff --git a/src/app.d.ts b/src/app.d.ts index 87f8dbd..65d0a16 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,7 +1,6 @@ import type { User, Session } from 'better-auth/minimal'; -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces +// SvelteKit app type declarations declare global { namespace App { interface Locals { user?: User; session?: Session } diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 087a601..ad08005 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -19,7 +19,7 @@ export const handle: Handle = async ({ event, resolve }) => { const { session, user } = await validateSessionToken(token); if (session) { - setSessionTokenCookie(event, token, session.expiresAt); // refresh cookie expiry + setSessionTokenCookie(event, token, session.expiresAt); } else { deleteSessionTokenCookie(event); } diff --git a/src/lib/index.ts b/src/lib/index.ts index 856f2b6..3e0cc23 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1 @@ -// place files you want to import through the `$lib` alias in this folder. +// Place shared exports accessible via $lib alias here diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index c2a17d9..a6dc8e0 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -34,13 +34,13 @@ export async function validateSessionToken(token: string) { if (!row) return { session: null, user: null }; - // Expired — clean up and reject + // Session expired, delete it if (Date.now() >= row.session.expiresAt.getTime()) { await db.delete(sessions).where(eq(sessions.id, sessionId)); return { session: null, user: null }; } - // Sliding expiration: renew if past the halfway mark + // Renew session if past the halfway point if (Date.now() >= row.session.expiresAt.getTime() - DAY_MS * 15) { const newExpiresAt = new Date(Date.now() + DAY_MS * 30); await db.update(sessions).set({ expiresAt: newExpiresAt }).where(eq(sessions.id, sessionId)); @@ -59,7 +59,7 @@ export function setSessionTokenCookie(event: RequestEvent, token: string, expire expires: expiresAt, path: '/', httpOnly: true, - secure: !import.meta.env.DEV, // allow http in local dev + secure: !import.meta.env.DEV, sameSite: 'lax' }); } diff --git a/src/lib/server/databaseManager.ts b/src/lib/server/databaseManager.ts index f629ea4..6a41290 100644 --- a/src/lib/server/databaseManager.ts +++ b/src/lib/server/databaseManager.ts @@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm'; import * as schema from '$lib/server/db/schema'; import { globalEmitter } from './globalEmitter'; -// For page.server.ts so that it doesnt look weird before loading +// Initial data for page load export async function getAllInitialInfo() { return { teams: await getTeams(), @@ -11,7 +11,7 @@ export async function getAllInitialInfo() { }; } -// Get teams object from database +// Fetch teams with optional filter export async function getTeams(teamId?: number) { const allTeams = await db .select() @@ -32,7 +32,7 @@ export async function getTeams(teamId?: number) { }; } -// Get all registered events from database +// Fetch registered events with optional filter export async function getRegisteredEvents(eventId?: number) { async function getWinnerInfo(teamId: number) { const teamInfo = await getTeams(teamId); @@ -90,12 +90,12 @@ export async function startEvent(eventId: number) { } } -// Get all players with an event id specified +// Fetch all players registered for a specific event export async function getAllRegisteredEventPlayers(eventId: number) { const eventPlayers = await db .select() .from(schema.registeredEventPlayersView) - // where the registered player is registered for that event + // Filter by event ID .where(eq(schema.registeredEventPlayersView.eventId, eventId)) .orderBy( schema.registeredEventPlayersView.bracket, @@ -138,48 +138,34 @@ export async function getResultPreset(presetId?: number) { }; } -// Moved the function the registeredEvents endpoint -// Just merges the results of the previous 3 functions into a standard format +// Merge events, players, brackets, and presets into a frontend-ready structure export async function getRegisteredEventsWithPlayers(eventId?: number) { - // Get updated events from database let newEvents = await getRegisteredEvents(eventId); let registeredEventList = newEvents['events']; - // Get all possible brackets from the database let brackets = await getAllBrackets(); - // Initilise the final eventList let fullEventList: any[] = []; - // For every event for (let registeredEvent in registeredEventList) { let event = registeredEventList[registeredEvent]; - - // Get the info about the result preset for the ui let resultPreset = await getResultPreset(event.resultPreset); - - // Get all players for the event let registeredPlayers = await getAllRegisteredEventPlayers(event.id); - // Map the players into an [] with structure {id: number, name: string, items: any[]} - // So that the players are sorted by bracket for the frontend + // Group players by bracket category for the frontend const bracketOrder = brackets.brackets.map((category) => { return { ...category, - // Filter the items that match the current bracket name items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name) }; }); - // append the player info and result preset to the event object let eventWithPlayers = { ...event, registeredPlayers: bracketOrder, ...resultPreset }; - // combine all of the events into one array fullEventList.push(eventWithPlayers); } - // Send to client return fullEventList; } diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 9a3b7a6..f02e084 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -9,7 +9,7 @@ export const scorers = sqliteTable('users', { }); export const sessions = sqliteTable('sessions', { - id: text('id').primaryKey(), // SHA-256 hash of the token, never the raw token + id: text('id').primaryKey(), userId: text('user_id') .notNull() .references(() => scorers.id), diff --git a/src/lib/ui/Table.svelte b/src/lib/ui/Table.svelte index ae4c5af..3c9ed0c 100644 --- a/src/lib/ui/Table.svelte +++ b/src/lib/ui/Table.svelte @@ -13,33 +13,26 @@ let containerRef = $state(null); let activeId = $state(null); - /** - * Public function to scroll a specific row into the center of the viewport. - * Can be called manually by the parent component. - */ + /** Scroll a specific row to the center of the viewport */ export function scrollToId(id: string | number) { if (!containerRef) return; const targetRow = containerRef.querySelector(`#row-${id}`) as HTMLElement | null; if (targetRow) { - // Update local state to highlight the centered row activeId = id; - // Calculate the midpoint math to center the row const rowOffsetTop = targetRow.offsetTop; const rowHeight = targetRow.offsetHeight; - const containerHeight = containerRef.clientHeight; - // Target = Row position - (half of container space) + (half of row height adjustment) const centerScrollTarget = rowOffsetTop - containerHeight / 2 + rowHeight / 2; - // Using behavior: 'auto' for an instantaneous snap instead of smooth scrolling containerRef.scrollTo({ top: centerScrollTarget, behavior: 'auto' }); } } + }
diff --git a/src/lib/ui/fitText.ts b/src/lib/ui/fitText.ts index b79899c..56f15af 100644 --- a/src/lib/ui/fitText.ts +++ b/src/lib/ui/fitText.ts @@ -10,7 +10,7 @@ export const fitText: Action = (node) => { node.style.transformOrigin = 'top left'; node.style.transform = 'none'; - // Step 1: fit to height + // Fit font size to container height let size = 1; node.style.fontSize = size + 'px'; while (node.scrollHeight <= container!.clientHeight) { @@ -19,7 +19,7 @@ export const fitText: Action = (node) => { } node.style.fontSize = size - 1 + 'px'; - // Step 2: stretch width to fill container + // Stretch width to fill container const scaleX = container!.clientWidth / node.scrollWidth; node.style.transform = `scaleX(${scaleX})`; } diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 156f4e6..d6c39ce 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,6 +1,6 @@ import { getAllInitialInfo } from '$lib/server/databaseManager'; -// Literally only here so that the frontend has the right structure +// Provide initial data for the home page export const load = async () => { return await getAllInitialInfo(); }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3c422c8..a8e9e61 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -14,21 +14,18 @@ let scoreEndpoint: EventSource; let eventEndpoint: EventSource; - // Ref map for event card DOM nodes + // Map event IDs to their DOM elements for scrolling let eventRefs = $state>({}); onMount(() => { - // Get the teams endpoint + // Subscribe to live team score updates via SSE scoreEndpoint = new EventSource('/api/teams'); - // When the endpoint sends something scoreEndpoint.onmessage = (e) => { - // Parse the json const teamsData = JSON.parse(e.data); - // If its teams info, then update the teams thing if (teamsData['teams']) teams = teamsData['teams']; }; - // Basically same for events + // Subscribe to live event updates via SSE eventEndpoint = new EventSource('/api/registeredEvents'); eventEndpoint.onmessage = (e) => { eventTable = JSON.parse(e.data); @@ -50,22 +47,18 @@ return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]); } - // When focusEventId changes, scroll to and highlight that event card + // Scroll to and highlight the currently ongoing event $effect(() => { if (focusEventId == null) return; tick().then(() => { - // Get focused element const el = eventRefs[focusEventId!]; - // Scroll it to the top of the box el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' }); - // Wait for that to finish tick().then(() => { - // Scroll the window back to the top window.scrollTo(0, 0); }); }); }); - // Svelte action: measures text overflow and drives CSS marquee + // Svelte action that enables CSS marquee when text overflows function marquee(node: HTMLElement) { function measure() { const inner = node.querySelector('.marquee-inner'); @@ -88,9 +81,9 @@ scoreEndpoint?.close()} />
- +
- + {#if leaderboard[0]} {@const team = leaderboard[0]} {/if} - + {#if leaderboard.length > 1}
{#each leaderboard.slice(1) as team, i (team.name)} @@ -158,7 +151,7 @@ {/if}
- +
diff --git a/src/routes/api/eventResults/+server.ts b/src/routes/api/eventResults/+server.ts index 992b8b8..feca0b5 100644 --- a/src/routes/api/eventResults/+server.ts +++ b/src/routes/api/eventResults/+server.ts @@ -5,31 +5,26 @@ import * as schema from '$lib/server/db/schema'; import { getRegisteredEvents } from '$lib/server/databaseManager'; export async function POST({ request }: any) { - // Decode body let responseBody = await request.json(); - // If there is no request then dont respond if (!responseBody) { return new Error('nuh uh'); } else { console.log(JSON.stringify(responseBody)); if (responseBody.eventId) { - // Get the event let eventData = await getRegisteredEvents(responseBody.eventId); - // If the event hasnt started or ended if (eventData.events[0].state != 1) { return new Error(); } let scoringPreset = eventData.events[0].scoringPreset; - // make a new main ledger entry + // Create ledger entry to record this scoring event let newLedgerEntry = await db .insert(schema.mainLedger) .values({ registeredEvent: responseBody.eventId }) .returning(); - // get the id so i can use it in the sub ledgers let ledgerEntryId = newLedgerEntry[0].id; function getPoints( @@ -44,30 +39,24 @@ export async function POST({ request }: any) { ); } - // Create a Map to collect total scores per team + // Accumulate scores per team, then batch insert const teamScores = new Map(); - // for every bracket for (let bracket in responseBody.brackets) { - // for every player for (let player in responseBody.brackets[bracket].players) { - // variable fun let currentPlayer = responseBody.brackets[bracket].players[player]; let currentPlayerTeam = currentPlayer.teamId; let currentPlayerPosition = currentPlayer.position; - // If they put in a score / result if (currentPlayerPosition > 0) { let score = getPoints(scoringPreset, currentPlayerPosition); - // If their score is in the preset and they put in a score if (currentPlayer.scores.length > 0) { if (score > 0) { - // Accumulate points for this team instead of inserting right away const currentTeamScore = teamScores.get(currentPlayerTeam) || 0; teamScores.set(currentPlayerTeam, currentTeamScore + score); } - // PLACEMENT LOGIC (Left untouched) + // Update player placement in the database let newPlayerPlacement = await db .update(schema.registeredPlayers) .set({ placement: currentPlayerPosition }) @@ -79,7 +68,7 @@ export async function POST({ request }: any) { } } - // After calculating all player scores, batch insert team scores into the ledger + // Batch insert team scores into the ledger if (teamScores.size > 0) { const ledgerEntries = Array.from(teamScores.entries()).map(([teamID, points]) => ({ ledgerID: ledgerEntryId, @@ -91,7 +80,7 @@ export async function POST({ request }: any) { console.log(newScores); } - // 2. Determine the winner right here using the same Map data + // Determine the winning team from accumulated scores let highestScore = -1; let winningTeamId = null; @@ -112,9 +101,7 @@ export async function POST({ request }: any) { } } - // Update the frontends globalEmitter.emit('scoreUpdate'); - // Return a response because return new Response('coolsies'); } } diff --git a/src/routes/api/registeredEvents/+server.ts b/src/routes/api/registeredEvents/+server.ts index f130d3d..773f571 100644 --- a/src/routes/api/registeredEvents/+server.ts +++ b/src/routes/api/registeredEvents/+server.ts @@ -3,21 +3,16 @@ import { globalEmitter } from '$lib/server/globalEmitter'; import { generateEndpoint } from '$lib/server/endpoint'; export async function GET({ request }) { - const endpoint = generateEndpoint(async (enqueue) => { + const endpoint = generateEndpoint(async (enqueue) => { let eventList = async () => { - // Get eventList with structure from database let newEventList = await getRegisteredEventsWithPlayers(); - // send to client enqueue(newEventList); }; - // Send the eventList to the client when a connection is made - // TODO make it so that this only happens on an initial post request + // Send initial data on connection, then subscribe to updates eventList(); - // When the data changes send an update to the client globalEmitter.on('eventUpdate', eventList); - // Return cleanup function to remove listener when it closes return () => { globalEmitter.off('eventUpdate', eventList); }; @@ -26,18 +21,13 @@ export async function GET({ request }) { } export async function POST({ request }: any) { - // Decode body let responseBody = await request.json(); - // If there is no request then dont respond if (!responseBody) { return new Response('nuh uh'); } else { - // Get requested event let eventRequested = responseBody.eventId; - // request eventList from database let eventList = await getRegisteredEventsWithPlayers(eventRequested); - // return eventList to client return new Response(JSON.stringify(eventList)); } } diff --git a/src/routes/api/registeredPlayers/+server.ts b/src/routes/api/registeredPlayers/+server.ts index e13bf2f..7c1ab6e 100644 --- a/src/routes/api/registeredPlayers/+server.ts +++ b/src/routes/api/registeredPlayers/+server.ts @@ -2,9 +2,6 @@ import { getAllRegisteredEventPlayers } from '$lib/server/databaseManager'; import { globalEmitter } from '$lib/server/globalEmitter'; import { generateEndpoint } from '$lib/server/endpoint'; -// Expose post request export async function POST({ request }: any) { - // When post request recieved increment testscores by 1 - // Return ok so the frontend is happy return new Response('ok'); } diff --git a/src/routes/api/teams/+server.ts b/src/routes/api/teams/+server.ts index ca12721..b3701b5 100644 --- a/src/routes/api/teams/+server.ts +++ b/src/routes/api/teams/+server.ts @@ -2,27 +2,22 @@ import { getTeams } from '$lib/server/databaseManager'; import { globalEmitter } from '$lib/server/globalEmitter'; import { generateEndpoint } from '$lib/server/endpoint'; -// Expose post request export async function POST({ request }: any) { - // When post request recieved increment testscores by 1 globalEmitter.emit('incrementScores'); - // Return ok so the frontend is happy return new Response('ok'); } export async function GET({ request }) { const endpoint = generateEndpoint(async (enqueue) => { - // Function to grab score from database and add it to message queue let newScore = async () => { let newScores = await getTeams(); enqueue(newScores); }; - // Initial Sync + // Send initial team scores, then subscribe to updates newScore(); globalEmitter.on('scoreUpdate', newScore); - // Simply return the cleanup function here return () => { globalEmitter.off('scoreUpdate', newScore); }; diff --git a/src/routes/event/[eventId]/+page.svelte b/src/routes/event/[eventId]/+page.svelte index 6e02276..96fe2eb 100644 --- a/src/routes/event/[eventId]/+page.svelte +++ b/src/routes/event/[eventId]/+page.svelte @@ -44,20 +44,13 @@ const data = await response.json(); - // Sort the players inside each bracket before returning the data + // Sort players: placed first (ascending), unplaced last if (data && data[0] && data[0].registeredPlayers) { data[0].registeredPlayers.forEach((bracket: any) => { bracket.items.sort((a: any, b: any) => { - // 1. Both have no placement (placement === 0) -> keep original order if (a.placement === 0 && b.placement === 0) return 0; - - // 2. 'a' has no placement, but 'b' does -> move 'b' to the top if (a.placement === 0) return 1; - - // 3. 'a' has a placement, but 'b' doesn't -> keep 'a' on top if (b.placement === 0) return -1; - - // 4. Both have placements -> sort ascending (1st, 2nd, 3rd, etc.) return a.placement - b.placement; }); }); diff --git a/src/routes/event/scoring/[eventId]/+page.svelte b/src/routes/event/scoring/[eventId]/+page.svelte index e02eb03..9c6b20b 100644 --- a/src/routes/event/scoring/[eventId]/+page.svelte +++ b/src/routes/event/scoring/[eventId]/+page.svelte @@ -41,7 +41,6 @@ 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); @@ -93,19 +92,17 @@ ...bracket, items: sortByScore ? [...bracket.items].sort((a, b) => { - // 1. Check if players actually have committed scores const scoresA = committedScores[a.id]?.filter((s) => s !== null) ?? []; const scoresB = committedScores[b.id]?.filter((s) => s !== null) ?? []; const hasA = scoresA.length > 0; const hasB = scoresB.length > 0; - // 2. Handle cases where one or both don't have scores - if (!hasA && !hasB) return 0; // Neither have scores, keep order - if (!hasA) return 1; // 'a' has no score -> push down - if (!hasB) return -1; // 'b' has no score -> push down + // Push unranked players to the bottom, then sort by score + if (!hasA && !hasB) return 0; + if (!hasA) return 1; + if (!hasB) return -1; - // 3. Both have scores, fallback to standard leaderboard sorting const fallback = lowerIsBetter ? Infinity : -Infinity; const sa = useAverage ? average(committedScores[a.id] ?? [], fallback) diff --git a/src/routes/layout.css b/src/routes/layout.css index db1d78c..c61258e 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -31,7 +31,7 @@ font-weight: 400; } -/* ── Leaderboard ── */ +/* Leaderboard */ .leaderboard { display: flex; flex-direction: column; @@ -46,7 +46,7 @@ border: 2px solid var(--c); color: var(--c); background: color-mix(in srgb, var(--c) 10%, transparent); - /* Grid stacking: ghost + fg share the same cell */ + /* Ghost + foreground stacked in same grid cell */ display: grid; grid-template-columns: 1fr; grid-template-rows: 1fr; @@ -71,7 +71,7 @@ min-height: 56px; } -/* Ghost SVG: fill the cell edge-to-edge, text flush to bottom */ +/* Ghost SVG fills cell, text sits at bottom */ .score-ghost { width: 100%; height: 100%; @@ -84,7 +84,7 @@ display: block; } -/* Foreground */ +/* Foreground content layer */ .score-fg { position: relative; z-index: 1; @@ -126,14 +126,7 @@ font-size: clamp(26px, 7.5vw, 52px); } -/* - * Runners-up grid: - * - 1 column on small screens (<480px) - * - 2 columns on medium (480–699px) - * - 4 columns on large (≥700px), max 5 - * --runner-count drives the actual column count on large screens - * so fewer than 5 teams still fill the whole row. - */ +/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */ .runners-grid { display: grid; gap: 10px; @@ -154,8 +147,7 @@ } @media (min-width: 700px) { .runners-grid { - /* clamp actual count to 4 on large, but use --runner-count - so e.g. 2 teams still fill 2 equal columns not 2 of 4 */ + /* Use --runner-count so fewer teams fill the row evenly */ grid-template-columns: repeat(min(var(--runner-count), 4), 1fr); } .winner { @@ -166,7 +158,7 @@ } } -/* ── Section label ── */ +/* Section label */ .section-label { font-size: 10px; letter-spacing: 2.5px; @@ -176,7 +168,7 @@ margin: 0; } -/* ── Events scrollable container ── */ +/* Events scrollable container */ .events-scroll { flex: 1; box-shadow: inset 0px 48px 20px -26px rgba(0, 0, 0, 0.35); @@ -185,7 +177,7 @@ /* max-height: 900px; */ min-height: 0; padding: 0 14px 24px; - /* Thin custom scrollbar */ + /* Thin scrollbar styling */ scrollbar-width: thin; scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent; } @@ -206,7 +198,7 @@ color: var(--winner-color); } -/* ── Event card ── */ +/* Event card */ .event-card { border-radius: 12px; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); @@ -218,7 +210,7 @@ background-color: color-mix(in srgb, currentColor 18%, transparent); color: var(--ctp-latte-peach); } -/* Focus highlight pulse — added/removed by $effect */ +/* Pulse animation for focused event card */ .event-card.highlight-pulse { animation: card-pulse 1.2s ease-out forwards; } @@ -254,7 +246,7 @@ opacity: 0.4; } -/* ── Brackets ── */ +/* Brackets */ .brackets { display: flex; flex-direction: column; @@ -271,9 +263,9 @@ flex-wrap: wrap; } -/* ── Player boxes ── */ +/* Player boxes */ .player-box { - flex: 1 1 0; /* equal widths, no min-content bias */ + flex: 1 1 0; max-width: 160px; position: relative; overflow: hidden; @@ -287,11 +279,11 @@ min-height: 52px; } -/* 1 col on small screens, 4 across on large */ +/* Mobile: single column layout */ @media (max-width: 479px) { .brackets { flex-direction: row; - align-items: stretch; /* was flex-start — lets columns fill full height */ + align-items: stretch; } .bracket-sep { width: 1px; @@ -326,7 +318,7 @@ @media (min-width: 700px) { .player-box { max-width: calc(25% - 6px); - } /* 4 per row */ + } } .player-ghost { diff --git a/src/routes/signup/+page.server.ts b/src/routes/signup/+page.server.ts index 7c3a39a..367bed0 100644 --- a/src/routes/signup/+page.server.ts +++ b/src/routes/signup/+page.server.ts @@ -21,7 +21,7 @@ export const actions: Actions = { return fail(400, { message: 'Username already taken' }); } - const passwordHash = await Bun.password.hash(password); // defaults to argon2id + const passwordHash = await Bun.password.hash(password); const userId = crypto.randomUUID(); await db.insert(scorers).values({ id: userId, username, passwordHash }); diff --git a/svelte.config.js b/svelte.config.js index f532c9b..7717a06 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -3,13 +3,10 @@ import adapter from '@sveltejs/adapter-auto'; /** @type {import('@sveltejs/kit').Config} */ const config = { compilerOptions: { - // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + // Force runes mode (except in node_modules). Remove when Svelte 6 makes it default. runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) }, kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), typescript: {