154 lines
5.6 KiB
Svelte
154 lines
5.6 KiB
Svelte
<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>
|