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>
105 lines
3.2 KiB
Vue
105 lines
3.2 KiB
Vue
<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>
|