Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/frontend/src/lib/api/tournament.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ export interface TournamentDetails extends Omit<Tournament, 'participants'> {
/**
* List all tournaments
*/
export async function listTournaments(): Promise<Tournament[]> {
export async function listTournaments(): Promise<Tournament[] | null> {
const { data, error } = await api.get<Tournament[]>('/api/tournament');
if (error) {
console.error('Failed to list tournaments:', error);
return [];
return null;
}
return data ?? [];
return data ?? null;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/lib/api/versus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,13 @@ export async function getAvailablePlayers(): Promise<Player[]> {
/**
* Get user's challenges
*/
export async function getChallenges(): Promise<ChallengesResponse> {
export async function getChallenges(): Promise<ChallengesResponse | null> {
const { data, error } = await api.get<ChallengesResponse>('/api/versus/challenges');
if (error) {
console.error('Failed to get challenges:', error);
return { received: [], sent: [], active: [], completed: [] };
return null;
}
return data ?? { received: [], sent: [], active: [], completed: [] };
return data ?? null;
}

/**
Expand Down
10 changes: 5 additions & 5 deletions apps/frontend/src/lib/components/Card.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
let { variant = 'default', hoverable = false, class: className = '', children }: Props = $props();

const variantStyles: Record<CardVariant, { bg: string; text: string }> = {
default: { bg: '#FFFFFF', text: '#000000' },
cyan: { bg: '#4A8A9A', text: '#FFFFFF' },
magenta: { bg: '#A0527D', text: '#FFFFFF' },
lime: { bg: '#6B9E50', text: '#FFFFFF' },
orange: { bg: '#FF6600', text: '#FFFFFF' }
default: { bg: 'var(--color-white)', text: 'var(--color-black)' },
cyan: { bg: 'var(--color-cyan)', text: 'var(--color-black)' },
magenta: { bg: 'var(--color-magenta)', text: 'var(--color-white)' },
lime: { bg: 'var(--color-lime)', text: 'var(--color-black)' },
orange: { bg: 'var(--color-orange)', text: 'var(--color-black)' }
};

const style = $derived(variantStyles[variant]);
Expand Down
12 changes: 7 additions & 5 deletions apps/frontend/src/lib/components/GameModeCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<div class="flex flex-col h-full relative">
{#if completed}
<div
class="absolute -top-2 -right-2 bg-lime brutal-border px-3 py-1 text-xs font-black uppercase flex items-center gap-1"
class="absolute -top-2 -right-2 bg-lime text-black brutal-border px-3 py-1 text-xs font-black uppercase flex items-center gap-1"
>
<span>Done</span>
</div>
Expand All @@ -46,16 +46,18 @@
<div class="text-5xl mb-6">{emoji}</div>
{/if}
<h3 class="text-2xl font-black mb-4">{title}</h3>
<p class="opacity-80 mb-6 grow leading-relaxed">
<p class="mb-6 grow leading-relaxed">
{description}
</p>
<div class="flex items-center justify-between">
<div class="flex flex-wrap items-center gap-3">
<span
class="brutal-border brutal-shadow-sm bg-white text-black px-4 py-2 text-sm font-bold"
class="brutal-border brutal-shadow-sm bg-white text-black max-w-full px-4 py-2 text-sm font-bold break-words"
>
{stat}
</span>
<span class="font-bold uppercase text-sm group-hover:translate-x-2 transition-transform">
<span
class="font-bold uppercase text-sm whitespace-nowrap group-hover:translate-x-2 transition-transform"
>
{completed ? 'View Results' : cta} →
</span>
</div>
Expand Down
58 changes: 26 additions & 32 deletions apps/frontend/src/lib/components/Header.svelte
Original file line number Diff line number Diff line change
@@ -1,41 +1,35 @@
<script lang="ts">
import { page } from '$app/state';
import Icon from '$lib/components/Icon.svelte';

const navItems = [
{ href: '/menu', label: 'Menu', icon: 'home' },
{ href: '/leaderboard', label: 'Leaderboard', icon: 'trophy' },
{ href: '/settings', label: 'Settings', icon: 'settings' },
{ href: '/about', label: 'About', icon: 'users' }
] as const;
</script>

<header class="header-fixed bg-white brutal-border">
<div class="w-full h-full px-6 flex items-center justify-between">
<a href="/menu">
<img src="/logo.png" alt="TigerSpot Logo" class="inline-block w-40" />
<div class="w-full h-full px-2 sm:px-4 md:px-6 flex items-center justify-between">
<a href="/menu" class="min-h-11 flex shrink-0 items-center">
<img src="/logo.png" alt="TigerSpot home" class="inline-block w-24 sm:w-36 md:w-40" />
</a>
<nav class="flex items-center gap-6">
<a
href="/menu"
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
>
<Icon name="home" class="text-base" />
Menu
</a>
<a
href="/leaderboard"
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
>
<Icon name="trophy" class="text-base" />
Leaderboard
</a>
<a
href="/settings"
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
>
<Icon name="settings" class="text-base" />
Settings
</a>
<a
href="/about"
class="font-bold text-sm uppercase hover:text-orange transition-colors inline-flex items-center gap-2"
>
<Icon name="users" class="text-base" />
About
</a>
<nav aria-label="Primary" class="flex shrink-0 items-center gap-1 md:gap-3 lg:gap-6">
{#each navItems as item}
<a
href={item.href}
title={item.label}
aria-current={page.url.pathname === item.href ? 'page' : undefined}
class="min-h-11 min-w-11 px-2 font-bold text-sm uppercase transition-colors inline-flex items-center justify-center gap-2 {page
.url.pathname === item.href
? 'text-orange-ink'
: 'hover:text-orange-ink'}"
>
<Icon name={item.icon} class="text-lg md:text-base" />
<span class="sr-only md:not-sr-only">{item.label}</span>
</a>
{/each}
</nav>
</div>
</header>
4 changes: 2 additions & 2 deletions apps/frontend/src/lib/components/Icon.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { icons, type IconName } from './icons';
import { icons, type IconDefinition, type IconName } from './icons';

let {
name,
Expand All @@ -15,7 +15,7 @@
style?: string;
} = $props();

const icon = $derived(icons[name]);
const icon: IconDefinition = $derived(icons[name]);
const svgClass = $derived(`inline-block align-middle ${className}`);
const resolvedStrokeWidth = $derived(icon?.strokeWidth ?? strokeWidth);
</script>
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/src/lib/components/StatCard.svelte
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
<script lang="ts">
import Card from './Card.svelte';

type CardVariant = 'cyan' | 'magenta' | 'lime' | 'orange' | 'white';
type CardVariant = 'cyan' | 'magenta' | 'lime' | 'orange';

let {
value,
label,
variant,
valueColor,
valueClass,
class: className = ''
}: {
value: string | number;
label: string;
variant?: CardVariant;
valueColor?: string;
valueClass?: string;
class?: string;
} = $props();

let displayValue = $derived(typeof value === 'number' ? value.toLocaleString() : value);
</script>

<Card {variant} class="text-center py-6 {className}">
<div class="text-3xl md:text-4xl font-black {valueColor || ''}" style={valueColor}>
<div class="text-3xl md:text-4xl font-black {valueClass || ''}">
{displayValue}
</div>
<div class="text-xs font-bold uppercase opacity-60 mt-2">{label}</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/lib/components/icons.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type IconDefinition = {
export type IconDefinition = {
viewBox?: string;
paths?: string[];
circles?: { cx: number; cy: number; r: number }[];
Expand Down
16 changes: 15 additions & 1 deletion apps/frontend/src/lib/stores/user.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class UserStore {
user = $state<User | null>(null);
loading = $state(true);
error = $state<string | null>(null);
private loadPromise: Promise<void> | null = null;

get isAuthenticated() {
return this.user !== null;
Expand All @@ -25,7 +26,20 @@ class UserStore {
/**
* Load user from API (checks auth cookie)
*/
async load() {
async load(): Promise<void> {
if (this.loadPromise) {
return this.loadPromise;
Comment on lines +30 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Force a fresh user load after dev login

If a developer opens and submits the custom dev-login form while the initial /api/auth/me request is still pending, devLoginAs() completes the login POST and calls load(), but this branch reuses the pre-login request instead of fetching the newly authenticated user. The method then reports success and navigates to /menu with user still null, causing an immediate redirect back to the login page. Authentication-changing operations need to bypass or invalidate the in-flight load before refreshing the user.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh not in my scope idc 😇

}

this.loadPromise = this.loadCurrentUser();
try {
await this.loadPromise;
} finally {
this.loadPromise = null;
}
}

private async loadCurrentUser(): Promise<void> {
this.loading = true;
this.error = null;

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/routes/admin/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
return;
}
const [tournamentsData, imagesData] = await Promise.all([listTournaments(), listImages()]);
tournaments = tournamentsData;
tournaments = tournamentsData ?? [];
images = imagesData;
loading = false;
});
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/routes/admin/tournaments/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
goto('/menu');
return;
}
tournaments = await listTournaments();
tournaments = (await listTournaments()) ?? [];
loading = false;
});

Expand All @@ -65,7 +65,7 @@

if (result) {
// Reload tournaments
tournaments = await listTournaments();
tournaments = (await listTournaments()) ?? [];

// Reset form
name = '';
Expand Down Expand Up @@ -108,7 +108,7 @@
const result = await addTestPlayers(tournamentId, testPlayerCount);
if (result?.success) {
// Reload tournaments to update participant count
tournaments = await listTournaments();
tournaments = (await listTournaments()) ?? [];
alert(`Added ${result.addedPlayers.length} test players!`);
} else {
alert('Failed to add test players');
Expand Down
17 changes: 14 additions & 3 deletions apps/frontend/src/routes/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
--color-magenta: #a0527d;
--color-lime: #6b9e50;
--color-orange: #ff6600;
--color-orange-ink: oklch(0.56 0.18 38);

/* Neutrals */
--color-black: #000000;
Expand Down Expand Up @@ -94,7 +95,7 @@ body {
/* Button Colors */
.btn-cyan {
background-color: var(--color-cyan);
color: var(--color-white);
color: var(--color-black);
}

.btn-magenta {
Expand All @@ -104,12 +105,12 @@ body {

.btn-lime {
background-color: var(--color-lime);
color: var(--color-white);
color: var(--color-black);
}

.btn-orange {
background-color: var(--color-orange);
color: var(--color-white);
color: var(--color-black);
}

.btn-white {
Expand Down Expand Up @@ -430,6 +431,16 @@ h6,
outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

/* Scrollbar styling */
::-webkit-scrollbar {
width: 14px;
Expand Down
Loading