fixed login and logout and started on player screen

This commit is contained in:
2026-06-30 17:11:37 +01:00
parent 201821d53c
commit c5473fec5c
18 changed files with 205 additions and 41 deletions

View File

@@ -53,7 +53,12 @@ async function seed() {
let passwordHash = await Bun.password.hash('password'); let passwordHash = await Bun.password.hash('password');
await db await db
.insert(schema.scorers) .insert(schema.scorers)
.values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash }); .values({
id: crypto.randomUUID(),
username: 'admin',
role: 'admin',
passwordHash: passwordHash
});
// Seed teams // Seed teams
const teamsCSV = readCSV('teams.csv'); const teamsCSV = readCSV('teams.csv');

View File

@@ -127,6 +127,11 @@ export async function getAllBrackets() {
}; };
} }
export async function getPlayerInfo(playerId: number) {
const playerInfo = await db.select().from(schema.players).where(eq(schema.players.id), playerId);
return playerInfo;
}
export async function getResultPreset(presetId?: number) { export async function getResultPreset(presetId?: number) {
const resultPresets = await db const resultPresets = await db
.select() .select()

View File

@@ -0,0 +1,15 @@
// alternative, if role isn't on locals.user
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) return { user: null };
const [row] = await db
.select({ role: scorers.role })
.from(scorers)
.where(eq(scorers.id, locals.user.id));
return { user: { ...locals.user, role: row?.role ?? 'scorer' } };
};

View File

