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');
await db
.insert(schema.scorers)
.values({ id: crypto.randomUUID(), username: 'admin', passwordHash: passwordHash });
.values({
id: crypto.randomUUID(),
username: 'admin',
role: 'admin',
passwordHash: passwordHash
});
// Seed teams
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) {
const resultPresets = await db
.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">
import './layout.css';
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>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
<div class="header goldman flex h-15 w-full">
<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
>
<div class="w-full"></div>
{#if data.user?.role === 'admin'}
<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"
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>
{@render children()}

View File

@@ -47,6 +47,17 @@
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
$effect(() => {
if (focusEventId == null) return;
@@ -160,7 +171,9 @@
<div
class="event-card"
class:ongoing-event={event.state == 1}
class:completed-event={event.state == 2}
bind:this={eventRefs[event.id]}
style={event.state == 2 ? `--event-color: ${event.winner.color}` : ''}
>
<div class="event-header">
<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>
<div class="bracket-vertical-sep"></div>
</div>
{#each bracket.items as player}
{#each sortPlayers(bracket.items) as player}
<div class="player-box" style="--c:{player.teamColor}">
<div class="player-ghost" aria-hidden="true">
<svg viewBox="0 0.1 100 0.6" preserveAspectRatio="none" class="ghost-svg">
@@ -204,10 +217,10 @@
</svg>
</div>
<div class="player-name-wrap" use:marquee>
<span class="marquee-inner">
<a href="/stats/player/{player.id}" class="marquee-inner">
{player.firstName}
{player.lastName}
</span>
</a>
</div>
{#if player.placement !== 0}
<div class="player-placement goldman">{ordinal(player.placement)}</div>

View File

@@ -10,7 +10,6 @@ export async function POST({ request }: any) {
if (!responseBody) {
return new Error('nuh uh');
} else {
console.log(JSON.stringify(responseBody));
if (responseBody.eventId) {
let eventData = await getRegisteredEvents(responseBody.eventId);
@@ -62,7 +61,6 @@ export async function POST({ request }: any) {
.set({ placement: currentPlayerPosition })
.where(eq(schema.registeredPlayers.id, currentPlayer.registeredPlayerId))
.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();
console.log(newScores);
}
// 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() })
.where(eq(schema.registeredEvents.id, responseBody.eventId))
.returning();
console.log(teamWinnerUpdate);
}
}
globalEmitter.emit('scoreUpdate');
globalEmitter.emit('eventUpdate');
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
style:background-color={event.state === 1
? '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"
>
{event.name} - {event.division} - scoring {#if event.state == 1}- ONGOING
{:else if event.state == 2}- FINISHED
{/if}
</div>

View File

@@ -210,6 +210,12 @@
background-color: color-mix(in srgb, currentColor 18%, transparent);
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 */
.event-card.highlight-pulse {
animation: card-pulse 1.2s ease-out forwards;
@@ -337,9 +343,14 @@
.player-name-wrap {
position: relative;
z-index: 1;
text-decoration: none;
overflow: hidden;
white-space: nowrap;
}
.player-name-wrap:hover {
text-decoration: underline;
}
.marquee-inner {
display: inline-block;
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 { scorers } from '$lib/server/db/schema';
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 = {
default: async (event) => {

View File

@@ -1,9 +1,20 @@
<script lang="ts">
import type { ActionData } from './$types';
import type { ActionData, PageData } from './$types';
export let data: PageData;
export let form: ActionData;
</script>
<div class="auth-card">
{#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">
@@ -29,6 +40,7 @@
<button type="submit">Log in</button>
</form>
{/if}
<!-- <p class="switch">No account? <a href="/signup">Sign up</a></p> -->
</div>

View File

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

View File

@@ -9,12 +9,19 @@
<form method="POST">
<label>
Username
<input name="username" type="text" autocomplete="username" required />
<input name="username" class="text-black" type="text" autocomplete="username" required />
</label>
<label>
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>
</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;
});