Compare commits

..

5 Commits

7 changed files with 230 additions and 411 deletions

View File

@@ -1,37 +1,37 @@
event_type,division,event_state,time_completed event_type,division,event_state,winner,time_completed
100m Sprint,Year 7,0, 100m Sprint,Year 7,0,,
100m Sprint,Year 8,0, 100m Sprint,Year 8,0,,
100m Sprint,Year 9,0, 100m Sprint,Year 9,2,East,
100m Sprint,Year 10,1, 100m Sprint,Year 10,1,,
200m Sprint,Year 7,0, 200m Sprint,Year 7,0,,
200m Sprint,Year 8,0, 200m Sprint,Year 8,0,,
200m Sprint,Year 9,0, 200m Sprint,Year 9,0,,
200m Sprint,Year 10,0, 200m Sprint,Year 10,0,,
500m Relay,Year 7,0, 500m Relay,Year 7,0,,
500m Relay,Year 8,0, 500m Relay,Year 8,0,,
500m Relay,Year 9,0, 500m Relay,Year 9,0,,
500m Relay,Year 10,0, 500m Relay,Year 10,0,,
750m Race,Year 7,0, 750m Race,Year 7,0,,
750m Race,Year 8,0, 750m Race,Year 8,0,,
750m Race,Year 9,0, 750m Race,Year 9,0,,
750m Race,Year 10,0, 750m Race,Year 10,0,,
1000m Long Distance,Year 7,0, 1000m Long Distance,Year 7,0,,
1000m Long Distance,Year 8,0, 1000m Long Distance,Year 8,0,,
1000m Long Distance,Year 9,0, 1000m Long Distance,Year 9,0,,
1000m Long Distance,Year 10,0, 1000m Long Distance,Year 10,0,,
Javelin,Year 7,0, Javelin,Year 7,0,,
Javelin,Year 8,0, Javelin,Year 8,0,,
Javelin,Year 9,0, Javelin,Year 9,0,,
Javelin,Year 10,0, Javelin,Year 10,0,,
Shotput,Year 7,0, Shotput,Year 7,0,,
Shotput,Year 8,0, Shotput,Year 8,0,,
Shotput,Year 9,0, Shotput,Year 9,0,,
Shotput,Year 10,0, Shotput,Year 10,0,,
Long Jump,Year 7,0, Long Jump,Year 7,0,,
Long Jump,Year 8,0, Long Jump,Year 8,0,,
Long Jump,Year 9,0, Long Jump,Year 9,0,,
Long Jump,Year 10,0, Long Jump,Year 10,0,,
Triple Jump,Year 7,0, Triple Jump,Year 7,0,,
Triple Jump,Year 8,0, Triple Jump,Year 8,0,,
Triple Jump,Year 9,0, Triple Jump,Year 9,0,,
Triple Jump,Year 10,0, Triple Jump,Year 10,0,,
1 event_type division event_state winner time_completed
2 100m Sprint Year 7 0
3 100m Sprint Year 8 0
4 100m Sprint Year 9 0 2 East
5 100m Sprint Year 10 1
6 200m Sprint Year 7 0
7 200m Sprint Year 8 0
8 200m Sprint Year 9 0
9 200m Sprint Year 10 0
10 500m Relay Year 7 0
11 500m Relay Year 8 0
12 500m Relay Year 9 0
13 500m Relay Year 10 0
14 750m Race Year 7 0
15 750m Race Year 8 0
16 750m Race Year 9 0
17 750m Race Year 10 0
18 1000m Long Distance Year 7 0
19 1000m Long Distance Year 8 0
20 1000m Long Distance Year 9 0
21 1000m Long Distance Year 10 0
22 Javelin Year 7 0
23 Javelin Year 8 0
24 Javelin Year 9 0
25 Javelin Year 10 0
26 Shotput Year 7 0
27 Shotput Year 8 0
28 Shotput Year 9 0
29 Shotput Year 10 0
30 Long Jump Year 7 0
31 Long Jump Year 8 0
32 Long Jump Year 9 0
33 Long Jump Year 10 0
34 Triple Jump Year 7 0
35 Triple Jump Year 8 0
36 Triple Jump Year 9 0
37 Triple Jump Year 10 0

View File