@@ -1,22 +1,41 @@
<script lang="ts"> <script lang="ts">
import './layout.css'; import './layout.css';
import favicon from '$lib/assets/favicon.svg'; import favicon from '$lib/assets/favicon.svg';
import type { LayoutData } from './$types';
let { children } = $props(); let { children, data }: { children: import('svelte').Snippet; data: LayoutData } = $props();
</script> </script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head> <svelte:head><link rel="icon" href={favicon} /></svelte:head>
<div class="header goldman flex h-15 w-full"> <div class="header goldman flex h-15 w-full">
<a <a
class="align-text-middle justify-left mx-3 my-1 h-auto content-center rounded-sm border-2 border-solid border-red-500 px-2" class="align-text-middle justify-left mx-3 my-1 h-auto content-center rounded-sm border-2
border-solid border-red-500 px-2"
href="/">home</a href="/">home</a
> >
<div class="w-full"></div> <div class="w-full"></div>
<a
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2 border-solid border-red-500 px-2" {#if data.user?.role === 'admin'}
href="/login">login</a <a
> class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
border-solid border-red-500 px-2"
href="/ledger">ledger</a
>
{/if}
{#if data.user}
<a
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
border-solid border-red-500 px-2"
href="/login">logout</a
>
{:else}
<a
class="align-text-middle justify-right mx-3 my-1 h-auto content-center rounded-sm border-2
border-solid border-red-500 px-2"
href="/login">login</a
>
{/if}
</div> </div>
{@render children()} {@render children()}

View File

@@ -47,6 +47,17 @@
return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]); return n + (s[(v - 20) % 10] ?? s[v] ?? s[0]);
} }
function sortPlayers(items: any) {
return [...items].sort((a, b) => {
// If a is unranked and b is ranked, move a down
if (a.placement === 0 && b.placement !== 0) return 1;
// If a is ranked and b is unranked, move a up
if (a.placement !== 0 && b.placement === 0) return -1;
// If both are ranked, sort numerically ascending (1st, 2nd, 3rd...)
return a.placement - b.placement;
});
}
// Scroll to and highlight the currently ongoing event // Scroll to and highlight the currently ongoing event
$effect(() => { $effect(() => {
if (focusEventId == null) return; if (focusEventId == null) return;
@@ -160,7 +171,9 @@
<div <div
class="event-card" class="event-card"
class:ongoing-event={event.state == 1} class:ongoing-event={event.state == 1}
class:completed-event={event.state == 2}
bind:this={eventRefs[event.id]} bind:this={eventRefs[event.id]}
style={event.state == 2 ? `--event-color: ${event.winner.color}` : ''}
> >
<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>
@@ -188,7 +201,7 @@
<span class="brackets-name-text align-text-middle">{bracket.name}</span> <span class="brackets-name-text align-text-middle">{bracket.name}</span>
<div class="bracket-vertical-sep"></div> <div class="bracket-vertical-sep"></div>
</div> </div>
{#each bracket.items as player} {#each sortPlayers(bracket.items) as player}
<div class="player-box" style="--c:{player.teamColor}"> <div class="player-box" style="--c:{player.teamColor}">
<div class="player-ghost" aria-hidden="true"> <div class="player-ghost" aria-hidden="true">
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg"> <svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
@@ -204,10 +217,10 @@
</svg> </svg>
</div> </div>
<div class="player-name-wrap" use:marquee> <div class="player-name-wrap" use:marquee>
<span class="marquee-inner"> <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>

View File

@@ -10,7 +10,6 @@ export async function POST({ request }: any) {
if (!responseBody) { if (!responseBody) {
return new Error('nuh uh'); return new Error('nuh uh');
} else { } else {
console.log(JSON.stringify(responseBody));
if (responseBody.eventId) { if (responseBody.eventId) {
let eventData = await getRegisteredEvents(responseBody.eventId); let eventData = await getRegisteredEvents(responseBody.eventId);
@@ -62,7 +61,6 @@ export async function POST({ request }: any) {
.set({ placement: currentPlayerPosition }) .set({ placement: currentPlayerPosition })
.where(eq(schema.registeredPlayers.id, currentPlayer.registeredPlayerId)) .where(eq(schema.registeredPlayers.id, currentPlayer.registeredPlayerId))
.returning(); .returning();
console.log(newPlayerPlacement[0].placement, currentPlayer.firstName);
} }
} }
} }
@@ -77,7 +75,6 @@ export async function POST({ request }: any) {
})); }));
let newScores = await db.insert(schema.scoreLedger).values(ledgerEntries).returning(); let newScores = await db.insert(schema.scoreLedger).values(ledgerEntries).returning();
console.log(newScores);
} }
// Determine the winning team from accumulated scores // Determine the winning team from accumulated scores
@@ -97,11 +94,11 @@ export async function POST({ request }: any) {
.set({ teamWinner: winningTeamId, state: 2, timeCompleted: Date.now() }) .set({ teamWinner: winningTeamId, state: 2, timeCompleted: Date.now() })
.where(eq(schema.registeredEvents.id, responseBody.eventId)) .where(eq(schema.registeredEvents.id, responseBody.eventId))
.returning(); .returning();
console.log(teamWinnerUpdate);
} }
} }
globalEmitter.emit('scoreUpdate'); globalEmitter.emit('scoreUpdate');
globalEmitter.emit('eventUpdate');
return new Response('coolsies'); return new Response('coolsies');
} }
} }

View File

@@ -0,0 +1,12 @@
import { redirect } from '@sveltejs/kit';
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async (event) => {
if (event.locals.session) {
await invalidateSession(event.locals.session.id);
}
deleteSessionTokenCookie(event);
throw redirect(303, '/login');
};

View File

