refactor: streamline header and sidebar components, update navigation icons, and enhance queue management logic
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user