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:
co-authored by
Claude Opus 4.6
parent
b70c49075e
commit
b0926db9ed
@@ -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>
|
||||
Reference in New Issue
Block a user