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" />
{/if}
<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>
{:else}
<a

View File

@@ -5,20 +5,11 @@
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) {
if (href === '/') return page.url.pathname === '/';
return page.url.pathname.startsWith(href);
}
const isStaff = $derived(user?.role === 'referee' || user?.role === 'admin');
const isAdmin = $derived(user?.role === 'admin');
// Close sidebar on navigation (mobile)
@@ -28,6 +19,24 @@
});
</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) -->
{#if open}
<button
@@ -50,16 +59,20 @@
<!-- Nav -->
<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
href={item.href}
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive(
item.href
)
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive(item.href)
? 'bg-accent-dim text-accent'
: 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}"
>
<span class="text-base">{item.icon}</span>
{@render navIcon(item.icon)}
{item.label}
</a>
{/each}
@@ -70,13 +83,11 @@
</div>
<a
href="/admin"
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive(
'/admin'
)
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive('/admin')
? 'bg-red-500/10 text-red-400'
: 'text-text-secondary hover:bg-surface-700 hover:text-text-primary'}"
>
<span class="text-base">⚡</span>
{@render navIcon('admin')}
Admin
</a>
{/if}
@@ -86,18 +97,16 @@
<div class="border-t border-border p-3">
<a
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(
'/settings'
)
class="font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors {isActive('/settings')
? '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
</a>
<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="/privacy" class="text-xs text-text-secondary/50 hover:text-text-secondary transition-colors">Privacy</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 transition-colors hover:text-text-secondary">Privacy</a>
</div>
</div>
</aside>

View File

@@ -103,7 +103,17 @@ async function findRecentActiveMatch(userId: string): Promise<string | null> {
return activeMatches[0]?.id ?? null;
}
const MAX_CONCURRENT_MATCHES = 4;
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({
orderBy: asc(matchQueue.elo)
});

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import { page } from '$app/state';
import { untrack } from 'svelte';
let { data } = $props();
@@ -66,11 +67,16 @@
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(() => {
if (data.user) {
fetchQueueStatus();
const interval = setInterval(fetchQueueStatus, queueStatus?.inQueue ? 2000 : 5000);
const interval = setInterval(
fetchQueueStatus,
untrack(() => (queueStatus?.inQueue ? 2000 : 5000))
);
return () => clearInterval(interval);
}
});
@@ -89,9 +95,9 @@
</script>
<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 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:type" content="website" />
<meta property="og:url" content={page.url.href} />
@@ -103,11 +109,7 @@
<!-- ── LANDING PAGE (not logged in) ── -->
{#if !data.user}
<div class="flex min-h-[70vh] flex-col items-center justify-center text-center">
<div
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>
<img src="/logo.png" alt="Vash Esports" class="mb-6 h-16 w-16" />
<h1 class="text-4xl font-800 tracking-tight">
Welcome to <span class="text-accent">Vash Esports</span>
</h1>
@@ -129,11 +131,14 @@
<!-- ── DASHBOARD (logged in) ── -->
{:else}
<div class="mx-auto max-w-5xl">
<div>
<h1 class="text-2xl font-700 tracking-tight">
Welcome back, <span class="text-accent">{data.user.name}</span>
</h1>
<p class="mt-1 text-sm text-text-secondary">Automated esports match management for osu!</p>
<div class="rounded-lg border border-border bg-surface-800 p-5">
<h1 class="text-3xl mb-4 font-semibold text-center font-800 tracking-tight">Vash Esports</h1>
<p class="mt-2 text-sm text-text-secondary leading-relaxed">
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>
<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>
<!-- Stats -->
@@ -164,7 +169,7 @@
<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>
<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 class="mt-3 flex flex-col gap-2">
{#each data.liveMatches as m}
@@ -173,7 +178,7 @@
{@const sc = stateConfig[m.state]}
<a
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">
{#if p1?.team.avatarUrl}
@@ -246,10 +251,9 @@
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"
>
<span
class="flex h-8 w-8 items-center justify-center rounded-md bg-accent/10 text-accent"
>⚔</span
>
<span 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>
<div>
<p class="text-sm font-600">Matches</p>
<p class="text-xs text-text-secondary">View all matches</p>
@@ -259,10 +263,9 @@
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"
>
<span
class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-500/10 text-purple-400"
>♫</span
>
<span 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>
<div>
<p class="text-sm font-600">Mappools</p>
<p class="text-xs text-text-secondary">Build a pool</p>
@@ -272,10 +275,9 @@
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"
>
<span
class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500/10 text-blue-400"
>⚑</span
>
<span 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>
<div>
<p class="text-sm font-600">Teams</p>
<p class="text-xs text-text-secondary">Manage rosters</p>
@@ -285,10 +287,9 @@
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"
>
<span
class="flex h-8 w-8 items-center justify-center rounded-md bg-yellow-500/10 text-yellow-400"
>🏆</span
>
<span 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>
<div>
<p class="text-sm font-600">Leaderboard</p>
<p class="text-xs text-text-secondary">Rankings & stats</p>

View File

@@ -35,9 +35,9 @@
<svelte:head>
<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: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 name="twitter:card" content="summary" />
</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="relative h-2 w-2 rounded-full bg-green-400"></div>
</div>
Live Now
Live Now (max 4 until osu! gives me bot account lol)
</h2>
<div class="mt-3 flex flex-col gap-2">
{#each liveMatches as m}

View File

@@ -24,6 +24,15 @@
}
</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">
<!-- Header -->
<div class="flex items-center justify-between">

View File

@@ -15,7 +15,7 @@
<h2>2. Use of the Service</h2>
<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:
</p>
<ul>