refactor: streamline header and sidebar components, update navigation icons, and enhance queue management logic

This commit is contained in:
2026-03-01 03:36:14 +01:00
parent 0960f91b6f
commit d1643e9b68
8 changed files with 90 additions and 69 deletions

View File

@@ -28,14 +28,6 @@
<img src={user.image} alt={user.name} class="h-7 w-7 rounded-full" /> <img src={user.image} alt={user.name} class="h-7 w-7 rounded-full" />
{/if} {/if}
<span class="hidden font-500 text-sm text-text-primary sm:inline">{user.name}</span> <span class="hidden font-500 text-sm text-text-primary sm:inline">{user.name}</span>
<form method="post" action="/settings?/logout" use:enhance>
<button
type="submit"
class="rounded-md border border-border px-2.5 py-1 text-xs text-text-secondary transition-colors hover:border-red-500/30 hover:text-red-400"
>
Logout
</button>
</form>
</div> </div>
{:else} {:else}
<a <a

View File

@@ -5,20 +5,11 @@
let { open = $bindable(false) } = $props(); let { open = $bindable(false) } = $props();
const nav = [
{ href: '/', label: 'Dashboard', icon: '⌂' },
{ href: '/matches', label: 'Matches', icon: '⚔' },
{ href: '/mappools', label: 'Mappools', icon: '♫' },
{ href: '/teams', label: 'Teams', icon: '⚑' },
{ href: '/leaderboard', label: 'Leaderboard', icon: '🏆' }
];
function isActive(href: string) { function isActive(href: string) {
if (href === '/') return page.url.pathname === '/'; if (href === '/') return page.url.pathname === '/';
return page.url.pathname.startsWith(href); return page.url.pathname.startsWith(href);
} }
const isStaff = $derived(user?.role === 'referee' || user?.role === 'admin');
const isAdmin = $derived(user?.role === 'admin'); const isAdmin = $derived(user?.role === 'admin');
// Close sidebar on navigation (mobile) // Close sidebar on navigation (mobile)
@@ -28,6 +19,24 @@
}); });
</script> </script>
{#snippet navIcon(name: string)}
{#if name === 'dashboard'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
{:else if name === 'matches'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 17.5 3 6V3h3l11.5 11.5"/><path d="m13 19 3.5-3.5"/><path d="m16.5 22 5-5"/><path d="M10 5.5 6 2H3v3l4 4"/></svg>
{:else if name === 'mappools'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="18" r="3"/><path d="M11 18V8l9-1v10"/><circle cx="20" cy="17" r="3"/></svg>
{:else if name === 'teams'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
{:else if name === 'leaderboard'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
{:else if name === 'settings'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
{:else if name === 'admin'}
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
{/if}
{/snippet}
<!-- Backdrop (mobile only) --> <!-- Backdrop (mobile only) -->
{#if open} {#if open}
<button <button
@@ -50,16 +59,20 @@
<!-- Nav --> <!-- Nav -->
<nav class="mt-2 flex flex-1 flex-col gap-0.5 px-3"> <nav class="mt-2 flex flex-1 flex-col gap-0.5 px-3">
{#each nav as item} {#each [
{ href: '/', label: 'Dashboard', icon: 'dashboard' },
{ href: '/matches', label: 'Matches', icon: 'matches' },
{ href: '/mappools', label: 'Mappools', icon: 'mappools' },
{ href: '/teams', label: 'Teams', icon: 'teams' },
{ href: '/leaderboard', label: 'Leaderboard', icon: 'leaderboard' },
] as item}
<a <a
href={item.href} href={item.href}
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive( class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive(item.href)
item.href
)
? 'bg-accent-dim text-accent' ? 'bg-accent-dim text-accent'
: 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}" : 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}"
> >
<span class="text-base">{item.icon}</span> {@render navIcon(item.icon)}
{item.label} {item.label}
</a> </a>
{/each} {/each}
@@ -70,13 +83,11 @@
</div> </div>
<a <a
href="/admin" href="/admin"
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive( class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive('/admin')
'/admin'
)
? 'bg-red-500/10 text-red-400' ? 'bg-red-500/10 text-red-400'
: 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}" : 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}"
> >
<span class="text-base">⚡</span> {@render navIcon('admin')}
Admin Admin
</a> </a>
{/if} {/if}
@@ -86,18 +97,16 @@
<div class="border-t border-border p-3"> <div class="border-t border-border p-3">
<a <a
href="/settings" href="/settings"
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-700 hover:text-text-primary {isActive( class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive('/settings')
'/settings'
)
? 'bg-accent-dim text-accent' ? 'bg-accent-dim text-accent'
: ''}" : 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}"
> >
<span class="text-base">⚙</span> {@render navIcon('settings')}
Settings Settings
</a> </a>
<div class="mt-2 flex gap-3 px-3 pb-1"> <div class="mt-2 flex gap-3 px-3 pb-1">
<a href="/terms" class="text-xs text-text-secondary/50 hover:text-text-secondary transition-colors">Terms</a> <a href="/terms" class="text-xs text-text-secondary/50 transition-colors hover:text-text-secondary">Terms</a>
<a href="/privacy" class="text-xs text-text-secondary/50 hover:text-text-secondary transition-colors">Privacy</a> <a href="/privacy" class="text-xs text-text-secondary/50 transition-colors hover:text-text-secondary">Privacy</a>
</div> </div>
</div> </div>
</aside> </aside>

View File

@@ -103,7 +103,17 @@ async function findRecentActiveMatch(userId: string): Promise<string | null> {
return activeMatches[0]?.id ?? null; return activeMatches[0]?.id ?? null;
} }
const MAX_CONCURRENT_MATCHES = 4;
async function tryMatchFromQueue() { async function tryMatchFromQueue() {
// Hard cap: osu! non-bot accounts can only host a limited number of lobbies
const [{ activeCount }] = await db
.select({ activeCount: sql<number>`count(*)` })
.from(match)
.where(inArray(match.state, [MATCH_STATES.LOBBY, MATCH_STATES.ROLLING, MATCH_STATES.PICKING, MATCH_STATES.PLAYING]));
if (Number(activeCount) >= MAX_CONCURRENT_MATCHES) return null;
const queue = await db.query.matchQueue.findMany({ const queue = await db.query.matchQueue.findMany({
orderBy: asc(matchQueue.elo) orderBy: asc(matchQueue.elo)
}); });

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { untrack } from 'svelte';
let { data } = $props(); let { data } = $props();
@@ -66,11 +67,16 @@
queueLoading = false; queueLoading = false;
} }
// Poll queue status while on dashboard (faster when in queue) // Poll queue status while on dashboard.
// Rate is read via untrack so queueStatus changes don't re-trigger the effect
// (which would cause an infinite fetch loop + rate limit exhaustion).
$effect(() => { $effect(() => {
if (data.user) { if (data.user) {
fetchQueueStatus(); fetchQueueStatus();
const interval = setInterval(fetchQueueStatus, queueStatus?.inQueue ? 2000 : 5000); const interval = setInterval(
fetchQueueStatus,
untrack(() => (queueStatus?.inQueue ? 2000 : 5000))
);
return () => clearInterval(interval); return () => clearInterval(interval);
} }
}); });
@@ -89,9 +95,9 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Vash Esports — osu! Tournament Platform</title> <title>Vash Esports</title>
<meta name="description" content="Run osu! tournaments with automated lobbies, live scoring, mappools, and team management. Built for serious competition." /> <meta name="description" content="Run osu! tournaments with automated lobbies, live scoring, mappools, and team management. Built for serious competition." />
<meta property="og:title" content="Vash Esports — osu! Tournament Platform" /> <meta property="og:title" content="Vash Esports" />
<meta property="og:description" content="Run osu! tournaments with automated lobbies, live scoring, mappools, and team management." /> <meta property="og:description" content="Run osu! tournaments with automated lobbies, live scoring, mappools, and team management." />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content={page.url.href} /> <meta property="og:url" content={page.url.href} />
@@ -103,11 +109,7 @@
<!-- ── LANDING PAGE (not logged in) ── --> <!-- ── LANDING PAGE (not logged in) ── -->
{#if !data.user} {#if !data.user}
<div class="flex min-h-[70vh] flex-col items-center justify-center text-center"> <div class="flex min-h-[70vh] flex-col items-center justify-center text-center">
<div <img src="/logo.png" alt="Vash Esports" class="mb-6 h-16 w-16" />
class="font-800 mb-6 flex h-16 w-16 items-center justify-center rounded-xl bg-accent text-2xl text-surface-900"
>
V
</div>
<h1 class="text-4xl font-800 tracking-tight"> <h1 class="text-4xl font-800 tracking-tight">
Welcome to <span class="text-accent">Vash Esports</span> Welcome to <span class="text-accent">Vash Esports</span>
</h1> </h1>
@@ -129,11 +131,14 @@
<!-- ── DASHBOARD (logged in) ── --> <!-- ── DASHBOARD (logged in) ── -->
{:else} {:else}
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-5xl">
<div> <div class="rounded-lg border border-border bg-surface-800 p-5">
<h1 class="text-2xl font-700 tracking-tight"> <h1 class="text-3xl mb-4 font-semibold text-center font-800 tracking-tight">Vash Esports</h1>
Welcome back, <span class="text-accent">{data.user.name}</span> <p class="mt-2 text-sm text-text-secondary leading-relaxed">
</h1> Press <span class="font-600 text-text-primary">'Find Match'</span> to join the queue to play a fully automated osu! match with other players in an ELO-appropriate map pool.
<p class="mt-1 text-sm text-text-secondary">Automated esports match management for osu!</p> </p>
<p class="mt-2 text-sm text-text-secondary leading-relaxed">
I'll be adding a lot more features soon, like arbitrarily large team sizes, fully automated tournaments, automated broadcasting, and more. Please join <a href="https://discord.gg/n3mZgWk" target="_blank" rel="noopener" class="text-text-primary underline underline-offset-2 hover:text-accent transition-colors">discord.gg/n3mZgWk</a> and give any feedback :)
</p>
</div> </div>
<!-- Stats --> <!-- Stats -->
@@ -164,7 +169,7 @@
<div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div>
<div class="relative h-2 w-2 rounded-full bg-green-400"></div> <div class="relative h-2 w-2 rounded-full bg-green-400"></div>
</div> </div>
<h2 class="text-sm font-600">Live Now</h2> <h2 class="text-sm font-600">Live Now (max 4 until osu! gives me bot account lol)</h2>
</div> </div>
<div class="mt-3 flex flex-col gap-2"> <div class="mt-3 flex flex-col gap-2">
{#each data.liveMatches as m} {#each data.liveMatches as m}
@@ -173,7 +178,7 @@
{@const sc = stateConfig[m.state]} {@const sc = stateConfig[m.state]}
<a <a
href="/matches/{m.id}" href="/matches/{m.id}"
class="flex items-center gap-4 rounded-lg border border-green-500/20 bg-surface-800 p-4 transition-all hover:border-green-500/40 hover:bg-surface-700" class="flex items-center gap-4 rounded-lg border border-white/15 bg-surface-800 p-4 transition-all hover:border-white/30 hover:bg-surface-700"
> >
<div class="flex flex-1 items-center gap-3"> <div class="flex flex-1 items-center gap-3">
{#if p1?.team.avatarUrl} {#if p1?.team.avatarUrl}
@@ -246,10 +251,9 @@
href="/matches" href="/matches"
class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"
> >
<span <span class="flex h-8 w-8 items-center justify-center rounded-md bg-accent/10 text-accent">
class="flex h-8 w-8 items-center justify-center rounded-md bg-accent/10 text-accent" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 17.5 3 6V3h3l11.5 11.5"/><path d="m13 19 3.5-3.5"/><path d="m16.5 22 5-5"/><path d="M10 5.5 6 2H3v3l4 4"/></svg>
>⚔</span </span>
>
<div> <div>
<p class="text-sm font-600">Matches</p> <p class="text-sm font-600">Matches</p>
<p class="text-xs text-text-secondary">View all matches</p> <p class="text-xs text-text-secondary">View all matches</p>
@@ -259,10 +263,9 @@
href="/mappools" href="/mappools"
class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"
> >
<span <span class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-500/10 text-purple-400">
class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-500/10 text-purple-400" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="18" r="3"/><path d="M11 18V8l9-1v10"/><circle cx="20" cy="17" r="3"/></svg>
>♫</span </span>
>
<div> <div>
<p class="text-sm font-600">Mappools</p> <p class="text-sm font-600">Mappools</p>
<p class="text-xs text-text-secondary">Build a pool</p> <p class="text-xs text-text-secondary">Build a pool</p>
@@ -272,10 +275,9 @@
href="/teams" href="/teams"
class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"
> >
<span <span class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500/10 text-blue-400">
class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500/10 text-blue-400" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
>⚑</span </span>
>
<div> <div>
<p class="text-sm font-600">Teams</p> <p class="text-sm font-600">Teams</p>
<p class="text-xs text-text-secondary">Manage rosters</p> <p class="text-xs text-text-secondary">Manage rosters</p>
@@ -285,10 +287,9 @@
href="/leaderboard" href="/leaderboard"
class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"
> >
<span <span class="flex h-8 w-8 items-center justify-center rounded-md bg-yellow-500/10 text-yellow-400">
class="flex h-8 w-8 items-center justify-center rounded-md bg-yellow-500/10 text-yellow-400" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>
>🏆</span </span>
>
<div> <div>
<p class="text-sm font-600">Leaderboard</p> <p class="text-sm font-600">Leaderboard</p>
<p class="text-xs text-text-secondary">Rankings & stats</p> <p class="text-xs text-text-secondary">Rankings & stats</p>

View File

@@ -35,9 +35,9 @@
<svelte:head> <svelte:head>
<title>Leaderboard — Vash Esports</title> <title>Leaderboard — Vash Esports</title>
<meta name="description" content="Top players and teams on the Vash Esports osu! tournament platform." /> <meta name="description" content="Top players and teams on Vash Esports." />
<meta property="og:title" content="Leaderboard — Vash Esports" /> <meta property="og:title" content="Leaderboard — Vash Esports" />
<meta property="og:description" content="Top players and teams on the Vash Esports osu! tournament platform." /> <meta property="og:description" content="Top players and teams on Vash Esports." />
<meta property="og:site_name" content="Vash Esports" /> <meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary" /> <meta name="twitter:card" content="summary" />
</svelte:head> </svelte:head>

View File

@@ -199,7 +199,7 @@
<div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div>
<div class="relative h-2 w-2 rounded-full bg-green-400"></div> <div class="relative h-2 w-2 rounded-full bg-green-400"></div>
</div> </div>
Live Now Live Now (max 4 until osu! gives me bot account lol)
</h2> </h2>
<div class="mt-3 flex flex-col gap-2"> <div class="mt-3 flex flex-col gap-2">
{#each liveMatches as m} {#each liveMatches as m}

View File

@@ -24,6 +24,15 @@
} }
</script> </script>
<svelte:head>
<title>Teams — Vash Esports</title>
<meta name="description" content="Browse and manage teams on Vash Esports." />
<meta property="og:title" content="Teams — Vash Esports" />
<meta property="og:description" content="Browse and manage teams on Vash Esports." />
<meta property="og:site_name" content="Vash Esports" />
<meta name="twitter:card" content="summary" />
</svelte:head>
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl">
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">

View File

@@ -15,7 +15,7 @@
<h2>2. Use of the Service</h2> <h2>2. Use of the Service</h2>
<p> <p>
Vash Esports is an osu! tournament management platform. You may use the Service only for lawful Vash Esports is an event management platform. You may use the Service only for lawful
purposes and in accordance with these Terms. You agree not to: purposes and in accordance with these Terms. You agree not to:
</p> </p>
<ul> <ul>