@@ -251,10 +251,13 @@
<div <div
style:background-color={event.state === 1 style:background-color={event.state === 1
? 'color-mix(in srgb, #fe640b 18%, transparent)' ? 'color-mix(in srgb, #fe640b 18%, transparent)'
: ''} : event.state === 2
? 'color-mix(in srgb, #a6e3a1 18%, transparent)'
: ''}
class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1" class="align-text-middle my-7 h-10 w-full rounded-2xl border-2 border-solid border-ctp-surface1"
> >
{event.name} - {event.division} - scoring {#if event.state == 1}- ONGOING {event.name} - {event.division} - scoring {#if event.state == 1}- ONGOING
{:else if event.state == 2}- FINISHED
{/if} {/if}
</div> </div>

View File

@@ -210,6 +210,12 @@
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);
} }
.completed-event {
border-color: var(--event-color);
/* If you want a subtle background tint like ongoing events usually have: */
background-color: color-mix(in srgb, var(--event-color) 10%, transparent);
}
/* Pulse animation for focused event card */ /* 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;
@@ -337,9 +343,14 @@
.player-name-wrap { .player-name-wrap {
position: relative; position: relative;
z-index: 1; z-index: 1;
text-decoration: none;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
.player-name-wrap:hover {
text-decoration: underline;
}
.marquee-inner { .marquee-inner {
display: inline-block; display: inline-block;
font-size: 11px; font-size: 11px;

View File

@@ -0,0 +1,23 @@
// src/routes/admin/+page.server.ts
import { error, redirect } from '@sveltejs/kit';
import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
const [row] = await db
.select({ role: scorers.role })
.from(scorers)
.where(eq(scorers.id, locals.user.id));
if (row?.role !== 'admin') {
throw error(403, 'Forbidden');
}
return { user: locals.user };
};

View File

@@ -0,0 +1 @@
<div>some</div>

View File

@@ -4,7 +4,13 @@ import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema'; import { scorers } from '$lib/server/db/schema';
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth'; import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from './$types'; import type { Actions } from '../login/$types';
import type { PageServerLoad } from '../login/$types';
export const load: PageServerLoad = async ({ locals }) => {
return { user: locals.user };
};
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {

View File

@@ -1,34 +1,46 @@
<script lang="ts"> <script lang="ts">
import type { ActionData } from './$types'; import type { ActionData, PageData } from './$types';
export let data: PageData;
export let form: ActionData; export let form: ActionData;
</script> </script>
<div class="auth-card"> <div class="auth-card">
<h1>Log in</h1> {#if data.user}
<div class="align-center flex h-full w-full flex-col text-center">
<h1>Already logged in</h1>
<p>You're signed in as <strong>{data.user.username}</strong></p>
<a href="/">Go to home</a>
<form method="POST" action="/api/logout">
<button type="submit">Log out</button>
</form>
</div>
{:else}
<h1>Log in</h1>
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input class="text-black" name="username" type="text" autocomplete="username" required /> <input class="text-black" name="username" type="text" autocomplete="username" required />
</label> </label>
<label> <label>
Password Password
<input <input
class="text-black" class="text-black"
name="password" name="password"
type="password" type="password"
autocomplete="current-password" autocomplete="current-password"
required required
/> />
</label> </label>
{#if form?.message} {#if form?.message}
<p class="error">{form.message}</p> <p class="error">{form.message}</p>
{/if} {/if}
<button type="submit">Log in</button> <button type="submit">Log in</button>
</form> </form>
{/if}
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> --> <!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
</div> </div>

View File

@@ -4,7 +4,7 @@ import { eq } from 'drizzle-orm';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { scorers } from '$lib/server/db/schema'; import { scorers } from '$lib/server/db/schema';
import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth'; import { generateSessionToken, createSession, setSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from './$types'; import type { Actions } from '../signup/$types';
export const actions: Actions = { export const actions: Actions = {
default: async (event) => { default: async (event) => {

View File

@@ -9,12 +9,19 @@
<form method="POST"> <form method="POST">
<label> <label>
Username Username
<input name="username" type="text" autocomplete="username" required /> <input name="username" class="text-black" type="text" autocomplete="username" required />
</label> </label>
<label> <label>
Password Password
<input name="password" type="password" autocomplete="new-password" minlength="8" required /> <input
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>

View File

@@ -0,0 +1,7 @@
import { getAllInitialInfo } from '$lib/server/databaseManager';
// Provide initial data for the home page
export const load = async () => {
return await getAllInitialInfo();
};

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { PageProps } from './$types';
import { getPlayerData } from './data.remote';
let { data, params }: PageProps = $props();
console.log(data);
let some = await getPlayerData(params.playerId);
console.log(some)
let playerId = params.playerId;
console.log(parseInt(playerId));
</script>
<div class="my-3 flex flex-col justify-center">
<div class="player-box w-full" style="--c:red">
<!-- <span>{console.log(getPlayerInfo(playerId))}</span> -->
</div>
</div>

View File

@@ -0,0 +1,8 @@
import { query } from '$app/server';
import { getPlayerInfo } from '$lib/server/databaseManager';
export const getPlayerData = query(async (playerId: number) => {
const playerInfo = await getPlayerInfo(playerId);
return playerInfo;
});