had opencode clean up all my nonsense comments too
This commit is contained in:
@@ -20,8 +20,6 @@ export default defineConfig(
|
|||||||
{
|
{
|
||||||
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
languageOptions: { globals: { ...globals.browser, ...globals.node } },
|
||||||
rules: {
|
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'
|
'no-undef': 'off'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -37,8 +35,6 @@ export default defineConfig(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Override or add rule settings here, such as:
|
|
||||||
// 'svelte/button-has-type': 'error'
|
|
||||||
rules: {}
|
rules: {}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function readCSV(filename: string): Record<string, any>[] {
|
|||||||
async function seed() {
|
async function seed() {
|
||||||
console.log('Resetting database...');
|
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 client.execute('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
await db.delete(schema.scoreLedger);
|
await db.delete(schema.scoreLedger);
|
||||||
@@ -55,28 +55,28 @@ async function seed() {
|
|||||||
.insert(schema.scorers)
|
.insert(schema.scorers)
|
||||||
.values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash });
|
.values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash });
|
||||||
|
|
||||||
// --- 1. Teams ---
|
// Seed teams
|
||||||
const teamsCSV = readCSV('teams.csv');
|
const teamsCSV = readCSV('teams.csv');
|
||||||
for (const row of teamsCSV) {
|
for (const row of teamsCSV) {
|
||||||
await db.insert(schema.teams).values({ name: row.team_name, color: row.color });
|
await db.insert(schema.teams).values({ name: row.team_name, color: row.color });
|
||||||
console.log(` → Team: ${row.team_name} (${row.color})`);
|
console.log(` → Team: ${row.team_name} (${row.color})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2. Divisions ---
|
// Seed divisions
|
||||||
const divisionsCSV = readCSV('divisions.csv');
|
const divisionsCSV = readCSV('divisions.csv');
|
||||||
for (const row of divisionsCSV) {
|
for (const row of divisionsCSV) {
|
||||||
await db.insert(schema.divisions).values({ name: row.div_name });
|
await db.insert(schema.divisions).values({ name: row.div_name });
|
||||||
console.log(` → Division: ${row.div_name}`);
|
console.log(` → Division: ${row.div_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2.5 Brackets (Added Section) ---
|
// Seed brackets
|
||||||
const bracketsCSV = readCSV('brackets.csv');
|
const bracketsCSV = readCSV('brackets.csv');
|
||||||
for (const row of bracketsCSV) {
|
for (const row of bracketsCSV) {
|
||||||
await db.insert(schema.brackets).values({ name: row.bracket_name });
|
await db.insert(schema.brackets).values({ name: row.bracket_name });
|
||||||
console.log(` → Bracket: ${row.bracket_name}`);
|
console.log(` → Bracket: ${row.bracket_name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 2.75 resultPresets ---
|
// Seed result presets
|
||||||
const resultPresetsCSV = readCSV('resultPresets.csv');
|
const resultPresetsCSV = readCSV('resultPresets.csv');
|
||||||
for (const row of resultPresetsCSV) {
|
for (const row of resultPresetsCSV) {
|
||||||
await db.insert(schema.resultPresets).values({
|
await db.insert(schema.resultPresets).values({
|
||||||
@@ -91,7 +91,7 @@ async function seed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. Scoring Presets ---
|
// Seed scoring presets
|
||||||
const scoringPresetsCSV = readCSV('scoringPresets.csv');
|
const scoringPresetsCSV = readCSV('scoringPresets.csv');
|
||||||
for (const row of scoringPresetsCSV) {
|
for (const row of scoringPresetsCSV) {
|
||||||
await db.insert(schema.scoringPresets).values({
|
await db.insert(schema.scoringPresets).values({
|
||||||
@@ -102,19 +102,19 @@ async function seed() {
|
|||||||
console.log(` → Preset ${row.preset}: placement ${row.placement} = ${row.points}pts`);
|
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 dbTeams = await db.select().from(schema.teams);
|
||||||
const dbDivisions = await db.select().from(schema.divisions);
|
const dbDivisions = await db.select().from(schema.divisions);
|
||||||
const dbResults = await db.select().from(schema.resultPresets);
|
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 teamMap = new Map(dbTeams.map((t) => [t.name, t.id]));
|
||||||
const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
|
const divisionMap = new Map(dbDivisions.map((d) => [d.name, d.id]));
|
||||||
const divisionNameMap = new Map([...divisionMap.entries()].map(([name, id]) => [id, name]));
|
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 bracketMap = new Map(dbBrackets.map((b) => [b.name, b.id]));
|
||||||
const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id])); // Map names to IDs
|
const resultPresetMap = new Map(dbResults.map((b) => [b.presetName, b.id]));
|
||||||
|
|
||||||
// --- 4. Players ---
|
// Seed players
|
||||||
const playersCSV = readCSV('players.csv');
|
const playersCSV = readCSV('players.csv');
|
||||||
for (const row of playersCSV) {
|
for (const row of playersCSV) {
|
||||||
const teamId = teamMap.get(row.team);
|
const teamId = teamMap.get(row.team);
|
||||||
@@ -132,7 +132,7 @@ async function seed() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 5. Event Types ---
|
// Seed event types
|
||||||
const eventTypesCSV = readCSV('eventTypes.csv');
|
const eventTypesCSV = readCSV('eventTypes.csv');
|
||||||
for (const row of eventTypesCSV) {
|
for (const row of eventTypesCSV) {
|
||||||
const presetId = resultPresetMap.get(row.resultPreset);
|
const presetId = resultPresetMap.get(row.resultPreset);
|
||||||
@@ -150,7 +150,7 @@ async function seed() {
|
|||||||
const dbEventTypes = await db.select().from(schema.eventTypes);
|
const dbEventTypes = await db.select().from(schema.eventTypes);
|
||||||
const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id]));
|
const eventTypeMap = new Map(dbEventTypes.map((et) => [et.name, et.id]));
|
||||||
|
|
||||||
// --- 6. Registered Events ---
|
// Seed registered events
|
||||||
const eventNameMap = new Map<string, number>();
|
const eventNameMap = new Map<string, number>();
|
||||||
const registeredEventsCSV = readCSV('registeredEvents.csv');
|
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}`
|
` → 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);
|
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 dbPlayers = await db.select().from(schema.players);
|
||||||
const playerMap = new Map(dbPlayers.map((p) => [`${p.firstName} ${p.lastName}`, p]));
|
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 divisionName = divisionNameMap.get(player?.division ?? -1);
|
||||||
const actualEventId = eventNameMap.get(`${row.event_registered}|${divisionName}`);
|
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);
|
const bracketId = bracketMap.get(row.bracket);
|
||||||
|
|
||||||
if (!player) throw new Error(`Player "${row.player_registered}" not found`);
|
if (!player) throw new Error(`Player "${row.player_registered}" not found`);
|
||||||
@@ -203,7 +202,7 @@ async function seed() {
|
|||||||
await db.insert(schema.registeredPlayers).values({
|
await db.insert(schema.registeredPlayers).values({
|
||||||
playerID: player.id,
|
playerID: player.id,
|
||||||
registeredEventID: actualEventId,
|
registeredEventID: actualEventId,
|
||||||
bracket: bracketId, // Using the real relational ID instead of raw value
|
bracket: bracketId,
|
||||||
placement: row.player_placement || 0
|
placement: row.player_placement || 0
|
||||||
});
|
});
|
||||||
console.log(
|
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');
|
await client.execute('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
console.log('\n✅ Seeding complete!');
|
console.log('\n✅ Seeding complete!');
|
||||||
|
|||||||
3
src/app.d.ts
vendored
3
src/app.d.ts
vendored
@@ -1,7 +1,6 @@
|
|||||||
import type { User, Session } from 'better-auth/minimal';
|
import type { User, Session } from 'better-auth/minimal';
|
||||||
|
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// SvelteKit app type declarations
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
interface Locals { user?: User; session?: Session }
|
interface Locals { user?: User; session?: Session }
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
const { session, user } = await validateSessionToken(token);
|
const { session, user } = await validateSessionToken(token);
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
setSessionTokenCookie(event, token, session.expiresAt); // refresh cookie expiry
|
setSessionTokenCookie(event, token, session.expiresAt);
|
||||||
} else {
|
} else {
|
||||||
deleteSessionTokenCookie(event);
|
deleteSessionTokenCookie(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// Place shared exports accessible via $lib alias here
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export async function validateSessionToken(token: string) {
|
|||||||
|
|
||||||
if (!row) return { session: null, user: null };
|
if (!row) return { session: null, user: null };
|
||||||
|
|
||||||
// Expired — clean up and reject
|
// Session expired, delete it
|
||||||
if (Date.now() >= row.session.expiresAt.getTime()) {
|
if (Date.now() >= row.session.expiresAt.getTime()) {
|
||||||
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
await db.delete(sessions).where(eq(sessions.id, sessionId));
|
||||||
return { session: null, user: null };
|
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) {
|
if (Date.now() >= row.session.expiresAt.getTime() - DAY_MS * 15) {
|
||||||
const newExpiresAt = new Date(Date.now() + DAY_MS * 30);
|
const newExpiresAt = new Date(Date.now() + DAY_MS * 30);
|
||||||
await db.update(sessions).set({ expiresAt: newExpiresAt }).where(eq(sessions.id, sessionId));
|
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,
|
expires: expiresAt,
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: !import.meta.env.DEV, // allow http in local dev
|
secure: !import.meta.env.DEV,
|
||||||
sameSite: 'lax'
|
sameSite: 'lax'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { eq } 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';
|
||||||
|
|
||||||
// For page.server.ts so that it doesnt look weird before loading
|
// Initial data for page load
|
||||||
export async function getAllInitialInfo() {
|
export async function getAllInitialInfo() {
|
||||||
return {
|
return {
|
||||||
teams: await getTeams(),
|
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) {
|
export async function getTeams(teamId?: number) {
|
||||||
const allTeams = await db
|
const allTeams = await db
|
||||||
.select()
|
.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) {
|
export async function getRegisteredEvents(eventId?: number) {
|
||||||
async function getWinnerInfo(teamId: number) {
|
async function getWinnerInfo(teamId: number) {
|
||||||
const teamInfo = await getTeams(teamId);
|
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) {
|
export async function getAllRegisteredEventPlayers(eventId: number) {
|
||||||
const eventPlayers = await db
|
const eventPlayers = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.registeredEventPlayersView)
|
.from(schema.registeredEventPlayersView)
|
||||||
// where the registered player is registered for that event
|
// Filter by event ID
|
||||||
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
|
.where(eq(schema.registeredEventPlayersView.eventId, eventId))
|
||||||
.orderBy(
|
.orderBy(
|
||||||
schema.registeredEventPlayersView.bracket,
|
schema.registeredEventPlayersView.bracket,
|
||||||
@@ -138,48 +138,34 @@ export async function getResultPreset(presetId?: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moved the function the registeredEvents endpoint
|
// Merge events, players, brackets, and presets into a frontend-ready structure
|
||||||
// Just merges the results of the previous 3 functions into a standard format
|
|
||||||
export async function getRegisteredEventsWithPlayers(eventId?: number) {
|
export async function getRegisteredEventsWithPlayers(eventId?: number) {
|
||||||
// Get updated events from database
|
|
||||||
let newEvents = await getRegisteredEvents(eventId);
|
let newEvents = await getRegisteredEvents(eventId);
|
||||||
let registeredEventList = newEvents['events'];
|
let registeredEventList = newEvents['events'];
|
||||||
|
|
||||||
// Get all possible brackets from the database
|
|
||||||
let brackets = await getAllBrackets();
|
let brackets = await getAllBrackets();
|
||||||
|
|
||||||
// Initilise the final eventList
|
|
||||||
let fullEventList: any[] = [];
|
let fullEventList: any[] = [];
|
||||||
|
|
||||||
// For every event
|
|
||||||
for (let registeredEvent in registeredEventList) {
|
for (let registeredEvent in registeredEventList) {
|
||||||
let event = registeredEventList[registeredEvent];
|
let event = registeredEventList[registeredEvent];
|
||||||
|
|
||||||
// Get the info about the result preset for the ui
|
|
||||||
let resultPreset = await getResultPreset(event.resultPreset);
|
let resultPreset = await getResultPreset(event.resultPreset);
|
||||||
|
|
||||||
// Get all players for the event
|
|
||||||
let registeredPlayers = await getAllRegisteredEventPlayers(event.id);
|
let registeredPlayers = await getAllRegisteredEventPlayers(event.id);
|
||||||
|
|
||||||
// Map the players into an [] with structure {id: number, name: string, items: any[]}
|
// Group players by bracket category for the frontend
|
||||||
// So that the players are sorted by bracket for the frontend
|
|
||||||
const bracketOrder = brackets.brackets.map((category) => {
|
const bracketOrder = brackets.brackets.map((category) => {
|
||||||
return {
|
return {
|
||||||
...category,
|
...category,
|
||||||
// Filter the items that match the current bracket name
|
|
||||||
items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name)
|
items: registeredPlayers.eventPlayers.filter((item) => item.bracket === category.name)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// append the player info and result preset to the event object
|
|
||||||
let eventWithPlayers = {
|
let eventWithPlayers = {
|
||||||
...event,
|
...event,
|
||||||
registeredPlayers: bracketOrder,
|
registeredPlayers: bracketOrder,
|
||||||
...resultPreset
|
...resultPreset
|
||||||
};
|
};
|
||||||
// combine all of the events into one array
|
|
||||||
fullEventList.push(eventWithPlayers);
|
fullEventList.push(eventWithPlayers);
|
||||||
}
|
}
|
||||||
// Send to client
|
|
||||||
return fullEventList;
|
return fullEventList;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const scorers = sqliteTable('users', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const sessions = sqliteTable('sessions', {
|
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')
|
userId: text('user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => scorers.id),
|
.references(() => scorers.id),
|
||||||
|
|||||||
@@ -13,33 +13,26 @@
|
|||||||
let containerRef = $state<HTMLDivElement | null>(null);
|
let containerRef = $state<HTMLDivElement | null>(null);
|
||||||
let activeId = $state<string | number | null>(null);
|
let activeId = $state<string | number | null>(null);
|
||||||
|
|
||||||
/**
|
/** Scroll a specific row to the center of the viewport */
|
||||||
* Public function to scroll a specific row into the center of the viewport.
|
|
||||||
* Can be called manually by the parent component.
|
|
||||||
*/
|
|
||||||
export function scrollToId(id: string | number) {
|
export function scrollToId(id: string | number) {
|
||||||
if (!containerRef) return;
|
if (!containerRef) return;
|
||||||
|
|
||||||
const targetRow = containerRef.querySelector(`#row-${id}`) as HTMLElement | null;
|
const targetRow = containerRef.querySelector(`#row-${id}`) as HTMLElement | null;
|
||||||
if (targetRow) {
|
if (targetRow) {
|
||||||
// Update local state to highlight the centered row
|
|
||||||
activeId = id;
|
activeId = id;
|
||||||
|
|
||||||
// Calculate the midpoint math to center the row
|
|
||||||
const rowOffsetTop = targetRow.offsetTop;
|
const rowOffsetTop = targetRow.offsetTop;
|
||||||
const rowHeight = targetRow.offsetHeight;
|
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;
|
const centerScrollTarget = rowOffsetTop - containerHeight / 2 + rowHeight / 2;
|
||||||
|
|
||||||
// Using behavior: 'auto' for an instantaneous snap instead of smooth scrolling
|
|
||||||
containerRef.scrollTo({
|
containerRef.scrollTo({
|
||||||
top: centerScrollTarget,
|
top: centerScrollTarget,
|
||||||
behavior: 'auto'
|
behavior: 'auto'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={containerRef} class="table-container" style="max-height: {maxHeight};">
|
<div bind:this={containerRef} class="table-container" style="max-height: {maxHeight};">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const fitText: Action<HTMLElement> = (node) => {
|
|||||||
node.style.transformOrigin = 'top left';
|
node.style.transformOrigin = 'top left';
|
||||||
node.style.transform = 'none';
|
node.style.transform = 'none';
|
||||||
|
|
||||||
// Step 1: fit to height
|
// Fit font size to container height
|
||||||
let size = 1;
|
let size = 1;
|
||||||
node.style.fontSize = size + 'px';
|
node.style.fontSize = size + 'px';
|
||||||
while (node.scrollHeight <= container!.clientHeight) {
|
while (node.scrollHeight <= container!.clientHeight) {
|
||||||
@@ -19,7 +19,7 @@ export const fitText: Action<HTMLElement> = (node) => {
|
|||||||
}
|
}
|
||||||
node.style.fontSize = size - 1 + 'px';
|
node.style.fontSize = size - 1 + 'px';
|
||||||
|
|
||||||
// Step 2: stretch width to fill container
|
// Stretch width to fill container
|
||||||
const scaleX = container!.clientWidth / node.scrollWidth;
|
const scaleX = container!.clientWidth / node.scrollWidth;
|
||||||
node.style.transform = `scaleX(${scaleX})`;
|
node.style.transform = `scaleX(${scaleX})`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getAllInitialInfo } from '$lib/server/databaseManager';
|
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 () => {
|
export const load = async () => {
|
||||||
return await getAllInitialInfo();
|
return await getAllInitialInfo();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,21 +14,18 @@
|
|||||||
let scoreEndpoint: EventSource;
|
let scoreEndpoint: EventSource;
|
||||||
let eventEndpoint: EventSource;
|
let eventEndpoint: EventSource;
|
||||||
|
|
||||||
// Ref map for event card DOM nodes
|
// Map event IDs to their DOM elements for scrolling
|
||||||
let eventRefs = $state<Record<number, HTMLElement>>({});
|
let eventRefs = $state<Record<number, HTMLElement>>({});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Get the teams endpoint
|
// Subscribe to live team score updates via SSE
|
||||||
scoreEndpoint = new EventSource('/api/teams');
|
scoreEndpoint = new EventSource('/api/teams');
|
||||||
// When the endpoint sends something
|
|
||||||
scoreEndpoint.onmessage = (e) => {
|
scoreEndpoint.onmessage = (e) => {
|
||||||
// Parse the json
|
|
||||||
const teamsData = JSON.parse(e.data);
|
const teamsData = JSON.parse(e.data);
|
||||||
// If its teams info, then update the teams thing
|
|
||||||
if (teamsData['teams']) teams = teamsData['teams'];
|
if (teamsData['teams']) teams = teamsData['teams'];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Basically same for events
|
// Subscribe to live event updates via SSE
|
||||||
eventEndpoint = new EventSource('/api/registeredEvents');
|
eventEndpoint = new EventSource('/api/registeredEvents');
|
||||||
eventEndpoint.onmessage = (e) => {
|
eventEndpoint.onmessage = (e) => {
|
||||||
eventTable = JSON.parse(e.data);
|
eventTable = JSON.parse(e.data);
|
||||||
@@ -50,22 +47,18 @@
|
|||||||
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
|
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(() => {
|
$effect(() => {
|
||||||
if (focusEventId == null) return;
|
if (focusEventId == null) return;
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
// Get focused element
|
|
||||||
const el = eventRefs[focusEventId!];
|
const el = eventRefs[focusEventId!];
|
||||||
// Scroll it to the top of the box
|
|
||||||
el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' });
|
el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' });
|
||||||
// Wait for that to finish
|
|
||||||
tick().then(() => {
|
tick().then(() => {
|
||||||
// Scroll the window back to the top
|
|
||||||
window.scrollTo(0, 0);
|
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 marquee(node: HTMLElement) {
|
||||||
function measure() {
|
function measure() {
|
||||||
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
const inner = node.querySelector<HTMLElement>('.marquee-inner');
|
||||||
@@ -88,9 +81,9 @@
|
|||||||
<svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
|
<svelte:window onbeforeunload={() => scoreEndpoint?.close()} />
|
||||||
|
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<!-- ═══════════ LEADERBOARD ═══════════ -->
|
<!-- LEADERBOARD -->
|
||||||
<section class="leaderboard">
|
<section class="leaderboard">
|
||||||
<!-- Winner — always full-width -->
|
<!-- Winner, always full-width -->
|
||||||
{#if leaderboard[0]}
|
{#if leaderboard[0]}
|
||||||
{@const team = leaderboard[0]}
|
{@const team = leaderboard[0]}
|
||||||
<a
|
<a
|
||||||
@@ -122,7 +115,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Runners-up: stretch to fill row, max 5 wide, 1 col on small -->
|
<!-- Runners-up, responsive grid -->
|
||||||
{#if leaderboard.length > 1}
|
{#if leaderboard.length > 1}
|
||||||
<div class="runners-grid" style="--runner-count:{Math.min(leaderboard.length - 1, 5)}">
|
<div class="runners-grid" style="--runner-count:{Math.min(leaderboard.length - 1, 5)}">
|
||||||
{#each leaderboard.slice(1) as team, i (team.name)}
|
{#each leaderboard.slice(1) as team, i (team.name)}
|
||||||
@@ -158,7 +151,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ═══════════ EVENTS TABLE ═══════════ -->
|
<!-- EVENTS TABLE -->
|
||||||
<p class="section-label">Events</p>
|
<p class="section-label">Events</p>
|
||||||
|
|
||||||
<section class="events-scroll">
|
<section class="events-scroll">
|
||||||
|
|||||||
@@ -5,31 +5,26 @@ import * as schema from '$lib/server/db/schema';
|
|||||||
import { getRegisteredEvents } from '$lib/server/databaseManager';
|
import { getRegisteredEvents } from '$lib/server/databaseManager';
|
||||||
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// Decode body
|
|
||||||
let responseBody = await request.json();
|
let responseBody = await request.json();
|
||||||
|
|
||||||
// If there is no request then dont respond
|
|
||||||
if (!responseBody) {
|
if (!responseBody) {
|
||||||
return new Error('nuh uh');
|
return new Error('nuh uh');
|
||||||
} else {
|
} else {
|
||||||
console.log(JSON.stringify(responseBody));
|
console.log(JSON.stringify(responseBody));
|
||||||
if (responseBody.eventId) {
|
if (responseBody.eventId) {
|
||||||
// Get the event
|
|
||||||
let eventData = await getRegisteredEvents(responseBody.eventId);
|
let eventData = await getRegisteredEvents(responseBody.eventId);
|
||||||
|
|
||||||
// If the event hasnt started or ended
|
|
||||||
if (eventData.events[0].state != 1) {
|
if (eventData.events[0].state != 1) {
|
||||||
return new Error();
|
return new Error();
|
||||||
}
|
}
|
||||||
let scoringPreset = eventData.events[0].scoringPreset;
|
let scoringPreset = eventData.events[0].scoringPreset;
|
||||||
|
|
||||||
// make a new main ledger entry
|
// Create ledger entry to record this scoring event
|
||||||
let newLedgerEntry = await db
|
let newLedgerEntry = await db
|
||||||
.insert(schema.mainLedger)
|
.insert(schema.mainLedger)
|
||||||
.values({ registeredEvent: responseBody.eventId })
|
.values({ registeredEvent: responseBody.eventId })
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
// get the id so i can use it in the sub ledgers
|
|
||||||
let ledgerEntryId = newLedgerEntry[0].id;
|
let ledgerEntryId = newLedgerEntry[0].id;
|
||||||
|
|
||||||
function getPoints(
|
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<any, number>();
|
const teamScores = new Map<any, number>();
|
||||||
|
|
||||||
// for every bracket
|
|
||||||
for (let bracket in responseBody.brackets) {
|
for (let bracket in responseBody.brackets) {
|
||||||
// for every player
|
|
||||||
for (let player in responseBody.brackets[bracket].players) {
|
for (let player in responseBody.brackets[bracket].players) {
|
||||||
// variable fun
|
|
||||||
let currentPlayer = responseBody.brackets[bracket].players[player];
|
let currentPlayer = responseBody.brackets[bracket].players[player];
|
||||||
let currentPlayerTeam = currentPlayer.teamId;
|
let currentPlayerTeam = currentPlayer.teamId;
|
||||||
let currentPlayerPosition = currentPlayer.position;
|
let currentPlayerPosition = currentPlayer.position;
|
||||||
|
|
||||||
// If they put in a score / result
|
|
||||||
if (currentPlayerPosition > 0) {
|
if (currentPlayerPosition > 0) {
|
||||||
let score = getPoints(scoringPreset, currentPlayerPosition);
|
let score = getPoints(scoringPreset, currentPlayerPosition);
|
||||||
// If their score is in the preset and they put in a score
|
|
||||||
if (currentPlayer.scores.length > 0) {
|
if (currentPlayer.scores.length > 0) {
|
||||||
if (score > 0) {
|
if (score > 0) {
|
||||||
// Accumulate points for this team instead of inserting right away
|
|
||||||
const currentTeamScore = teamScores.get(currentPlayerTeam) || 0;
|
const currentTeamScore = teamScores.get(currentPlayerTeam) || 0;
|
||||||
teamScores.set(currentPlayerTeam, currentTeamScore + score);
|
teamScores.set(currentPlayerTeam, currentTeamScore + score);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PLACEMENT LOGIC (Left untouched)
|
// Update player placement in the database
|
||||||
let newPlayerPlacement = await db
|
let newPlayerPlacement = await db
|
||||||
.update(schema.registeredPlayers)
|
.update(schema.registeredPlayers)
|
||||||
.set({ placement: currentPlayerPosition })
|
.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) {
|
if (teamScores.size > 0) {
|
||||||
const ledgerEntries = Array.from(teamScores.entries()).map(([teamID, points]) => ({
|
const ledgerEntries = Array.from(teamScores.entries()).map(([teamID, points]) => ({
|
||||||
ledgerID: ledgerEntryId,
|
ledgerID: ledgerEntryId,
|
||||||
@@ -91,7 +80,7 @@ export async function POST({ request }: any) {
|
|||||||
console.log(newScores);
|
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 highestScore = -1;
|
||||||
let winningTeamId = null;
|
let winningTeamId = null;
|
||||||
|
|
||||||
@@ -112,9 +101,7 @@ export async function POST({ request }: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the frontends
|
|
||||||
globalEmitter.emit('scoreUpdate');
|
globalEmitter.emit('scoreUpdate');
|
||||||
// Return a response because
|
|
||||||
return new Response('coolsies');
|
return new Response('coolsies');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,16 @@ import { globalEmitter } from '$lib/server/globalEmitter';
|
|||||||
import { generateEndpoint } from '$lib/server/endpoint';
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
export async function GET({ request }) {
|
export async function GET({ request }) {
|
||||||
const endpoint = generateEndpoint(async (enqueue) => {
|
const endpoint = generateEndpoint(async (enqueue) => {
|
||||||
let eventList = async () => {
|
let eventList = async () => {
|
||||||
// Get eventList with structure from database
|
|
||||||
let newEventList = await getRegisteredEventsWithPlayers();
|
let newEventList = await getRegisteredEventsWithPlayers();
|
||||||
// send to client
|
|
||||||
enqueue(newEventList);
|
enqueue(newEventList);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the eventList to the client when a connection is made
|
// Send initial data on connection, then subscribe to updates
|
||||||
// TODO make it so that this only happens on an initial post request
|
|
||||||
eventList();
|
eventList();
|
||||||
// When the data changes send an update to the client
|
|
||||||
globalEmitter.on('eventUpdate', eventList);
|
globalEmitter.on('eventUpdate', eventList);
|
||||||
|
|
||||||
// Return cleanup function to remove listener when it closes
|
|
||||||
return () => {
|
return () => {
|
||||||
globalEmitter.off('eventUpdate', eventList);
|
globalEmitter.off('eventUpdate', eventList);
|
||||||
};
|
};
|
||||||
@@ -26,18 +21,13 @@ export async function GET({ request }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// Decode body
|
|
||||||
let responseBody = await request.json();
|
let responseBody = await request.json();
|
||||||
|
|
||||||
// If there is no request then dont respond
|
|
||||||
if (!responseBody) {
|
if (!responseBody) {
|
||||||
return new Response('nuh uh');
|
return new Response('nuh uh');
|
||||||
} else {
|
} else {
|
||||||
// Get requested event
|
|
||||||
let eventRequested = responseBody.eventId;
|
let eventRequested = responseBody.eventId;
|
||||||
// request eventList from database
|
|
||||||
let eventList = await getRegisteredEventsWithPlayers(eventRequested);
|
let eventList = await getRegisteredEventsWithPlayers(eventRequested);
|
||||||
// return eventList to client
|
|
||||||
return new Response(JSON.stringify(eventList));
|
return new Response(JSON.stringify(eventList));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { getAllRegisteredEventPlayers } from '$lib/server/databaseManager';
|
|||||||
import { globalEmitter } from '$lib/server/globalEmitter';
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
import { generateEndpoint } from '$lib/server/endpoint';
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
// Expose post request
|
|
||||||
export async function POST({ request }: any) {
|
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');
|
return new Response('ok');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,22 @@ import { getTeams } from '$lib/server/databaseManager';
|
|||||||
import { globalEmitter } from '$lib/server/globalEmitter';
|
import { globalEmitter } from '$lib/server/globalEmitter';
|
||||||
import { generateEndpoint } from '$lib/server/endpoint';
|
import { generateEndpoint } from '$lib/server/endpoint';
|
||||||
|
|
||||||
// Expose post request
|
|
||||||
export async function POST({ request }: any) {
|
export async function POST({ request }: any) {
|
||||||
// When post request recieved increment testscores by 1
|
|
||||||
globalEmitter.emit('incrementScores');
|
globalEmitter.emit('incrementScores');
|
||||||
// Return ok so the frontend is happy
|
|
||||||
return new Response('ok');
|
return new Response('ok');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function GET({ request }) {
|
export async function GET({ request }) {
|
||||||
const endpoint = generateEndpoint(async (enqueue) => {
|
const endpoint = generateEndpoint(async (enqueue) => {
|
||||||
// Function to grab score from database and add it to message queue
|
|
||||||
let newScore = async () => {
|
let newScore = async () => {
|
||||||
let newScores = await getTeams();
|
let newScores = await getTeams();
|
||||||
enqueue(newScores);
|
enqueue(newScores);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial Sync
|
// Send initial team scores, then subscribe to updates
|
||||||
newScore();
|
newScore();
|
||||||
globalEmitter.on('scoreUpdate', newScore);
|
globalEmitter.on('scoreUpdate', newScore);
|
||||||
|
|
||||||
// Simply return the cleanup function here
|
|
||||||
return () => {
|
return () => {
|
||||||
globalEmitter.off('scoreUpdate', newScore);
|
globalEmitter.off('scoreUpdate', newScore);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,20 +44,13 @@
|
|||||||
|
|
||||||
const data = await response.json();
|
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) {
|
if (data && data[0] && data[0].registeredPlayers) {
|
||||||
data[0].registeredPlayers.forEach((bracket: any) => {
|
data[0].registeredPlayers.forEach((bracket: any) => {
|
||||||
bracket.items.sort((a: any, b: 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;
|
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;
|
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;
|
if (b.placement === 0) return -1;
|
||||||
|
|
||||||
// 4. Both have placements -> sort ascending (1st, 2nd, 3rd, etc.)
|
|
||||||
return a.placement - b.placement;
|
return a.placement - b.placement;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
let dropTarget = $state<{ bi: number; pi: number } | null>(null);
|
let dropTarget = $state<{ bi: number; pi: number } | null>(null);
|
||||||
let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
|
let submitStatus = $state<'idle' | 'submitting' | 'done' | 'error'>('idle');
|
||||||
|
|
||||||
// Drag state
|
|
||||||
let dragSrc = $state<{ bi: number; pi: number } | null>(null);
|
let dragSrc = $state<{ bi: number; pi: number } | null>(null);
|
||||||
|
|
||||||
let sortByScore = $state(true);
|
let sortByScore = $state(true);
|
||||||
@@ -93,19 +92,17 @@
|
|||||||
...bracket,
|
...bracket,
|
||||||
items: sortByScore
|
items: sortByScore
|
||||||
? [...bracket.items].sort((a, b) => {
|
? [...bracket.items].sort((a, b) => {
|
||||||
// 1. Check if players actually have committed scores
|
|
||||||
const scoresA = committedScores[a.id]?.filter((s) => s !== null) ?? [];
|
const scoresA = committedScores[a.id]?.filter((s) => s !== null) ?? [];
|
||||||
const scoresB = committedScores[b.id]?.filter((s) => s !== null) ?? [];
|
const scoresB = committedScores[b.id]?.filter((s) => s !== null) ?? [];
|
||||||
|
|
||||||
const hasA = scoresA.length > 0;
|
const hasA = scoresA.length > 0;
|
||||||
const hasB = scoresB.length > 0;
|
const hasB = scoresB.length > 0;
|
||||||
|
|
||||||
// 2. Handle cases where one or both don't have scores
|
// Push unranked players to the bottom, then sort by score
|
||||||
if (!hasA && !hasB) return 0; // Neither have scores, keep order
|
if (!hasA && !hasB) return 0;
|
||||||
if (!hasA) return 1; // 'a' has no score -> push down
|
if (!hasA) return 1;
|
||||||
if (!hasB) return -1; // 'b' has no score -> push down
|
if (!hasB) return -1;
|
||||||
|
|
||||||
// 3. Both have scores, fallback to standard leaderboard sorting
|
|
||||||
const fallback = lowerIsBetter ? Infinity : -Infinity;
|
const fallback = lowerIsBetter ? Infinity : -Infinity;
|
||||||
const sa = useAverage
|
const sa = useAverage
|
||||||
? average(committedScores[a.id] ?? [], fallback)
|
? average(committedScores[a.id] ?? [], fallback)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Leaderboard ── */
|
/* Leaderboard */
|
||||||
.leaderboard {
|
.leaderboard {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
border: 2px solid var(--c);
|
border: 2px solid var(--c);
|
||||||
color: var(--c);
|
color: var(--c);
|
||||||
background: color-mix(in srgb, var(--c) 10%, transparent);
|
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;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
min-height: 56px;
|
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 {
|
.score-ghost {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Foreground */
|
/* Foreground content layer */
|
||||||
.score-fg {
|
.score-fg {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -126,14 +126,7 @@
|
|||||||
font-size: clamp(26px, 7.5vw, 52px);
|
font-size: clamp(26px, 7.5vw, 52px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
.runners-grid {
|
.runners-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -154,8 +147,7 @@
|
|||||||
}
|
}
|
||||||
@media (min-width: 700px) {
|
@media (min-width: 700px) {
|
||||||
.runners-grid {
|
.runners-grid {
|
||||||
/* clamp actual count to 4 on large, but use --runner-count
|
/* Use --runner-count so fewer teams fill the row evenly */
|
||||||
so e.g. 2 teams still fill 2 equal columns not 2 of 4 */
|
|
||||||
grid-template-columns: repeat(min(var(--runner-count), 4), 1fr);
|
grid-template-columns: repeat(min(var(--runner-count), 4), 1fr);
|
||||||
}
|
}
|
||||||
.winner {
|
.winner {
|
||||||
@@ -166,7 +158,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Section label ── */
|
/* Section label */
|
||||||
.section-label {
|
.section-label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: 2.5px;
|
letter-spacing: 2.5px;
|
||||||
@@ -176,7 +168,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Events scrollable container ── */
|
/* Events scrollable container */
|
||||||
.events-scroll {
|
.events-scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
box-shadow: inset 0px 48px 20px -26px rgba(0, 0, 0, 0.35);
|
box-shadow: inset 0px 48px 20px -26px rgba(0, 0, 0, 0.35);
|
||||||
@@ -185,7 +177,7 @@
|
|||||||
/* max-height: 900px; */
|
/* max-height: 900px; */
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 0 14px 24px;
|
padding: 0 14px 24px;
|
||||||
/* Thin custom scrollbar */
|
/* Thin scrollbar styling */
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent;
|
scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent;
|
||||||
}
|
}
|
||||||
@@ -206,7 +198,7 @@
|
|||||||
color: var(--winner-color);
|
color: var(--winner-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Event card ── */
|
/* Event card */
|
||||||
.event-card {
|
.event-card {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||||
@@ -218,7 +210,7 @@
|
|||||||
background-color: color-mix(in srgb, currentColor 18%, transparent);
|
background-color: color-mix(in srgb, currentColor 18%, transparent);
|
||||||
color: var(--ctp-latte-peach);
|
color: var(--ctp-latte-peach);
|
||||||
}
|
}
|
||||||
/* Focus highlight pulse — added/removed by $effect */
|
/* Pulse animation for focused event card */
|
||||||
.event-card.highlight-pulse {
|
.event-card.highlight-pulse {
|
||||||
animation: card-pulse 1.2s ease-out forwards;
|
animation: card-pulse 1.2s ease-out forwards;
|
||||||
}
|
}
|
||||||
@@ -254,7 +246,7 @@
|
|||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Brackets ── */
|
/* Brackets */
|
||||||
.brackets {
|
.brackets {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -271,9 +263,9 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Player boxes ── */
|
/* Player boxes */
|
||||||
.player-box {
|
.player-box {
|
||||||
flex: 1 1 0; /* equal widths, no min-content bias */
|
flex: 1 1 0;
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -287,11 +279,11 @@
|
|||||||
min-height: 52px;
|
min-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 1 col on small screens, 4 across on large */
|
/* Mobile: single column layout */
|
||||||
@media (max-width: 479px) {
|
@media (max-width: 479px) {
|
||||||
.brackets {
|
.brackets {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: stretch; /* was flex-start — lets columns fill full height */
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
.bracket-sep {
|
.bracket-sep {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
@@ -326,7 +318,7 @@
|
|||||||
@media (min-width: 700px) {
|
@media (min-width: 700px) {
|
||||||
.player-box {
|
.player-box {
|
||||||
max-width: calc(25% - 6px);
|
max-width: calc(25% - 6px);
|
||||||
} /* 4 per row */
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-ghost {
|
.player-ghost {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const actions: Actions = {
|
|||||||
return fail(400, { message: 'Username already taken' });
|
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();
|
const userId = crypto.randomUUID();
|
||||||
|
|
||||||
await db.insert(scorers).values({ id: userId, username, passwordHash });
|
await db.insert(scorers).values({ id: userId, username, passwordHash });
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ import adapter from '@sveltejs/adapter-auto';
|
|||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
compilerOptions: {
|
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)
|
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
|
||||||
},
|
},
|
||||||
kit: {
|
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(),
|
adapter: adapter(),
|
||||||
|
|
||||||
typescript: {
|
typescript: {
|
||||||
|
|||||||
Reference in New Issue
Block a user