@@ -149,6 +149,7 @@ async function seed() {
for (const row of registeredEventsCSV) { for (const row of registeredEventsCSV) {
const eventTypeId = eventTypeMap.get(row.event_type); const eventTypeId = eventTypeMap.get(row.event_type);
const teamId = teamMap.get(row.winner);
const divisionId = divisionMap.get(row.division); const divisionId = divisionMap.get(row.division);
if (!eventTypeId) throw new Error(`Event Type "${row.event_type}" not found`); if (!eventTypeId) throw new Error(`Event Type "${row.event_type}" not found`);
@@ -160,10 +161,13 @@ async function seed() {
eventType: eventTypeId, eventType: eventTypeId,
division: divisionId, division: divisionId,
state: row.event_state || 0, state: row.event_state || 0,
timeCompleted: row.time_completed || null timeCompleted: row.time_completed || null,
teamWinner: teamId || null
}) })
.returning(); .returning();
console.log(` → Registered Event [id:${inserted.id}]: ${row.event_type} | ${row.division}`); console.log(
` → 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 the textual event name (e.g., "100m Sprint") to the generated DB ID
eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id); eventNameMap.set(`${row.event_type}|${row.division}`, inserted.id);

View File

@@ -11,8 +11,11 @@ export async function getAllInitialInfo() {
} }
// Get teams object from database // Get teams object from database
export async function getTeams() { export async function getTeams(teamId?: number) {
const allTeams = await db.select().from(schema.teamScoresView); const allTeams = await db
.select()
.from(schema.teamScoresView)
.where(teamId ? eq(schema.teamScoresView.teamId, teamId) : undefined);
for (let team in allTeams) { for (let team in allTeams) {
let currentTeam = allTeams[team]; let currentTeam = allTeams[team];
if (!currentTeam.totalPoints) { if (!currentTeam.totalPoints) {
@@ -30,21 +33,29 @@ export async function getTeams() {
// Get all registered events from database // Get all registered events from database
export async function getRegisteredEvents(eventId?: number) { export async function getRegisteredEvents(eventId?: number) {
async function getWinnerInfo(teamId: number) {
const teamInfo = await getTeams(teamId);
return teamInfo.teams[0];
}
const allEvents = await db const allEvents = await db
.select() .select()
.from(schema.registeredEventsView) .from(schema.registeredEventsView)
// If event id specified, get that event, otherwise get all events
.where(eventId ? eq(schema.registeredEventsView.eventId, eventId) : undefined); .where(eventId ? eq(schema.registeredEventsView.eventId, eventId) : undefined);
return {
events: allEvents.map((events) => ({ const events = await Promise.all(
allEvents.map(async (events) => ({
id: events.eventId, id: events.eventId,
name: events.eventName, name: events.eventName,
division: events.division, division: events.division,
state: events.state, state: events.state,
completed: events.timeCompleted || 0, completed: events.timeCompleted || 0,
resultPreset: events.resultPreset resultPreset: events.resultPreset,
winner: events.winner ? await getWinnerInfo(events.winner) : 'UNDECIDED'
})) }))
}; );
return { events };
} }
// Get all players with an event id specified // Get all players with an event id specified

View File

@@ -63,7 +63,8 @@ export const registeredEvents = sqliteTable('registeredEvents', {
.references(() => divisions.id) .references(() => divisions.id)
.notNull(), .notNull(),
state: integer('event_state').notNull().default(0), state: integer('event_state').notNull().default(0),
timeCompleted: integer('time_completed') timeCompleted: integer('time_completed'),
teamWinner: integer('event_team_winner').references(() => teams.id)
}); });
export const registeredPlayers = sqliteTable('registeredPlayers', { export const registeredPlayers = sqliteTable('registeredPlayers', {
@@ -131,11 +132,12 @@ export const registeredEventsView = sqliteView('registeredEventsView').as((qb) =
division: divisions.name, division: divisions.name,
state: registeredEvents.state, state: registeredEvents.state,
timeCompleted: registeredEvents.timeCompleted, timeCompleted: registeredEvents.timeCompleted,
winner: registeredEvents.teamWinner,
resultPreset: eventTypes.resultPreset resultPreset: eventTypes.resultPreset
}) })
.from(registeredEvents) .from(registeredEvents)
.innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id)) .innerJoin(eventTypes, eq(registeredEvents.eventType, eventTypes.id))
.innerJoin(divisions, eq(registeredEvents.division, divisions.id)) .innerJoin(divisions, eq(registeredEvents.division, divisions.id));
}); });
export const registeredEventPlayersView = sqliteView('registeredEventPlayersView').as((qb) => { export const registeredEventPlayersView = sqliteView('registeredEventPlayersView').as((qb) => {

View File

@@ -18,14 +18,21 @@
let eventRefs = $state<Record<number, HTMLElement>>({}); let eventRefs = $state<Record<number, HTMLElement>>({});
onMount(() => { onMount(() => {
// Get the teams endpoint
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
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);
console.log(eventTable);
}; };
}); });
@@ -44,20 +51,18 @@
} }
// When focusEventId changes, scroll to and highlight that event card // When focusEventId changes, scroll to and highlight that event card
// TODO make this scrolling shit work idk
$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!];
const container = el?.closest('.events-scroll') as HTMLElement; // Scroll it to the top of the box
if (el && container) { el.scrollIntoView({ alignToTop: true, behavior: 'instant', container: 'nearest' });
const elMid = el.offsetTop + el.offsetHeight / 2; // Wait for that to finish
const targetScroll = elMid - container.clientHeight / 2 - 60; tick().then(() => {
container.scrollTo({ top: targetScroll, behavior: 'smooth' }); // Scroll the window back to the top
el.classList.remove('highlight-pulse'); window.scrollTo(0, 0);
void el.offsetWidth; });
el.classList.add('highlight-pulse');
}
}); });
}); });
// Svelte action: measures text overflow and drives CSS marquee // Svelte action: measures text overflow and drives CSS marquee
@@ -159,10 +164,22 @@
<section class="events-scroll"> <section class="events-scroll">
<div class="events"> <div class="events">
{#each eventTable as event (event.id)} {#each eventTable as event (event.id)}
<div class="event-card" bind:this={eventRefs[event.id]}> <div
class="event-card"
class:ongoing-event={event.state == 1}
bind:this={eventRefs[event.id]}
>
<div class="event-header"> <div class="event-header">
<a href="/event/{event.id}" class="event-name goldman">{event.name}</a> <a href="/event/{event.id}" class="event-name goldman">{event.name}</a>
<span class="event-division">{event.division}</span> <span class="event-division">{event.division}</span>
{#if event.state == 1}
<span class="event-status goldman">ONGOING</span>
{:else if event.state == 2}
<span
class="event-winner event-status goldman"
style="--winner-color:{event.winner.color}">Won By {event.winner.name}</span
>
{/if}
</div> </div>
<div class="brackets"> <div class="brackets">
@@ -211,355 +228,3 @@
</div> </div>
</section> </section>
</div> </div>
<!-- <style> -->
<!-- @import url('https://fonts.googleapis.com/css2?family=Black+Ops+One&display=swap'); -->
<!---->
<!-- :global(html), -->
<!-- :global(body) { -->
<!-- min-height: 100vh; -->
<!-- } -->
<!---->
<!-- :global(body > div), /* SvelteKit's root wrapper */ -->
<!-- :global(#svelte) { -->
<!-- min-height: 100vh; -->
<!-- display: flex; -->
<!-- flex-direction: column; -->
<!-- } -->
<!---->
<!-- .page { -->
<!-- display: flex; -->
<!-- flex-direction: column; -->
<!-- max-height: 150vh; -->
<!-- max-width: 1000px; -->
<!-- margin: 0 auto; -->
<!-- width: 100%; -->
<!-- } -->
<!-- .goldman { -->
<!-- font-family: 'Black Ops One', system-ui; -->
<!-- font-weight: 400; -->
<!-- } -->
<!---->
<!-- /* ── Leaderboard ── */ -->
<!-- .leaderboard { -->
<!-- display: flex; -->
<!-- flex-direction: column; -->
<!-- gap: 10px; -->
<!-- padding: 14px 14px 6px; -->
<!-- } -->
<!---->
<!-- .score-box { -->
<!-- position: relative; -->
<!-- overflow: hidden; -->
<!-- border-radius: 14px; -->
<!-- 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 */ -->
<!-- display: grid; -->
<!-- grid-template-columns: 1fr; -->
<!-- grid-template-rows: 1fr; -->
<!-- text-decoration: none; -->
<!-- transition: filter 0.15s ease; -->
<!-- } -->
<!-- .score-box:hover { -->
<!-- filter: brightness(1.15); -->
<!-- } -->
<!-- .score-box > * { -->
<!-- grid-column: 1; -->
<!-- grid-row: 1; -->
<!-- } -->
<!---->
<!-- .winner { -->
<!-- aspect-ratio: 2 / 1; -->
<!-- border-width: 3px; -->
<!-- min-height: 80px; -->
<!-- } -->
<!-- .runner { -->
<!-- aspect-ratio: 4 / 1; -->
<!-- min-height: 56px; -->
<!-- } -->
<!---->
<!-- /* Ghost SVG: fill the cell edge-to-edge, text flush to bottom */ -->
<!-- .score-ghost { -->
<!-- width: 100%; -->
<!-- height: 100%; -->
<!-- opacity: 0.18; -->
<!-- fill: currentColor; -->
<!-- } -->
<!-- .ghost-svg { -->
<!-- width: 100%; -->
<!-- height: 100%; -->
<!-- display: block; -->
<!-- } -->
<!---->
<!-- /* Foreground */ -->
<!-- .score-fg { -->
<!-- position: relative; -->
<!-- z-index: 1; -->
<!-- display: flex; -->
<!-- align-items: center; -->
<!-- justify-content: space-between; -->
<!-- padding: 0 14px; -->
<!-- gap: 8px; -->
<!-- } -->
<!-- .score-meta { -->
<!-- display: flex; -->
<!-- flex-direction: column; -->
<!-- min-width: 0; -->
<!-- flex: 1; -->
<!-- } -->
<!-- .score-rank { -->
<!-- font-size: 10px; -->
<!-- letter-spacing: 1.5px; -->
<!-- text-transform: uppercase; -->
<!-- opacity: 0.5; -->
<!-- line-height: 1; -->
<!-- margin-bottom: 3px; -->
<!-- } -->
<!-- .score-name { -->
<!-- font-size: clamp(11px, 3vw, 20px); -->
<!-- white-space: nowrap; -->
<!-- overflow: hidden; -->
<!-- text-overflow: ellipsis; -->
<!-- } -->
<!-- .score-pts { -->
<!-- font-size: clamp(20px, 5.5vw, 40px); -->
<!-- letter-spacing: 2px; -->
<!-- flex-shrink: 0; -->
<!-- } -->
<!-- .winner .score-name { -->
<!-- font-size: clamp(16px, 5vw, 32px); -->
<!-- } -->
<!-- .winner .score-pts { -->
<!-- font-size: clamp(26px, 7.5vw, 52px); -->
<!-- } -->
<!---->
<!-- /* -->
<!-- * Runners-up grid: -->
<!-- * - 1 column on small screens (<480px) -->
<!-- * - 2 columns on medium (480699px) -->
<!-- * - 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 { -->
<!-- display: grid; -->
<!-- gap: 10px; -->
<!-- grid-template-columns: 1fr; -->
<!-- } -->
<!-- @media (min-width: 480px) { -->
<!-- .runners-grid { -->
<!-- grid-template-columns: repeat(2, 1fr); -->
<!-- } -->
<!---->
<!-- .winner { -->
<!-- aspect-ratio: 2 / 1; -->
<!-- } -->
<!---->
<!-- .runner { -->
<!-- aspect-ratio: 2 / 1; -->
<!-- } -->
<!-- } -->
<!-- @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 */ -->
<!-- grid-template-columns: repeat(min(var(--runner-count), 4), 1fr); -->
<!-- } -->
<!-- .winner { -->
<!-- aspect-ratio: 3 / 1; -->
<!-- } -->
<!-- .runner { -->
<!-- aspect-ratio: 3 / 2; -->
<!-- } -->
<!-- } -->
<!---->
<!-- /* ── Section label ── */ -->
<!-- .section-label { -->
<!-- font-size: 10px; -->
<!-- letter-spacing: 2.5px; -->
<!-- text-transform: uppercase; -->
<!-- opacity: 0.4; -->
<!-- padding: 12px 16px 4px; -->
<!-- margin: 0; -->
<!-- } -->
<!---->
<!-- /* ── Events scrollable container ── */ -->
<!-- .events-scroll { -->
<!-- flex: 1; -->
<!-- overflow-y: auto; -->
<!-- /* max-height: 900px; */ -->
<!-- min-height: 0; -->
<!-- padding: 0 14px 24px; -->
<!-- /* Thin custom scrollbar */ -->
<!-- scrollbar-width: thin; -->
<!-- scrollbar-color: color-mix(in srgb, currentColor 30%, transparent) transparent; -->
<!-- } -->
<!-- .events { -->
<!-- display: flex; -->
<!-- flex-direction: column; -->
<!-- gap: 10px; -->
<!-- } -->
<!---->
<!-- /* ── Event card ── */ -->
<!-- .event-card { -->
<!-- border-radius: 12px; -->
<!-- border: 1px solid color-mix(in srgb, currentColor 18%, transparent); -->
<!-- overflow: hidden; -->
<!-- transition: box-shadow 0.3s ease; -->
<!-- } -->
<!-- /* Focus highlight pulse — added/removed by $effect */ -->
<!-- .event-card.highlight-pulse { -->
<!-- animation: card-pulse 1.2s ease-out forwards; -->
<!-- } -->
<!-- @keyframes card-pulse { -->
<!-- 0% { -->
<!-- box-shadow: 0 0 0 3px currentColor; -->
<!-- } -->
<!-- 100% { -->
<!-- box-shadow: 0 0 0 0px currentColor; -->
<!-- } -->
<!-- } -->
<!---->
<!-- .event-header { -->
<!-- display: flex; -->
<!-- align-items: baseline; -->
<!-- gap: 10px; -->
<!-- padding: 9px 13px 7px; -->
<!-- border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent); -->
<!-- flex-wrap: wrap; -->
<!-- } -->
<!-- .event-name { -->
<!-- font-size: 15px; -->
<!-- text-decoration: none; -->
<!-- color: inherit; -->
<!-- } -->
<!-- .event-name:hover { -->
<!-- text-decoration: underline; -->
<!-- } -->
<!-- .event-division { -->
<!-- font-size: 10px; -->
<!-- letter-spacing: 1.2px; -->
<!-- text-transform: uppercase; -->
<!-- opacity: 0.4; -->
<!-- } -->
<!---->
<!-- /* ── Brackets ── */ -->
<!-- .brackets { -->
<!-- display: flex; -->
<!-- flex-direction: column; -->
<!-- } -->
<!-- .brackets-name { -->
<!-- font-size: 20px; -->
<!-- letter-spacing: 1.2px; -->
<!-- text-transform: uppercase; -->
<!-- opacity: 0.4; -->
<!-- margin-left: 1px; -->
<!-- } -->
<!-- .brackets-name-text { -->
<!-- display: table-cell; -->
<!-- vertical-align: middle; -->
<!-- } -->
<!-- .bracket-vertical-sep { -->
<!-- width: 1px; -->
<!-- height: 100%; -->
<!-- background: var(--ctp-mocha-surface2); -->
<!-- margin: 10px 5px; -->
<!-- } -->
<!-- .bracket-sep { -->
<!-- height: 1px; -->
<!-- background: color-mix(in srgb, currentColor 10%, transparent); -->
<!-- margin: 0 10px; -->
<!-- } -->
<!-- .bracket-row { -->
<!-- display: flex; -->
<!-- gap: 6px; -->
<!-- padding: 7px 9px; -->
<!-- flex-wrap: wrap; -->
<!-- } -->
<!---->
<!-- /* ── Player boxes ── */ -->
<!-- .player-box { -->
<!-- flex: 1 1 0; /* equal widths, no min-content bias */ -->
<!-- max-width: 160px; -->
<!-- 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; -->
<!-- min-height: 52px; -->
<!-- } -->
<!---->
<!-- /* 1 col on small screens, 4 across on large */ -->
<!-- @media (max-width: 479px) { -->
<!-- .bracket-row { -->
<!-- flex-direction: column; -->
<!-- } -->
<!-- .player-box { -->
<!-- max-width: 100%; -->
<!-- } -->
<!-- } -->
<!-- @media (min-width: 700px) { -->
<!-- .player-box { -->
<!-- max-width: calc(25% - 6px); -->
<!-- } /* 4 per row */ -->
<!-- } -->
<!---->
<!-- .player-ghost { -->
<!-- position: absolute; -->
<!-- inset: 0; -->
<!-- opacity: 0.15; -->
<!-- fill: currentColor; -->
<!-- pointer-events: none; -->
<!-- } -->
<!-- .ghost-svg { -->
<!-- width: 100%; -->
<!-- height: 100%; -->
<!-- display: block; -->
<!-- } -->
<!---->
<!-- .player-name-wrap { -->
<!-- position: relative; -->
<!-- z-index: 1; -->
<!-- overflow: hidden; -->
<!-- white-space: nowrap; -->
<!-- } -->
<!-- .marquee-inner { -->
<!-- display: inline-block; -->
<!-- font-size: 11px; -->
<!-- font-weight: 600; -->
<!-- white-space: nowrap; -->
<!-- } -->
<!-- .marquee-inner.scrolling { -->
<!-- animation: marquee-scroll 7s ease-in-out infinite; -->
<!-- } -->
<!-- @keyframes marquee-scroll { -->
<!-- 0%, -->
<!-- 20% { -->
<!-- transform: translateX(0); -->
<!-- } -->
<!-- 70%, -->
<!-- 90% { -->
<!-- transform: translateX(var(--scroll-dist, 0px)); -->
<!-- } -->
<!-- 100% { -->
<!-- transform: translateX(0); -->
<!-- } -->
<!-- } -->
<!---->
<!-- .player-placement { -->
<!-- position: relative; -->
<!-- z-index: 1; -->
<!-- font-size: 17px; -->
<!-- line-height: 1.1; -->
<!-- margin-top: 2px; -->
<!-- } -->
<!-- .player-placement-gap { -->
<!-- height: 20px; -->
<!-- } -->
<!-- </style> -->

View File

@@ -0,0 +1,92 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import type { PageProps } from './$types';
let { params }: PageProps = $props();
function ordinal(n: number) {
const s = ['th', 'st', 'nd', 'rd'];
const v = n % 100;
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
}
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() };
}
let eventId = params.eventId;
let eventEndpoint: EventSource;
async function getEventData() {
let response = await fetch('/api/registeredEvents', {
method: 'POST',
body: JSON.stringify({
eventId: eventId
}),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
});
return response.json();
}
let eventDataPromise = getEventData();
onMount(() => {
eventEndpoint = new EventSource('/api/registeredEvents');
// eventEndpoint.onmessage = (e) => {
// const eventData = JSON.parse(e.data);
// console.log(eventData);
// };
});
</script>
{#await eventDataPromise}
<div>loading</div>
{:then eventData}
{@const event = eventData[0]}
<div class="flex justify-center">
<div class="w-full flex-col px-[5vw] text-center">
{console.log(event)}
<div class="align-text-middle h-10 w-full bg-red-500">
{event.name} - {event.division} - scoring
</div>
<div class="flex flex-row justify-center">
{#each event.registeredPlayers as bracket, bi}
<div class="w-full min-w-0">
<div class="brackets-name text-bold">
<span class="brackets-name-text">{bracket.name}</span>
<div class=""></div>
</div>
{#each bracket.items as player}
<div class="" style="color:{player.teamColor}">
<div class="player-name-wrap" use:marquee>
{player.firstName}
{player.lastName}
</div>
</div>
{/each}
</div>
{/each}
</div>
</div>
</div>
{/await}
<style>
</style>

View File

@@ -179,6 +179,8 @@
/* ── 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);
border-radius: 25px;
overflow-y: auto; overflow-y: auto;
/* max-height: 900px; */ /* max-height: 900px; */
min-height: 0; min-height: 0;
@@ -193,6 +195,17 @@
gap: 10px; gap: 10px;
} }
.event-status {
align-self: center;
justify-self: end;
text-align: end;
flex: 1;
}
.event-winner {
color: var(--winner-color);
}
/* ── Event card ── */ /* ── Event card ── */
.event-card { .event-card {
border-radius: 12px; border-radius: 12px;
@@ -200,6 +213,11 @@
overflow: hidden; overflow: hidden;
transition: box-shadow 0.3s ease; transition: box-shadow 0.3s ease;
} }
.ongoing-event {
background-color: color-mix(in srgb, currentColor 18%, transparent);
color: var(--ctp-latte-peach);
}
/* Focus highlight pulse — added/removed by $effect */ /* Focus highlight pulse — added/removed by $effect */
.event-card.highlight-pulse { .event-card.highlight-pulse {
animation: card-pulse 1.2s ease-out forwards; animation: card-pulse 1.2s ease-out forwards;
@@ -271,13 +289,40 @@
/* 1 col on small screens, 4 across on large */ /* 1 col on small screens, 4 across on large */
@media (max-width: 479px) { @media (max-width: 479px) {
.brackets {
flex-direction: row;
align-items: stretch; /* was flex-start — lets columns fill full height */
}
.bracket-sep {
width: 1px;
height: auto;
margin: 10px 0;
align-self: stretch;
}
.bracket-row { .bracket-row {
flex-direction: column; flex-direction: column;
min-width: 0;
flex: 1;
} }
.player-box { .player-box {
max-width: 100%; max-width: 100%;
flex: 1; /* equal height across all player boxes in the column */
}
.bracket-row {
flex: 1;
flex-direction: column;
align-items: stretch; /* player boxes fill the full column width */
}
.brackets-name {
text-align: center;
align-items: center;
}
.brackets-name-text {
text-align: center;
} }
} }
@media (min-width: 700px) { @media (min-width: 700px) {
.player-box { .player-box {
max-width: calc(25% - 6px); max-width: calc(25% - 6px);