feat: tournament bracket UI

TournamentListPage with status filters, TournamentPage with CSS grid
bracket display, lobby view for open tournaments, champion banner,
live polling for active tournaments. Routes at /tournaments and
/tournament/:id. NavBar link added.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 21:00:48 +00:00
co-authored by Claude Opus 4.6
parent b70c49075e
commit b0926db9ed
4 changed files with 359 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { RouterLink } from 'vue-router'
interface Tournament {
id: string
name: string
format: string
size: number
entrySats: number
prizeSats: number
status: 'open' | 'active' | 'finished'
currentRound: number
createdAt: string
startedAt: string | null
finishedAt: string | null
}
const tournaments = ref<Tournament[]>([])
const isLoading = ref(true)
const filter = ref<'all' | 'open' | 'active' | 'finished'>('all')
const filtered = computed(() => {
if (filter.value === 'all') return tournaments.value
return tournaments.value.filter(t => t.status === filter.value)
})
async function fetchTournaments() {
isLoading.value = true
try {
const res = await fetch('/api/tournaments')
if (res.ok) {
const data = await res.json()
tournaments.value = data.tournaments || []
}
} catch {
console.warn('[Tournaments] load failed')
}
isLoading.value = false
}
function statusLabel(s: string) {
if (s === 'open') return 'OPEN'
if (s === 'active') return 'LIVE'
return 'FINISHED'
}
function statusColor(s: string) {
if (s === 'open') return 'text-neon-green'
if (s === 'active') return 'text-neon-pink'
return 'text-text-muted'
}
onMounted(fetchTournaments)
</script>
<template>
<div class="max-w-3xl mx-auto px-4 py-8 space-y-6">
<h1 class="font-display font-black text-2xl text-neon-cyan tracking-wider">TOURNAMENTS</h1>
<div class="flex gap-2">
<button
v-for="f in (['all', 'open', 'active', 'finished'] as const)"
:key="f"
class="px-3 py-1 text-xs font-display font-bold tracking-wider rounded border transition-colors"
:class="filter === f
? 'border-neon-cyan text-neon-cyan bg-neon-cyan/10'
: 'border-border text-text-secondary hover:text-neon-cyan hover:border-neon-cyan/50'"
@click="filter = f"
>
{{ f.toUpperCase() }}
</button>
</div>
<div v-if="isLoading" class="text-text-muted text-sm font-mono">Loading...</div>
<div v-else-if="filtered.length === 0" class="text-text-muted text-sm font-mono">
No tournaments found.
</div>
<div v-else class="space-y-3">
<RouterLink
v-for="t in filtered"
:key="t.id"
:to="`/tournament/${t.id}`"
class="block border border-border rounded-lg p-4 bg-surface/50 hover:border-neon-cyan/50 transition-colors"
>
<div class="flex items-center justify-between">
<h2 class="font-display font-bold text-text-primary tracking-wide">{{ t.name }}</h2>
<span class="text-xs font-display font-bold tracking-wider" :class="statusColor(t.status)">
{{ statusLabel(t.status) }}
</span>
</div>
<div class="mt-2 flex gap-4 text-xs text-text-muted font-mono">
<span>{{ t.format === 'single_elim' ? 'Single Elim' : 'Round Robin' }}</span>
<span>{{ t.size }} bots</span>
<span v-if="t.entrySats > 0">{{ t.entrySats }} sats entry</span>
<span v-if="t.prizeSats > 0">{{ t.prizeSats }} sats prize</span>
<span>Round {{ t.currentRound }}</span>
</div>
</RouterLink>
</div>
</div>
</template>