vbytes-lan-attendence/web/src/routes/(tournament)/tournament/+page.svelte

154 lines
5.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { onMount } from 'svelte';
import { listenToTournamentEvents } from '$lib/client/tournament-events';
import type { TournamentInfo } from '$lib/types';
const props = $props<{ data: { tournaments: TournamentInfo[] } }>();
let tournamentList = $state(props.data.tournaments ?? []);
function sortTournaments(list: TournamentInfo[]): TournamentInfo[] {
return [...list].sort((a, b) => {
const timeA = a.start_at ? new Date(a.start_at).getTime() : Number.POSITIVE_INFINITY;
const timeB = b.start_at ? new Date(b.start_at).getTime() : Number.POSITIVE_INFINITY;
if (!Number.isNaN(timeA) && !Number.isNaN(timeB) && timeA !== timeB) {
return timeA - timeB;
}
return a.title.localeCompare(b.title, 'sv');
});
}
const tournaments = $derived(() => sortTournaments(tournamentList));
function upsertTournament(tournament: TournamentInfo) {
const index = tournamentList.findIndex((item) => item.id === tournament.id);
if (index >= 0) {
tournamentList = tournamentList.map((item, idx) => (idx === index ? tournament : item));
return;
}
tournamentList = [...tournamentList, tournament];
}
function removeTournament(id: number) {
if (!tournamentList.some((item) => item.id === id)) return;
tournamentList = tournamentList.filter((item) => item.id !== id);
}
function formatDate(value: string | null) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
return date.toLocaleString('sv-SE', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}
function registrationSummary(tournament: TournamentInfo) {
const teams = tournament.total_registrations ?? 0;
const participants = tournament.total_participants ?? 0;
if (tournament.signup_config.mode === 'team') {
if (teams === 0) {
return 'Inga lag anmälda ännu';
}
return `${teams} ${teams === 1 ? 'lag' : 'lag'} · ${participants} ${participants === 1 ? 'spelare' : 'spelare'}`;
}
const count = Math.max(participants, teams);
if (count === 0) {
return 'Inga spelare anmälda ännu';
}
return `${count} ${count === 1 ? 'spelare' : 'spelare'}`;
}
onMount(() => {
const stop = listenToTournamentEvents(upsertTournament, removeTournament);
return () => {
stop();
};
});
</script>
<svelte:head>
<title>Turneringar VBytes LAN</title>
</svelte:head>
<div class="min-h-screen bg-slate-950 text-slate-100">
<div class="mx-auto max-w-6xl space-y-12 px-4 py-16 sm:py-20">
<header class="space-y-3 text-center">
<p class="text-xs uppercase tracking-[0.4em] text-indigo-300">VBytes LAN</p>
<h1 class="text-4xl font-bold sm:text-5xl">Turneringar</h1>
<p class="mx-auto max-w-2xl text-base text-slate-300">
Samla laget, följ brackets i realtid och håll koll på allt som händer under turneringarna.
</p>
</header>
{#if tournaments().length > 0}
<div class="flex flex-wrap justify-center gap-8">
{#each tournaments() as tournament}
<article class="group w-full max-w-md flex flex-col rounded-3xl border border-slate-800 bg-gradient-to-br from-slate-900/80 via-slate-900/70 to-slate-900/50 p-8 shadow-xl transition duration-200 hover:-translate-y-1 hover:border-indigo-400/70 hover:shadow-indigo-500/25">
<div class="flex items-center justify-between gap-3 text-xs uppercase tracking-wide">
<span class="font-semibold text-indigo-200">{tournament.game}</span>
<span class="rounded-full border border-slate-700 px-3 py-1 text-[0.7rem] font-semibold text-slate-300">
{registrationSummary(tournament)}
</span>
</div>
<h2 class="mt-4 text-3xl font-semibold text-slate-50">{tournament.title}</h2>
{#if tournament.tagline}
<p class="mt-3 text-base text-slate-300">{tournament.tagline}</p>
{:else if tournament.description}
<p class="mt-3 text-base text-slate-400">{tournament.description}</p>
{/if}
<dl class="mt-6 space-y-3 text-sm text-slate-300">
{#if tournament.start_at}
<div class="flex items-center gap-3 text-[0.95rem]">
<span class="text-indigo-200">Start:</span>
<span>{formatDate(tournament.start_at) ?? tournament.start_at}</span>
</div>
{/if}
{#if tournament.location}
<div class="flex items-center gap-3 text-[0.95rem]">
<span class="text-indigo-200">Plats:</span>
<span>{tournament.location}</span>
</div>
{/if}
{#if tournament.contact}
<div class="flex items-center gap-3 text-[0.95rem]">
<span class="text-indigo-200">Kontakt:</span>
<span>{tournament.contact}</span>
</div>
{/if}
</dl>
<div class="mt-auto pt-8">
{#if tournament.slug}
<a
href={`/tournament/${tournament.slug}`}
class="inline-flex items-center justify-center rounded-full bg-indigo-500 px-5 py-2 text-sm font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-600"
>
Visa turnering
</a>
{:else}
<span class="text-xs text-slate-500">Ingen publik sida tillgänglig.</span>
{/if}
</div>
</article>
{/each}
</div>
{:else}
<p class="rounded-2xl border border-dashed border-slate-700 bg-slate-900/40 px-6 py-12 text-center text-sm text-slate-400">
Inga turneringar är publicerade ännu. Kom tillbaka senare!
</p>
{/if}
<div class="text-center">
<a
href="/admin"
class="inline-flex items-center justify-center rounded-full bg-indigo-500 px-6 py-3 text-sm font-semibold uppercase tracking-wide text-white transition hover:bg-indigo-600"
>
Till admin
</a>
</div>
</div>
</div>