From 961417fbc290bd5c3e2f0d86733e292a3ac2d420 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Fri, 14 Aug 2026 13:52:04 -0400 Subject: [PATCH 1/2] (fix) harden and polish TigerSpot homepage --- apps/frontend/src/lib/api/tournament.ts | 6 +- apps/frontend/src/lib/api/versus.ts | 6 +- apps/frontend/src/lib/components/Card.svelte | 10 +- .../src/lib/components/GameModeCard.svelte | 12 +- .../frontend/src/lib/components/Header.svelte | 58 ++++---- apps/frontend/src/lib/components/Icon.svelte | 4 +- .../src/lib/components/StatCard.svelte | 8 +- apps/frontend/src/lib/components/icons.ts | 2 +- apps/frontend/src/lib/stores/user.svelte.ts | 16 ++- apps/frontend/src/routes/admin/+page.svelte | 2 +- .../src/routes/admin/tournaments/+page.svelte | 6 +- apps/frontend/src/routes/layout.css | 17 ++- apps/frontend/src/routes/menu/+page.svelte | 132 ++++++++++++++---- .../src/routes/tournament/+page.svelte | 2 +- apps/frontend/src/routes/versus/+page.svelte | 27 ++-- 15 files changed, 204 insertions(+), 104 deletions(-) diff --git a/apps/frontend/src/lib/api/tournament.ts b/apps/frontend/src/lib/api/tournament.ts index 46d4363..f99c680 100644 --- a/apps/frontend/src/lib/api/tournament.ts +++ b/apps/frontend/src/lib/api/tournament.ts @@ -54,13 +54,13 @@ export interface TournamentDetails extends Omit { /** * List all tournaments */ -export async function listTournaments(): Promise { +export async function listTournaments(): Promise { const { data, error } = await api.get('/api/tournament'); if (error) { console.error('Failed to list tournaments:', error); - return []; + return null; } - return data ?? []; + return data ?? null; } /** diff --git a/apps/frontend/src/lib/api/versus.ts b/apps/frontend/src/lib/api/versus.ts index 75ec505..8236669 100644 --- a/apps/frontend/src/lib/api/versus.ts +++ b/apps/frontend/src/lib/api/versus.ts @@ -132,13 +132,13 @@ export async function getAvailablePlayers(): Promise { /** * Get user's challenges */ -export async function getChallenges(): Promise { +export async function getChallenges(): Promise { const { data, error } = await api.get('/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; } /** diff --git a/apps/frontend/src/lib/components/Card.svelte b/apps/frontend/src/lib/components/Card.svelte index fdc650d..ff9c1c5 100644 --- a/apps/frontend/src/lib/components/Card.svelte +++ b/apps/frontend/src/lib/components/Card.svelte @@ -13,11 +13,11 @@ let { variant = 'default', hoverable = false, class: className = '', children }: Props = $props(); const variantStyles: Record = { - 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]); diff --git a/apps/frontend/src/lib/components/GameModeCard.svelte b/apps/frontend/src/lib/components/GameModeCard.svelte index 9b76b11..33f4733 100644 --- a/apps/frontend/src/lib/components/GameModeCard.svelte +++ b/apps/frontend/src/lib/components/GameModeCard.svelte @@ -35,7 +35,7 @@
{#if completed}
Done
@@ -46,16 +46,18 @@
{emoji}
{/if}

{title}

-

+

{description}

-
+
{stat} - + {completed ? 'View Results' : cta} →
diff --git a/apps/frontend/src/lib/components/Header.svelte b/apps/frontend/src/lib/components/Header.svelte index d00a87a..d18198e 100644 --- a/apps/frontend/src/lib/components/Header.svelte +++ b/apps/frontend/src/lib/components/Header.svelte @@ -1,41 +1,35 @@
-
diff --git a/apps/frontend/src/lib/components/Icon.svelte b/apps/frontend/src/lib/components/Icon.svelte index 157eced..c667a41 100644 --- a/apps/frontend/src/lib/components/Icon.svelte +++ b/apps/frontend/src/lib/components/Icon.svelte @@ -1,5 +1,5 @@ diff --git a/apps/frontend/src/lib/components/StatCard.svelte b/apps/frontend/src/lib/components/StatCard.svelte index 7ec30b0..b7ceece 100644 --- a/apps/frontend/src/lib/components/StatCard.svelte +++ b/apps/frontend/src/lib/components/StatCard.svelte @@ -1,19 +1,19 @@ -
+
{displayValue}
{label}
diff --git a/apps/frontend/src/lib/components/icons.ts b/apps/frontend/src/lib/components/icons.ts index e9d9faf..40cb071 100644 --- a/apps/frontend/src/lib/components/icons.ts +++ b/apps/frontend/src/lib/components/icons.ts @@ -1,4 +1,4 @@ -type IconDefinition = { +export type IconDefinition = { viewBox?: string; paths?: string[]; circles?: { cx: number; cy: number; r: number }[]; diff --git a/apps/frontend/src/lib/stores/user.svelte.ts b/apps/frontend/src/lib/stores/user.svelte.ts index ffaed4d..3907e7c 100644 --- a/apps/frontend/src/lib/stores/user.svelte.ts +++ b/apps/frontend/src/lib/stores/user.svelte.ts @@ -5,6 +5,7 @@ class UserStore { user = $state(null); loading = $state(true); error = $state(null); + private loadPromise: Promise | null = null; get isAuthenticated() { return this.user !== null; @@ -25,7 +26,20 @@ class UserStore { /** * Load user from API (checks auth cookie) */ - async load() { + async load(): Promise { + if (this.loadPromise) { + return this.loadPromise; + } + + this.loadPromise = this.loadCurrentUser(); + try { + await this.loadPromise; + } finally { + this.loadPromise = null; + } + } + + private async loadCurrentUser(): Promise { this.loading = true; this.error = null; diff --git a/apps/frontend/src/routes/admin/+page.svelte b/apps/frontend/src/routes/admin/+page.svelte index cfff9f3..1c420a1 100644 --- a/apps/frontend/src/routes/admin/+page.svelte +++ b/apps/frontend/src/routes/admin/+page.svelte @@ -21,7 +21,7 @@ return; } const [tournamentsData, imagesData] = await Promise.all([listTournaments(), listImages()]); - tournaments = tournamentsData; + tournaments = tournamentsData ?? []; images = imagesData; loading = false; }); diff --git a/apps/frontend/src/routes/admin/tournaments/+page.svelte b/apps/frontend/src/routes/admin/tournaments/+page.svelte index 9e0cd96..3392443 100644 --- a/apps/frontend/src/routes/admin/tournaments/+page.svelte +++ b/apps/frontend/src/routes/admin/tournaments/+page.svelte @@ -42,7 +42,7 @@ goto('/menu'); return; } - tournaments = await listTournaments(); + tournaments = (await listTournaments()) ?? []; loading = false; }); @@ -65,7 +65,7 @@ if (result) { // Reload tournaments - tournaments = await listTournaments(); + tournaments = (await listTournaments()) ?? []; // Reset form name = ''; @@ -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'); diff --git a/apps/frontend/src/routes/layout.css b/apps/frontend/src/routes/layout.css index eb04822..f61c988 100644 --- a/apps/frontend/src/routes/layout.css +++ b/apps/frontend/src/routes/layout.css @@ -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; @@ -94,7 +95,7 @@ body { /* Button Colors */ .btn-cyan { background-color: var(--color-cyan); - color: var(--color-white); + color: var(--color-black); } .btn-magenta { @@ -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 { @@ -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; diff --git a/apps/frontend/src/routes/menu/+page.svelte b/apps/frontend/src/routes/menu/+page.svelte index 1caf8d3..99a0066 100644 --- a/apps/frontend/src/routes/menu/+page.svelte +++ b/apps/frontend/src/routes/menu/+page.svelte @@ -13,34 +13,81 @@ import { getDailyStatus } from '$lib/api/game'; let stats = $state(null); - let pendingChallenges = $state(0); - let activeTournaments = $state(0); - let dailyCompleted = $state(false); + let pendingChallenges = $state(null); + let activeTournaments = $state(null); + let dailyCompleted = $state(null); + let loading = $state(true); + let loadError = $state(false); + + const dailyStat = $derived( + stats + ? `${stats.currentStreak} day${stats.currentStreak === 1 ? '' : 's'} streak` + : loading + ? 'Loading' + : 'Unavailable' + ); + const pendingStat = $derived( + pendingChallenges !== null + ? `${pendingChallenges} pending` + : loading + ? 'Loading' + : 'Unavailable' + ); + const tournamentStat = $derived( + activeTournaments !== null ? `${activeTournaments} active` : loading ? 'Loading' : 'Unavailable' + ); + const loadStatus = $derived( + loading + ? 'Loading your game status' + : loadError + ? 'Some stats could not be loaded' + : 'Game status loaded' + ); onMount(async () => { - // Redirect to login if not authenticated - if (!userStore.isAuthenticated && !userStore.loading) { - goto('/'); + await userStore.load(); + if (!userStore.isAuthenticated) { + await goto('/'); return; } - // Load user stats - stats = await getMyStats(); - - // Load pending challenges count - const challenges = await getChallenges(); - pendingChallenges = challenges.received.length + challenges.active.length; + await loadMenuData(); + }); - // Load active tournaments count - const tournaments = await listTournaments(); - activeTournaments = tournaments.filter( - (t) => t.status === 'open' || t.status === 'in_progress' - ).length; + async function loadMenuData() { + loading = true; + loadError = false; + + const [statsResult, challengesResult, tournamentsResult, dailyResult] = + await Promise.allSettled([ + getMyStats(), + getChallenges(), + listTournaments(), + getDailyStatus() + ]); + + const statsData = statsResult.status === 'fulfilled' ? statsResult.value : null; + const challengesData = challengesResult.status === 'fulfilled' ? challengesResult.value : null; + const tournamentsData = + tournamentsResult.status === 'fulfilled' ? tournamentsResult.value : null; + const dailyData = dailyResult.status === 'fulfilled' ? dailyResult.value : null; + + if (statsData) stats = statsData; + if (challengesData) { + pendingChallenges = challengesData.received.length + challengesData.active.length; + } + if (tournamentsData) { + activeTournaments = tournamentsData.filter( + (t) => t.status === 'open' || t.status === 'in_progress' + ).length; + } + if (dailyData) dailyCompleted = dailyData.hasPlayed; - // Check if daily challenge is completed - const dailyStatus = await getDailyStatus(); - dailyCompleted = dailyStatus?.hasPlayed ?? false; - }); + loadError = [statsData, challengesData, tournamentsData, dailyData].some( + (value) => value === null + ); + loading = false; + } @@ -55,7 +102,9 @@
-
+
+

{loadStatus}

+
+ {#if loadError} +
+

Some stats couldn't load.

+ +
+ {/if} + -

Choose Your Mode

+

Choose Your Mode

@@ -120,17 +178,29 @@ icon="trophy" title="Tournament" description="Bracket-style competition!" - stat="{activeTournaments} active" + stat={tournamentStat} cta="Compete" />
- - - - + + + +
diff --git a/apps/frontend/src/routes/tournament/+page.svelte b/apps/frontend/src/routes/tournament/+page.svelte index d16630f..9299563 100644 --- a/apps/frontend/src/routes/tournament/+page.svelte +++ b/apps/frontend/src/routes/tournament/+page.svelte @@ -44,7 +44,7 @@ async function loadTournaments() { loading = true; - tournaments = await listTournaments(); + tournaments = (await listTournaments()) ?? []; loading = false; } diff --git a/apps/frontend/src/routes/versus/+page.svelte b/apps/frontend/src/routes/versus/+page.svelte index 04e1b63..c563aac 100644 --- a/apps/frontend/src/routes/versus/+page.svelte +++ b/apps/frontend/src/routes/versus/+page.svelte @@ -18,7 +18,8 @@ cancelChallenge as apiCancelChallenge, forfeitMatch as apiForfeitMatch, type Player, - type Challenge + type Challenge, + type ChallengesResponse } from '$lib/api/versus'; import { userStore } from '$lib/stores/user.svelte'; @@ -28,6 +29,12 @@ let activeMatches = $state([]); let completedMatches = $state([]); let loading = $state(true); + const emptyChallenges: ChallengesResponse = { + received: [], + sent: [], + active: [], + completed: [] + }; let pollInterval: ReturnType | null = null; const POLL_INTERVAL_MS = 5000; // Poll every 5 seconds @@ -63,11 +70,12 @@ getAvailablePlayers(), getChallenges() ]); + const challenges = challengesData ?? emptyChallenges; players = playersData; - receivedChallenges = challengesData.received; - sentChallenges = challengesData.sent; - activeMatches = challengesData.active; - completedMatches = challengesData.completed; + receivedChallenges = challenges.received; + sentChallenges = challenges.sent; + activeMatches = challenges.active; + completedMatches = challenges.completed; loading = false; } @@ -79,11 +87,12 @@ getAvailablePlayers(), getChallenges() ]); + const challenges = challengesData ?? emptyChallenges; players = playersData; - receivedChallenges = challengesData.received; - sentChallenges = challengesData.sent; - activeMatches = challengesData.active; - completedMatches = challengesData.completed; + receivedChallenges = challenges.received; + sentChallenges = challenges.sent; + activeMatches = challenges.active; + completedMatches = challenges.completed; } async function challengePlayer(username: string) { From c25aa3c12c119a2b272475c46067bdb8987aa406 Mon Sep 17 00:00:00 2001 From: Ammaar Alam Date: Fri, 14 Aug 2026 13:59:36 -0400 Subject: [PATCH 2/2] (fix) avoid redundant menu auth revalidation --- apps/frontend/src/routes/menu/+page.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/routes/menu/+page.svelte b/apps/frontend/src/routes/menu/+page.svelte index 99a0066..5c51f75 100644 --- a/apps/frontend/src/routes/menu/+page.svelte +++ b/apps/frontend/src/routes/menu/+page.svelte @@ -45,7 +45,9 @@ ); onMount(async () => { - await userStore.load(); + if (userStore.loading) { + await userStore.load(); + } if (!userStore.isAuthenticated) { await goto('/'); return;