lots of styling changes and some random fixes

This commit is contained in:
2026-07-05 13:15:08 +01:00
parent f93f407d66
commit ac4f02f597
8 changed files with 340 additions and 346 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy, tick } from 'svelte'; import { onMount, onDestroy, tick } from 'svelte';
import { flip } from 'svelte/animate';
let { let {
data data
@@ -132,6 +133,7 @@
{#each leaderboard.slice(1) as team, i (team.name)} {#each leaderboard.slice(1) as team, i (team.name)}
<a <a
href="/" href="/"
animate:flip={{ duration: 300 }}
class="score-box runner" class="score-box runner"
style="--c:{team.color}" style="--c:{team.color}"
aria-label="{team.name}, {ordinal(i + 2)} place, {team.points} points" aria-label="{team.name}, {ordinal(i + 2)} place, {team.points} points"
@@ -169,6 +171,7 @@
<div class="events"> <div class="events">
{#each eventTable as event (event.id)} {#each eventTable as event (event.id)}
<div <div
animate:flip={{ duration: 300 }}
class="event-card" class="event-card"
class:ongoing-event={event.state == 1} class:ongoing-event={event.state == 1}
class:completed-event={event.state == 2} class:completed-event={event.state == 2}

View File

@@ -44,7 +44,6 @@
const data = await response.json(); const data = await response.json();
// 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) => {
@@ -63,34 +62,27 @@
onMount(() => { onMount(() => {
eventEndpoint = new EventSource('/api/registeredEvents'); eventEndpoint = new EventSource('/api/registeredEvents');
// eventEndpoint.onmessage = (e) => {
// const eventData = JSON.parse(e.data);
// console.log(eventData);
// };
}); });
</script> </script>
{#await eventDataPromise} {#await eventDataPromise}
<div>loading</div> <div class="page-container"><div>loading</div></div>
{:then eventData} {:then eventData}
{@const event = eventData[0]} {@const event = eventData[0]}
{console.log(event)} <div class="page-container">
<div class="flex justify-center"> <div class="event-card" class:ongoing-event={event.state == 1} class:completed-event={event.state == 2}
<div class="w-full flex-col px-[5vw] text-center"> style={event.state == 2 ? `--event-color: ${event.winner?.color ?? 'currentColor'}` : ''}>
<div <div class="event-header">
style:background-color={event.state === 1 <span class="event-name goldman">{event.name}</span>
? 'color-mix(in srgb, #fe640b 18%, transparent)' <span class="event-division">{event.division}</span>
: event.state === 2 {#if event.state == 1}
? 'color-mix(in srgb, #a6e3a1 18%, transparent)' <span class="event-status goldman">ONGOING</span>
: ''} {:else if event.state == 2}
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1" <span class="event-status goldman">FINISHED</span>
>
{event.name} - {event.division}
{#if event.state == 1}- ONGOING
{:else if event.state == 2}- FINISHED
{/if} {/if}
</div> </div>
<div class="brackets px-[5vw]">
{#each event.registeredPlayers as bracket, bi} {#each event.registeredPlayers as bracket, bi}
{#if bi > 0} {#if bi > 0}
<div class="bracket-sep" aria-hidden="true"></div> <div class="bracket-sep" aria-hidden="true"></div>
@@ -116,16 +108,16 @@
</svg> </svg>
</div> </div>
<div class="player-name-wrap" use:marquee> <div class="player-name-wrap" use:marquee>
<span class=" text-xl"> <a href="/stats/player/{player.id}" class="marquee-inner">
{player.firstName} {player.firstName}
{player.lastName} {player.lastName}
</span> </a>
</div> </div>
{#if player.placement !== 0} {#if player.placement !== 0}
<div class="player-placement goldman">{ordinal(player.placement)}</div> <div class="player-placement goldman">{ordinal(player.placement)}</div>
<div class="resultContainer flex justify-center"> <div class="flex flex-col justify-center">
{#each player.playerScores as score} {#each player.playerScores as score}
<div class="mx-5 my-1 rounded border-2" style="border-color:{player.teamColor}"> <div class="mx-1 my-0.5 rounded border px-1 text-xs" style="border-color:{player.teamColor}">
Run {score.resultIndex + 1}: {score.result}{event.resultPresets[0].unit} Run {score.resultIndex + 1}: {score.result}{event.resultPresets[0].unit}
</div> </div>
{/each} {/each}
@@ -139,6 +131,7 @@
{/each} {/each}
</div> </div>
</div> </div>
{#if data.user} {#if data.user}
<div class="mt-10 flex w-full justify-center"> <div class="mt-10 flex w-full justify-center">
<a <a
@@ -147,15 +140,5 @@
> >
</div> </div>
{/if} {/if}
</div>
{/await} {/await}
<style>
.resultContainer {
flex-direction: column;
}
@media (max-width: 479px) {
.resultContainer {
flex-direction: column;
}
}
</style>

View File

@@ -17,6 +17,13 @@
flex-direction: column; flex-direction: column;
} }
.page-container {
max-width: 1000px;
margin: 0 auto;
width: 100%;
padding: 14px;
}
.page { .page {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -51,7 +58,7 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
text-decoration: none; text-decoration: none;
transition: filter 0.15s ease; transition: aspect-ratio 0.3s ease, border-width 0.3s ease, min-height 0.3s ease, filter 0.15s ease;
} }
.score-box:hover { .score-box:hover {
filter: brightness(1.15); filter: brightness(1.15);
@@ -65,10 +72,12 @@
aspect-ratio: 2 / 1; aspect-ratio: 2 / 1;
border-width: 3px; border-width: 3px;
min-height: 80px; min-height: 80px;
transition: aspect-ratio 0.3s ease, border-width 0.3s ease, min-height 0.3s ease;
} }
.runner { .runner {
aspect-ratio: 4 / 1; aspect-ratio: 4 / 1;
min-height: 56px; min-height: 56px;
transition: aspect-ratio 0.3s ease, min-height 0.3s ease;
} }
/* Ghost SVG fills cell, text sits at bottom */ /* Ghost SVG fills cell, text sits at bottom */
@@ -90,40 +99,83 @@
z-index: 1; z-index: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
padding: 0 14px; padding: 0 14px;
gap: 8px; min-width: 0;
} }
.score-meta { .score-meta {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
overflow: hidden;
} }
.score-rank { .score-rank {
font-size: 10px; font-size: clamp(14px, 2.5vw, 17px);
letter-spacing: 1.5px; letter-spacing: 1.2px;
text-transform: uppercase; text-transform: uppercase;
opacity: 0.5; opacity: 0.85;
line-height: 1; line-height: 1;
margin-bottom: 3px; margin-bottom: 2px;
white-space: nowrap;
text-shadow: 0 0 6px rgb(0 0 0 / 50%);
} }
.score-name { .score-name {
font-size: clamp(11px, 3vw, 20px); font-size: clamp(20px, 5.5vw, 40px);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 100%;
} }
.score-pts { .score-pts {
font-size: clamp(20px, 5.5vw, 40px); font-size: clamp(20px, 5.5vw, 40px);
letter-spacing: 2px; letter-spacing: 2px;
flex-shrink: 0; position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
z-index: 2;
text-shadow: 0 0 8px rgb(0 0 0 / 80%), 0 0 4px rgb(0 0 0 / 60%);
}
.winner .score-meta {
flex-direction: column;
align-items: flex-start;
}
.winner .score-rank {
font-size: clamp(16px, 3vw, 20px);
opacity: 0.85;
margin-bottom: 3px;
} }
.winner .score-name { .winner .score-name {
font-size: clamp(16px, 5vw, 32px); font-size: clamp(34px, 9vw, 64px);
max-width: 70%;
} }
.winner .score-pts { .winner .score-pts {
font-size: clamp(26px, 7.5vw, 52px); font-size: clamp(34px, 9vw, 64px);
}
/* Smallest screens: one-line for runners, bigger text to fill height */
@media (max-width: 479px) {
.score-meta {
flex-direction: row;
align-items: center;
gap: 10px;
}
.score-rank {
margin-bottom: 0;
}
.score-name {
font-size: clamp(18px, 5vw, 28px);
}
.score-pts {
font-size: clamp(22px, 6vw, 34px);
}
.winner .score-name {
font-size: clamp(26px, 7vw, 42px);
max-width: 60%;
}
.winner .score-pts {
font-size: clamp(28px, 8vw, 48px);
}
} }
/* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */ /* Responsive runners-up: 1 col <480px, 2 cols 480-699px, 4 cols ≥700px */
@@ -131,6 +183,7 @@
display: grid; display: grid;
gap: 10px; gap: 10px;
grid-template-columns: 1fr; grid-template-columns: 1fr;
transition: gap 0.3s ease;
} }
@media (min-width: 480px) { @media (min-width: 480px) {
.runners-grid { .runners-grid {
@@ -256,17 +309,20 @@
.brackets { .brackets {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transition: gap 0.3s ease, align-items 0.3s ease;
} }
.bracket-sep { .bracket-sep {
height: 1px; height: 1px;
background: color-mix(in srgb, currentColor 10%, transparent); background: color-mix(in srgb, currentColor 10%, transparent);
margin: 0 10px; margin: 0 10px;
transition: height 0.3s ease, width 0.3s ease, margin 0.3s ease;
} }
.bracket-row { .bracket-row {
display: flex; display: flex;
gap: 6px; gap: 6px;
padding: 7px 9px; padding: 7px 9px;
flex-wrap: wrap; flex-wrap: wrap;
transition: gap 0.3s ease, padding 0.3s ease, min-width 0.3s ease, flex 0.3s ease;
} }
/* Player boxes */ /* Player boxes */
@@ -283,6 +339,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 52px; min-height: 52px;
transition: max-width 0.3s ease, flex 0.3s ease, min-height 0.3s ease;
} }
/* Mobile: single column layout */ /* Mobile: single column layout */
@@ -384,3 +441,76 @@
.player-placement-gap { .player-placement-gap {
height: 50px; height: 50px;
} }
/* Shared card pattern for auth, settings, etc. */
.card {
border-radius: 12px;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.card h1 {
font-size: 1.25rem;
font-weight: 700;
margin: 0;
}
.card label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.85rem;
opacity: 0.8;
}
.card input,
.card select {
padding: 0.5rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 6px;
background: color-mix(in srgb, currentColor 6%, transparent);
color: inherit;
font-size: 0.9rem;
}
.card input:focus,
.card select:focus {
outline: none;
border-color: currentColor;
}
.card button {
padding: 0.5rem 1rem;
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
border-radius: 6px;
background: color-mix(in srgb, currentColor 10%, transparent);
color: inherit;
cursor: pointer;
font-size: 0.9rem;
transition: filter 0.15s ease;
}
.card button:hover {
filter: brightness(1.15);
}
.card button:disabled {
opacity: 0.4;
cursor: default;
}
.card .error {
color: var(--ctp-mocha-red, #f38ba8);
font-size: 0.85rem;
margin: 0;
}
.card .switch {
font-size: 0.85rem;
text-align: center;
opacity: 0.7;
}
.card .switch a {
color: inherit;
}
.card .switch a:hover {
text-decoration: underline;
}
.card small {
opacity: 0.6;
font-size: 0.8rem;
}

View File

@@ -67,12 +67,13 @@
}); });
</script> </script>
<div class="p-4"> <div class="page-container">
<h2 class="mb-4 text-lg font-bold">Manual Ledger Entry</h2> <h2 class="section-label">Manual Ledger Entry</h2>
<div class="mb-4 flex flex-wrap gap-4 items-end"> <div class="card">
<div class="flex flex-wrap gap-4 items-end">
<div> <div>
<label class="block text-sm mb-1">Event</label> <label class="block text-sm mb-1">Event</label>
<select bind:value={selectedEventId} class="border rounded px-2 py-1"> <select bind:value={selectedEventId}>
<option value="">Select event...</option> <option value="">Select event...</option>
{#each data.events as event (event.id)} {#each data.events as event (event.id)}
<option value={event.id}>{event.name} {event.division}</option> <option value={event.id}>{event.name} {event.division}</option>
@@ -86,18 +87,26 @@
type="number" type="number"
value={inputPoints[team] ?? '0'} value={inputPoints[team] ?? '0'}
oninput={(e) => { inputPoints[team] = (e.target as HTMLInputElement).value; }} oninput={(e) => { inputPoints[team] = (e.target as HTMLInputElement).value; }}
class="border rounded px-2 py-1 w-20" style="width: 5rem;"
/> />
</div> </div>
{/each} {/each}
<button <button onclick={submitEntry} disabled={!selectedEventId || submitting}>
onclick={submitEntry}
disabled={!selectedEventId || submitting}
class="border rounded px-4 py-1 bg-green-700 text-white disabled:opacity-50"
>
Submit Submit
</button> </button>
</div> </div>
</div>
<div class="section-label" style="padding-bottom: 4px;">
Ledger Entries
<button
onclick={() => loadEntries(!showAll)}
style="margin-left: 0.5rem; padding: 0.15rem 0.5rem; font-size: 0.75rem; border: 1px solid color-mix(in srgb, currentColor 18%, transparent); border-radius: 4px; background: transparent; color: inherit; cursor: pointer;"
>
{showAll ? 'Show 100 most recent' : 'Load all entries'}
</button>
<span class="text-sm opacity-60" style="margin-left: 0.5rem;">{entries.length} entries</span>
</div>
<Table data={entries} maxHeight="none"> <Table data={entries} maxHeight="none">
{#snippet header()} {#snippet header()}
@@ -121,14 +130,4 @@
<td class="px-2">{formatTime(entry.timestamp)}</td> <td class="px-2">{formatTime(entry.timestamp)}</td>
{/snippet} {/snippet}
</Table> </Table>
<div class="mt-4">
<button
onclick={() => loadEntries(!showAll)}
class="border rounded px-4 py-1"
>
{showAll ? 'Show 100 most recent' : 'Load all entries'}
</button>
<span class="ml-2 text-sm">{entries.length} entries shown</span>
</div>
</div> </div>

View File

@@ -4,7 +4,8 @@
export let form: ActionData; export let form: ActionData;
</script> </script>
<div class="auth-card"> <div class="page-container">
<div class="card" style="max-width: 360px; margin: 3rem auto;">
{#if data.user} {#if data.user}
<div class="align-center flex h-full w-full flex-col text-center"> <div class="align-center flex h-full w-full flex-col text-center">
<h1>Already logged in</h1> <h1>Already logged in</h1>
@@ -20,18 +21,12 @@
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input class="text-black" name="username" type="text" autocomplete="username" required /> <input name="username" type="text" autocomplete="username" required />
</label> </label>
<label> <label>
Password Password
<input <input name="password" type="password" autocomplete="current-password" required />
class="text-black"
name="password"
type="password"
autocomplete="current-password"
required
/>
</label> </label>
{#if form?.message} {#if form?.message}
@@ -40,51 +35,8 @@
<button type="submit">Log in</button> <button type="submit">Log in</button>
</form> </form>
<p class="switch">No account? <a href="/register">Sign up</a></p>
{/if} {/if}
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
</div> </div>
</div>
<style>
.auth-card {
max-width: 320px;
margin: 4rem auto;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
button {
padding: 0.6rem;
border: none;
border-radius: 4px;
background: #333;
color: #fff;
cursor: pointer;
}
.error {
color: #c0392b;
font-size: 0.85rem;
margin: 0;
}
.switch {
margin-top: 1rem;
font-size: 0.85rem;
text-align: center;
}
</style>

View File

@@ -4,7 +4,8 @@
export let form: ActionData; export let form: ActionData;
</script> </script>
<div class="auth-card"> <div class="page-container">
<div class="card" style="max-width: 360px; margin: 3rem auto;">
<h1>Sign up</h1> <h1>Sign up</h1>
{#if data.disabled} {#if data.disabled}
@@ -13,19 +14,12 @@
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input name="username" class="text-black" type="text" autocomplete="username" required /> <input name="username" type="text" autocomplete="username" required />
</label> </label>
<label> <label>
Password Password
<input <input name="password" type="password" autocomplete="new-password" minlength="8" required />
class="text-black"
name="password"
type="password"
autocomplete="new-password"
minlength="8"
required
/>
<small>At least 8 characters</small> <small>At least 8 characters</small>
</label> </label>
@@ -39,50 +33,4 @@
<p class="switch">Already have an account? <a href="/login">Log in</a></p> <p class="switch">Already have an account? <a href="/login">Log in</a></p>
{/if} {/if}
</div> </div>
</div>
<style>
.auth-card {
max-width: 320px;
margin: 4rem auto;
padding: 2rem;
border: 1px solid #ddd;
border-radius: 8px;
}
form {
display: flex;
flex-direction: column;
gap: 1rem;
}
label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.9rem;
}
input {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
small {
color: #888;
}
button {
padding: 0.6rem;
border: none;
border-radius: 4px;
background: #333;
color: #fff;
cursor: pointer;
}
.error {
color: #c0392b;
font-size: 0.85rem;
margin: 0;
}
.switch {
margin-top: 1rem;
font-size: 0.85rem;
text-align: center;
}
</style>

View File

@@ -5,21 +5,21 @@
let { data }: { data: PageData } = $props(); let { data }: { data: PageData } = $props();
</script> </script>
<div class="p-4"> <div class="page-container">
<h2 class="mb-4 text-lg font-bold">Settings</h2> <h2 class="section-label">Settings</h2>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<form method="POST" action="?/toggleRegistration" use:enhance> <form method="POST" action="?/toggleRegistration" use:enhance>
<div class="flex items-center gap-4 border rounded p-4"> <div class="card" style="flex-direction: row; align-items: center;">
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold">Disable Registration</p> <p class="font-semibold">Disable Registration</p>
<p class="text-sm opacity-70">Prevents new users from signing up via /register</p> <p class="text-sm opacity-70">Prevents new users from signing up via /register</p>
</div> </div>
<button <button
type="submit" type="submit"
class="border rounded px-4 py-1 {data.disableRegistration style={data.disableRegistration
? 'bg-red-700 text-white' ? 'background: color-mix(in srgb, var(--ctp-mocha-red) 30%, transparent); border-color: var(--ctp-mocha-red);'
: 'bg-green-700 text-white'}" : 'background: color-mix(in srgb, var(--ctp-mocha-green) 30%, transparent); border-color: var(--ctp-mocha-green);'}
> >
{data.disableRegistration ? 'Disabled' : 'Enabled'} {data.disableRegistration ? 'Disabled' : 'Enabled'}
</button> </button>
@@ -27,16 +27,16 @@
</form> </form>
<form method="POST" action="?/toggleLeaderboardLock" use:enhance> <form method="POST" action="?/toggleLeaderboardLock" use:enhance>
<div class="flex items-center gap-4 border rounded p-4"> <div class="card" style="flex-direction: row; align-items: center;">
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold">Require Login for Leaderboard</p> <p class="font-semibold">Require Login for Leaderboard</p>
<p class="text-sm opacity-70">Non-logged-in users will be redirected to /login</p> <p class="text-sm opacity-70">Non-logged-in users will be redirected to /login</p>
</div> </div>
<button <button
type="submit" type="submit"
class="border rounded px-4 py-1 {data.requireLoginForLeaderboard style={data.requireLoginForLeaderboard
? 'bg-red-700 text-white' ? 'background: color-mix(in srgb, var(--ctp-mocha-red) 30%, transparent); border-color: var(--ctp-mocha-red);'
: 'bg-green-700 text-white'}" : 'background: color-mix(in srgb, var(--ctp-mocha-green) 30%, transparent); border-color: var(--ctp-mocha-green);'}
> >
{data.requireLoginForLeaderboard ? 'Locked' : 'Public'} {data.requireLoginForLeaderboard ? 'Locked' : 'Public'}
</button> </button>

View File

@@ -23,12 +23,12 @@
</script> </script>
{#await data.playerInfo} {#await data.playerInfo}
<div>Loading...</div> <div class="page-container"><div>Loading...</div></div>
{:then playerInfo} {:then playerInfo}
{console.log(playerInfo)} <div class="page-container">
<div class="mt-5 flex justify-center"> <div class="flex justify-center">
<div <div
class="player-single-box aspect-square w-full max-w-[95vw] justify-center md:aspect-2/1 lg:aspect-2/1" class="player-box aspect-square w-full max-w-[95vw] md:aspect-2/1 lg:aspect-2/1"
style="--c:{playerInfo.teamInfo.color}" style="--c:{playerInfo.teamInfo.color}"
> >
<div class="player-ghost" aria-hidden="true"> <div class="player-ghost" aria-hidden="true">
@@ -50,26 +50,5 @@
</div> </div>
</div> </div>
</div> </div>
</div>
{/await} {/await}
<style>
.player-single-box {
flex: 1 1 0;
position: relative;
overflow: hidden;
border-radius: 8px;
border: 1.5px solid var(--c);
color: var(--c);
background: color-mix(in srgb, var(--c) 10%, transparent);
padding: 5px 7px 5px;
display: flex;
flex-direction: column;
}
.player-name-wrap {
position: relative;
z-index: 1;
text-decoration: none;
overflow: hidden;
white-space: nowrap;
}
</style>