2026-03-08 08:38:27 +00:00
|
|
|
<script setup lang="ts">
|
2026-03-08 10:33:30 +00:00
|
|
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
2026-03-08 08:38:27 +00:00
|
|
|
import { RouterLink } from 'vue-router'
|
|
|
|
|
import SpritePreview from '../components/SpritePreview.vue'
|
|
|
|
|
import PixelGlove from '../components/PixelGlove.vue'
|
2026-03-08 19:55:44 +00:00
|
|
|
import BetPanel from '../components/BetPanel.vue'
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
interface BotInfo {
|
|
|
|
|
name: string
|
|
|
|
|
avatarSeed: string
|
|
|
|
|
archetype: string
|
|
|
|
|
tier: number
|
|
|
|
|
eloRating: number
|
|
|
|
|
botType?: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 08:38:27 +00:00
|
|
|
interface UpcomingFight {
|
|
|
|
|
id: string
|
2026-03-08 19:55:44 +00:00
|
|
|
botAId: string
|
|
|
|
|
botBId: string
|
2026-03-08 10:33:30 +00:00
|
|
|
botA: BotInfo | null
|
|
|
|
|
botB: BotInfo | null
|
2026-03-08 08:38:27 +00:00
|
|
|
arenaInfo: { name: string } | null
|
|
|
|
|
arena: string
|
|
|
|
|
status: string
|
|
|
|
|
totalRounds: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fights = ref<UpcomingFight[]>([])
|
|
|
|
|
const isLoading = ref(true)
|
2026-03-08 10:33:30 +00:00
|
|
|
const mainEvent = ref<UpcomingFight | null>(null)
|
|
|
|
|
const selectedId = ref<string | null>(null)
|
|
|
|
|
const isPunching = ref(false)
|
|
|
|
|
let punchInterval: ReturnType<typeof setInterval> | null = null
|
|
|
|
|
|
|
|
|
|
// Derive featured from selectedId — always look up from fights array for fresh data
|
|
|
|
|
const featured = computed<UpcomingFight | null>(() => {
|
|
|
|
|
if (selectedId.value) {
|
|
|
|
|
const found = fights.value.find(f => f.id === selectedId.value)
|
|
|
|
|
if (found && found.botA && found.botB) return found
|
|
|
|
|
}
|
|
|
|
|
return mainEvent.value
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Is the main event currently featured?
|
|
|
|
|
const isMainFeatured = computed(() => !selectedId.value || selectedId.value === mainEvent.value?.id)
|
|
|
|
|
|
|
|
|
|
// Undercard fights (everything except main event)
|
|
|
|
|
const undercardFights = computed(() => {
|
|
|
|
|
if (!mainEvent.value) return fights.value
|
|
|
|
|
return fights.value.filter(f => f.id !== mainEvent.value!.id)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Safe accessors for featured fight bots (never null when featured exists)
|
|
|
|
|
const fighterA = computed<BotInfo | null>(() => featured.value?.botA ?? null)
|
|
|
|
|
const fighterB = computed<BotInfo | null>(() => featured.value?.botB ?? null)
|
2026-03-08 08:38:27 +00:00
|
|
|
|
|
|
|
|
const tierNames: Record<number, string> = {
|
|
|
|
|
0: 'ROOKIE', 1: 'BRAWLER', 2: 'WARRIOR', 3: 'CHAMPION', 4: 'LEGEND', 5: 'MYTHIC',
|
|
|
|
|
}
|
|
|
|
|
const tierColors: Record<number, string> = {
|
|
|
|
|
0: '#8a8a9a', 1: '#4ade80', 2: '#38bdf8', 3: '#a855f7', 4: '#f97316', 5: '#ef4444',
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 15:25:34 +00:00
|
|
|
function isCreator(bot: BotInfo | null): boolean {
|
|
|
|
|
return bot?.archetype === 'the_creator'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTierName(bot: BotInfo | null): string {
|
|
|
|
|
if (!bot) return ''
|
|
|
|
|
if (isCreator(bot)) return '₿ CREATOR'
|
|
|
|
|
return tierNames[bot.tier ?? 0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getTierColor(bot: BotInfo | null): string {
|
|
|
|
|
if (!bot) return '#8a8a9a'
|
|
|
|
|
if (isCreator(bot)) return '#ffd700'
|
|
|
|
|
return tierColors[bot.tier ?? 0]
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
function selectFight(fightId: string) {
|
|
|
|
|
const fight = fights.value.find(f => f.id === fightId)
|
|
|
|
|
if (!fight || !fight.botA || !fight.botB) return
|
|
|
|
|
selectedId.value = fightId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectMainEvent() {
|
|
|
|
|
selectedId.value = null
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 08:38:27 +00:00
|
|
|
onMounted(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/fights')
|
|
|
|
|
if (res.ok) {
|
2026-03-08 10:33:30 +00:00
|
|
|
const data: UpcomingFight[] = await res.json()
|
2026-03-08 08:38:27 +00:00
|
|
|
fights.value = data
|
2026-03-08 10:33:30 +00:00
|
|
|
// Main event = first fight with both bots present
|
|
|
|
|
mainEvent.value = data.find(f => f.botA && f.botB) || null
|
2026-03-08 08:38:27 +00:00
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.warn('[FightCard] load failed:', err)
|
|
|
|
|
}
|
|
|
|
|
isLoading.value = false
|
2026-03-08 10:33:30 +00:00
|
|
|
|
|
|
|
|
punchInterval = setInterval(() => {
|
|
|
|
|
isPunching.value = true
|
|
|
|
|
setTimeout(() => { isPunching.value = false }, 800)
|
|
|
|
|
}, 6000)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
if (punchInterval) clearInterval(punchInterval)
|
2026-03-08 08:38:27 +00:00
|
|
|
})
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-03-08 15:25:34 +00:00
|
|
|
<div class="h-[calc(100dvh-4rem)] flex flex-col overflow-hidden relative">
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Page background -->
|
2026-03-08 08:38:27 +00:00
|
|
|
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
|
|
|
|
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[200%] h-full opacity-20"
|
|
|
|
|
style="background: radial-gradient(ellipse at 50% 0%, rgba(255,45,120,0.4) 0%, transparent 60%)" />
|
|
|
|
|
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 w-[200%] h-full opacity-15"
|
|
|
|
|
style="background: radial-gradient(ellipse at 50% 100%, rgba(0,240,255,0.3) 0%, transparent 50%)" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="isLoading" class="flex-1 flex items-center justify-center">
|
|
|
|
|
<p class="font-display text-text-muted animate-pulse tracking-wider">LOADING...</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<template v-else-if="featured">
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Desktop: poster 2/3 + fight list 1/3 | Mobile: 50/50 -->
|
|
|
|
|
<div class="flex-1 min-h-0 flex flex-col lg:flex-row relative z-10">
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- LEFT: Fight poster -->
|
2026-03-08 08:38:27 +00:00
|
|
|
<RouterLink
|
|
|
|
|
:to="`/arena/${featured.id}`"
|
2026-03-08 10:33:30 +00:00
|
|
|
class="h-1/2 lg:h-full lg:w-2/3 flex flex-col overflow-hidden lg:border-r lg:border-border/30 group relative poster-bg"
|
2026-03-08 08:38:27 +00:00
|
|
|
>
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- ═══ Dramatic poster background layers ═══ -->
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Dark vignette base -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none"
|
|
|
|
|
style="background: radial-gradient(ellipse at 50% 45%, transparent 30%, rgba(0,0,0,0.6) 100%)" />
|
|
|
|
|
|
|
|
|
|
<!-- Dual spotlights — cyan left, pink right -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none"
|
|
|
|
|
style="background:
|
|
|
|
|
conic-gradient(from 200deg at 15% 0%, rgba(0,240,255,0.15) 0deg, transparent 40deg),
|
|
|
|
|
conic-gradient(from -20deg at 85% 0%, rgba(255,45,120,0.15) 0deg, transparent 40deg)" />
|
|
|
|
|
|
|
|
|
|
<!-- Center clash glow -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none"
|
|
|
|
|
style="background:
|
|
|
|
|
radial-gradient(ellipse at 25% 55%, rgba(0,240,255,0.14) 0%, transparent 45%),
|
|
|
|
|
radial-gradient(ellipse at 75% 55%, rgba(255,45,120,0.14) 0%, transparent 45%),
|
|
|
|
|
radial-gradient(circle at 50% 50%, rgba(168,85,247,0.08) 0%, transparent 35%)" />
|
|
|
|
|
|
|
|
|
|
<!-- Top pink wash -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none"
|
|
|
|
|
style="background: radial-gradient(ellipse at 50% -10%, rgba(255,45,120,0.25) 0%, transparent 40%)" />
|
|
|
|
|
|
|
|
|
|
<!-- Bottom purple haze -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none"
|
|
|
|
|
style="background: radial-gradient(ellipse at 50% 110%, rgba(168,85,247,0.12) 0%, transparent 35%)" />
|
|
|
|
|
|
|
|
|
|
<!-- Cross-hatched lines -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none poster-crosshatch" />
|
|
|
|
|
|
|
|
|
|
<!-- Scan lines overlay -->
|
|
|
|
|
<div class="absolute inset-0 pointer-events-none poster-scanlines" />
|
|
|
|
|
|
|
|
|
|
<!-- Neon edge lines -->
|
|
|
|
|
<div class="absolute top-0 inset-x-0 h-[2px] bg-gradient-to-r from-transparent via-neon-pink/70 to-transparent" />
|
|
|
|
|
<div class="absolute bottom-0 inset-x-0 h-[2px] bg-gradient-to-r from-transparent via-neon-cyan/70 to-transparent" />
|
|
|
|
|
<div class="absolute left-0 inset-y-0 w-[2px] bg-gradient-to-b from-neon-pink/30 via-neon-purple/20 to-neon-cyan/30 hidden lg:block" />
|
|
|
|
|
|
|
|
|
|
<!-- Corner accents -->
|
|
|
|
|
<div class="absolute top-0 left-0 w-8 h-8 sm:w-12 sm:h-12 border-t-2 border-l-2 border-neon-pink/30" />
|
|
|
|
|
<div class="absolute top-0 right-0 w-8 h-8 sm:w-12 sm:h-12 border-t-2 border-r-2 border-neon-pink/30" />
|
|
|
|
|
<div class="absolute bottom-0 left-0 w-8 h-8 sm:w-12 sm:h-12 border-b-2 border-l-2 border-neon-cyan/30" />
|
|
|
|
|
<div class="absolute bottom-0 right-0 w-8 h-8 sm:w-12 sm:h-12 border-b-2 border-r-2 border-neon-cyan/30" />
|
|
|
|
|
|
|
|
|
|
<!-- Ring ropes (horizontal accent lines) -->
|
|
|
|
|
<div class="absolute left-0 right-0 top-[30%] h-px bg-gradient-to-r from-transparent via-neon-yellow/10 to-transparent" />
|
|
|
|
|
<div class="absolute left-0 right-0 top-[70%] h-px bg-gradient-to-r from-transparent via-neon-yellow/10 to-transparent" />
|
|
|
|
|
|
|
|
|
|
<!-- Title -->
|
|
|
|
|
<div class="text-center pt-2 sm:pt-3 lg:pt-5 px-4 relative z-10 shrink-0">
|
|
|
|
|
<p class="font-pixel text-[10px] sm:text-sm lg:text-base text-neon-yellow/90 tracking-[0.5em] animate-pulse">
|
|
|
|
|
{{ isMainFeatured ? 'TONIGHT\'S MAIN EVENT' : 'UNDERCARD BOUT' }}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="relative inline-block mt-0.5 sm:mt-1">
|
|
|
|
|
<h1 class="font-neon text-3xl sm:text-5xl lg:text-8xl text-neon-pink glow-pink neon-flicker tracking-wider leading-none poster-title">
|
|
|
|
|
FIGHT NIGHT
|
|
|
|
|
</h1>
|
|
|
|
|
<div class="absolute -inset-2 sm:-inset-3 border-2 border-neon-pink/30 rounded neon-border-pink" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center gap-3 mt-1.5 sm:mt-2 max-w-sm sm:max-w-md mx-auto">
|
|
|
|
|
<div class="flex-1 h-px bg-gradient-to-r from-transparent to-neon-purple/30" />
|
|
|
|
|
<div class="shrink-0 px-3 sm:px-4 py-0.5 sm:py-1 border border-neon-purple/25 rounded bg-neon-purple/5">
|
|
|
|
|
<p class="font-display font-black text-[7px] sm:text-[10px] lg:text-xs tracking-[0.3em] text-neon-purple/80">
|
2026-03-08 08:38:27 +00:00
|
|
|
{{ featured.arenaInfo?.name?.toUpperCase() || 'THE RING' }}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-03-08 10:33:30 +00:00
|
|
|
<div class="flex-1 h-px bg-gradient-to-l from-transparent to-neon-purple/30" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Fighters -->
|
|
|
|
|
<div class="flex-1 flex flex-col items-center justify-center relative z-10 min-h-0 px-2 sm:px-4 lg:px-6">
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Sprites -->
|
|
|
|
|
<div class="flex items-end justify-center w-full max-w-4xl">
|
|
|
|
|
<div class="flex-1 flex flex-col items-center min-w-0">
|
2026-03-08 11:03:16 +00:00
|
|
|
<div v-if="fighterA" class="relative fighter-bob">
|
|
|
|
|
<!-- Circular glow behind fighter -->
|
2026-03-08 15:25:34 +00:00
|
|
|
<div class="fighter-glow" :class="isCreator(fighterA) ? 'fighter-glow-gold' : 'fighter-glow-cyan'" />
|
2026-03-08 11:03:16 +00:00
|
|
|
<SpritePreview
|
|
|
|
|
:key="`a-${featured.id}-${fighterA.avatarSeed}`"
|
|
|
|
|
:seed="fighterA.avatarSeed || fighterA.name"
|
|
|
|
|
:archetype="fighterA.archetype || 'standard'"
|
|
|
|
|
:tier="fighterA.tier"
|
|
|
|
|
:size="120"
|
|
|
|
|
pose="win"
|
|
|
|
|
class="relative z-10
|
|
|
|
|
sm:!w-[180px] sm:!h-[180px] lg:!w-[260px] lg:!h-[260px]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-03-08 10:33:30 +00:00
|
|
|
</div>
|
|
|
|
|
<div class="flex-1 flex flex-col items-center min-w-0 -ml-6 sm:-ml-12 lg:-ml-20">
|
2026-03-08 11:03:16 +00:00
|
|
|
<div v-if="fighterB" class="relative fighter-bob-delayed" style="transform: scaleX(-1)">
|
|
|
|
|
<!-- Circular glow behind fighter -->
|
2026-03-08 15:25:34 +00:00
|
|
|
<div class="fighter-glow" :class="isCreator(fighterB) ? 'fighter-glow-gold' : 'fighter-glow-pink'" />
|
2026-03-08 11:03:16 +00:00
|
|
|
<SpritePreview
|
2026-03-08 10:33:30 +00:00
|
|
|
:key="`b-${featured.id}-${fighterB.avatarSeed}`"
|
|
|
|
|
:seed="fighterB.avatarSeed || fighterB.name"
|
|
|
|
|
:archetype="fighterB.archetype || 'standard'"
|
|
|
|
|
:tier="fighterB.tier"
|
2026-03-08 11:03:16 +00:00
|
|
|
:size="120"
|
2026-03-08 10:33:30 +00:00
|
|
|
pose="idle"
|
2026-03-08 11:03:16 +00:00
|
|
|
class="relative z-10
|
|
|
|
|
sm:!w-[180px] sm:!h-[180px] lg:!w-[260px] lg:!h-[260px]"
|
2026-03-08 10:33:30 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Names with VS orb in center -->
|
|
|
|
|
<div class="flex items-stretch w-full max-w-3xl mt-1 sm:mt-2 lg:mt-3 shrink-0 relative">
|
2026-03-08 08:38:27 +00:00
|
|
|
|
2026-03-08 15:25:34 +00:00
|
|
|
<!-- Fighter A -->
|
|
|
|
|
<div class="flex-1 min-w-0 border-2 rounded-l-lg px-2 sm:px-3 pr-16 sm:pr-20 lg:pr-24 py-1.5 sm:py-2 lg:py-3 text-center"
|
|
|
|
|
:class="isCreator(fighterA) ? 'name-block-gold border-yellow-500/40' : 'name-block-cyan border-neon-cyan/40'">
|
|
|
|
|
<p class="font-funky text-base sm:text-2xl lg:text-4xl tracking-widest truncate pb-0.5 uppercase"
|
|
|
|
|
:class="isCreator(fighterA) ? 'text-yellow-400 poster-name-gold' : 'text-neon-cyan poster-name-cyan'">
|
2026-03-08 10:33:30 +00:00
|
|
|
{{ fighterA?.name || '???' }}
|
|
|
|
|
</p>
|
|
|
|
|
<p class="font-pixel text-[6px] sm:text-[8px] lg:text-[11px] tracking-[0.2em]"
|
2026-03-08 15:25:34 +00:00
|
|
|
:style="{ color: getTierColor(fighterA) }">
|
|
|
|
|
{{ getTierName(fighterA) }}
|
2026-03-08 10:33:30 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- VS orb — 3D circle, bigger than name cards -->
|
|
|
|
|
<div class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-30">
|
|
|
|
|
<div class="vs-orb" :class="{ 'vs-punching': isPunching }">
|
|
|
|
|
<div class="vs-orb-inner flex items-center justify-center">
|
|
|
|
|
<PixelGlove :size="22" class="sm:!w-[32px] lg:!w-[42px] vs-glove-left" :class="{ 'glove-smash-left': isPunching }" />
|
|
|
|
|
<span v-if="!isPunching" class="font-pixel text-xl sm:text-3xl lg:text-4xl text-white tracking-widest leading-none mx-1.5 sm:mx-2">VS</span>
|
|
|
|
|
<span v-else class="font-pixel text-2xl sm:text-4xl lg:text-5xl text-neon-yellow tracking-widest leading-none mx-0 vs-impact">💥</span>
|
|
|
|
|
<PixelGlove :size="22" flip class="sm:!w-[32px] lg:!w-[42px] vs-glove-right" :class="{ 'glove-smash-right': isPunching }" />
|
2026-03-08 08:38:27 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-08 15:25:34 +00:00
|
|
|
<!-- Fighter B -->
|
|
|
|
|
<div class="flex-1 min-w-0 border-2 border-l-0 rounded-r-lg px-2 sm:px-3 pl-16 sm:pl-20 lg:pl-24 py-1.5 sm:py-2 lg:py-3 text-center"
|
|
|
|
|
:class="isCreator(fighterB) ? 'name-block-gold border-yellow-500/40' : 'name-block-pink border-neon-pink/40'">
|
|
|
|
|
<p class="font-funky text-base sm:text-2xl lg:text-4xl tracking-widest truncate pb-0.5 uppercase"
|
|
|
|
|
:class="isCreator(fighterB) ? 'text-yellow-400 poster-name-gold' : 'text-neon-pink poster-name-pink'">
|
2026-03-08 10:33:30 +00:00
|
|
|
{{ fighterB?.name || '???' }}
|
|
|
|
|
</p>
|
|
|
|
|
<p class="font-pixel text-[6px] sm:text-[8px] lg:text-[11px] tracking-[0.2em]"
|
2026-03-08 15:25:34 +00:00
|
|
|
:style="{ color: getTierColor(fighterB) }">
|
|
|
|
|
{{ getTierName(fighterB) }}
|
2026-03-08 08:38:27 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-03-08 10:33:30 +00:00
|
|
|
|
|
|
|
|
<!-- CTA button -->
|
|
|
|
|
<div class="relative z-10 shrink-0 border-t border-white/5 px-4 py-2 sm:py-3 lg:py-4 text-center
|
|
|
|
|
bg-gradient-to-r from-neon-pink/8 via-neon-purple/8 to-neon-cyan/8">
|
|
|
|
|
<span class="inline-block px-6 sm:px-10 py-1.5 sm:py-2.5 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
|
|
|
|
font-display font-black text-[10px] sm:text-xs tracking-widest
|
|
|
|
|
group-hover:bg-neon-pink/20 transition-all neon-border-pink">
|
|
|
|
|
{{ featured.status === 'finished' ? 'WATCH REPLAY' : featured.status === 'live' ? 'WATCH LIVE' : 'VIEW FIGHT' }}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-03-08 08:38:27 +00:00
|
|
|
</RouterLink>
|
|
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- RIGHT: Fight list (bottom half on mobile, 1/3 on desktop) -->
|
|
|
|
|
<div class="h-1/2 lg:h-full lg:w-1/3 flex flex-col overflow-hidden border-t lg:border-t-0 border-border/30">
|
|
|
|
|
|
|
|
|
|
<!-- Main event card — ALWAYS visible at top -->
|
|
|
|
|
<button
|
|
|
|
|
v-if="mainEvent && mainEvent.botA && mainEvent.botB"
|
|
|
|
|
class="mx-4 mt-3 lg:mt-5 mb-2 px-3 py-2 border-2 rounded-lg transition-all text-left"
|
|
|
|
|
:class="isMainFeatured
|
|
|
|
|
? 'border-neon-yellow/50 bg-neon-yellow/10 ring-1 ring-neon-yellow/20'
|
|
|
|
|
: 'border-neon-yellow/20 bg-neon-yellow/5 hover:bg-neon-yellow/10 hover:border-neon-yellow/40'"
|
|
|
|
|
@click.prevent="selectMainEvent"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex items-center justify-between mb-1">
|
|
|
|
|
<p class="font-pixel text-[8px] text-neon-yellow/70 tracking-[0.3em]">★ MAIN EVENT</p>
|
|
|
|
|
<span v-if="isMainFeatured" class="font-pixel text-[7px] text-neon-yellow/50 tracking-wider">VIEWING</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<div class="flex-1 flex items-center justify-end gap-1.5 min-w-0">
|
2026-03-08 15:25:34 +00:00
|
|
|
<p class="font-display font-bold text-xs truncate"
|
|
|
|
|
:class="isCreator(mainEvent.botA) ? 'text-yellow-400' : 'text-neon-cyan'">
|
|
|
|
|
{{ isCreator(mainEvent.botA) ? '₿ ' : '' }}{{ mainEvent.botA.name }}
|
|
|
|
|
</p>
|
2026-03-08 10:33:30 +00:00
|
|
|
<SpritePreview
|
|
|
|
|
:seed="mainEvent.botA.avatarSeed || mainEvent.botA.name"
|
|
|
|
|
:archetype="mainEvent.botA.archetype || 'standard'"
|
|
|
|
|
:tier="mainEvent.botA.tier"
|
|
|
|
|
:size="20"
|
|
|
|
|
class="shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="font-pixel text-neon-yellow text-[10px] px-2 shrink-0">VS</span>
|
|
|
|
|
<div class="flex-1 flex items-center gap-1.5 min-w-0">
|
|
|
|
|
<SpritePreview
|
|
|
|
|
:seed="mainEvent.botB.avatarSeed || mainEvent.botB.name"
|
|
|
|
|
:archetype="mainEvent.botB.archetype || 'standard'"
|
|
|
|
|
:tier="mainEvent.botB.tier"
|
|
|
|
|
:size="20"
|
|
|
|
|
class="shrink-0"
|
|
|
|
|
style="transform: scaleX(-1)"
|
|
|
|
|
/>
|
2026-03-08 15:25:34 +00:00
|
|
|
<p class="font-display font-bold text-xs truncate"
|
|
|
|
|
:class="isCreator(mainEvent.botB) ? 'text-yellow-400' : 'text-neon-pink'">
|
|
|
|
|
{{ mainEvent.botB.name }}{{ isCreator(mainEvent.botB) ? ' ₿' : '' }}
|
|
|
|
|
</p>
|
2026-03-08 10:33:30 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<!-- Undercard header -->
|
|
|
|
|
<div class="px-4 py-2 lg:py-3 shrink-0">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-neon-purple/30 to-transparent" />
|
|
|
|
|
<p class="font-pixel text-[9px] sm:text-[10px] text-neon-purple tracking-[0.3em]">
|
|
|
|
|
{{ undercardFights.length > 0 ? 'UNDERCARD' : 'NO OTHER BOUTS' }}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-neon-purple/30 to-transparent" />
|
|
|
|
|
</div>
|
2026-03-08 08:38:27 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Fight list -->
|
|
|
|
|
<div class="flex-1 min-h-0 overflow-y-auto px-4 pb-2 lg:pb-4 space-y-1.5">
|
|
|
|
|
<button
|
|
|
|
|
v-for="fight in undercardFights"
|
2026-03-08 08:38:27 +00:00
|
|
|
:key="fight.id"
|
2026-03-08 10:33:30 +00:00
|
|
|
class="w-full flex items-center px-3 py-2 border rounded-lg transition-all text-left"
|
|
|
|
|
:class="fight.id === selectedId
|
|
|
|
|
? 'border-neon-purple/50 bg-neon-purple/10 ring-1 ring-neon-purple/20'
|
|
|
|
|
: (fight.botA && fight.botB)
|
|
|
|
|
? 'border-border/50 bg-surface-raised/30 hover:border-neon-purple/30 hover:bg-surface-overlay/20'
|
|
|
|
|
: 'border-border/30 bg-surface-raised/10 opacity-50 cursor-not-allowed'"
|
|
|
|
|
:disabled="!fight.botA || !fight.botB"
|
|
|
|
|
@click.prevent="fight.botA && fight.botB && selectFight(fight.id)"
|
2026-03-08 08:38:27 +00:00
|
|
|
>
|
2026-03-08 10:33:30 +00:00
|
|
|
<div class="flex-1 flex items-center justify-end gap-1.5 min-w-0">
|
|
|
|
|
<p class="font-display font-bold text-[11px] truncate"
|
2026-03-08 15:25:34 +00:00
|
|
|
:class="isCreator(fight.botA) ? 'text-yellow-400' : fight.botA ? 'text-text-primary' : 'text-text-muted'">
|
|
|
|
|
{{ isCreator(fight.botA) ? '₿ ' : '' }}{{ fight.botA?.name || '???' }}
|
2026-03-08 08:38:27 +00:00
|
|
|
</p>
|
|
|
|
|
<SpritePreview
|
|
|
|
|
v-if="fight.botA"
|
|
|
|
|
:seed="fight.botA.avatarSeed || fight.botA.name"
|
|
|
|
|
:archetype="fight.botA.archetype || 'standard'"
|
|
|
|
|
:tier="fight.botA.tier"
|
2026-03-08 10:33:30 +00:00
|
|
|
:size="22"
|
2026-03-08 08:38:27 +00:00
|
|
|
class="shrink-0"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-03-08 10:33:30 +00:00
|
|
|
<span class="font-funky text-neon-purple text-[10px] px-1.5 shrink-0">VS</span>
|
|
|
|
|
<div class="flex-1 flex items-center gap-1.5 min-w-0">
|
2026-03-08 08:38:27 +00:00
|
|
|
<SpritePreview
|
|
|
|
|
v-if="fight.botB"
|
|
|
|
|
:seed="fight.botB.avatarSeed || fight.botB.name"
|
|
|
|
|
:archetype="fight.botB.archetype || 'standard'"
|
|
|
|
|
:tier="fight.botB.tier"
|
2026-03-08 10:33:30 +00:00
|
|
|
:size="22"
|
2026-03-08 08:38:27 +00:00
|
|
|
class="shrink-0"
|
|
|
|
|
style="transform: scaleX(-1)"
|
|
|
|
|
/>
|
2026-03-08 10:33:30 +00:00
|
|
|
<p class="font-display font-bold text-[11px] truncate"
|
2026-03-08 15:25:34 +00:00
|
|
|
:class="isCreator(fight.botB) ? 'text-yellow-400' : fight.botB ? 'text-text-primary' : 'text-text-muted'">
|
|
|
|
|
{{ fight.botB?.name || '???' }}{{ isCreator(fight.botB) ? ' ₿' : '' }}
|
2026-03-08 08:38:27 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-03-08 10:33:30 +00:00
|
|
|
<div class="ml-1.5 shrink-0">
|
|
|
|
|
<span v-if="fight.botA?.botType === 'classic' || fight.botB?.botType === 'classic'"
|
|
|
|
|
class="font-display text-[8px] text-text-muted/50 tracking-wider">PRAC</span>
|
|
|
|
|
<span v-else-if="fight.status === 'finished'" class="font-mono text-[8px] text-text-muted">R{{ fight.totalRounds }}</span>
|
|
|
|
|
<span v-else-if="fight.status === 'live'" class="font-display text-[9px] text-neon-yellow animate-pulse">LIVE</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-08 19:55:44 +00:00
|
|
|
<!-- Bet panel for upcoming fights -->
|
|
|
|
|
<div v-if="featured && featured.botA && featured.botB && featured.status === 'scheduled'" class="px-4 pb-2 shrink-0">
|
|
|
|
|
<BetPanel
|
|
|
|
|
:fight-id="featured.id"
|
|
|
|
|
:bot-a="{ id: featured.botAId, name: featured.botA.name, eloRating: featured.botA.eloRating, tier: featured.botA.tier }"
|
|
|
|
|
:bot-b="{ id: featured.botBId, name: featured.botB.name, eloRating: featured.botB.eloRating, tier: featured.botB.tier }"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-08 10:33:30 +00:00
|
|
|
<!-- Enter the ring -->
|
|
|
|
|
<div class="px-4 pb-3 lg:pb-5 shrink-0">
|
|
|
|
|
<RouterLink
|
|
|
|
|
to="/join"
|
|
|
|
|
class="block text-center px-6 py-2.5 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
|
|
|
|
font-display font-black text-[10px] sm:text-xs tracking-widest
|
|
|
|
|
hover:bg-neon-pink/20 transition-all neon-border-pink"
|
|
|
|
|
>
|
|
|
|
|
ENTER THE RING
|
2026-03-08 08:38:27 +00:00
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div v-else class="flex-1 flex flex-col items-center justify-center gap-4 relative z-10">
|
|
|
|
|
<p class="font-display text-text-muted text-lg tracking-wider">No fights scheduled.</p>
|
|
|
|
|
<RouterLink
|
|
|
|
|
to="/join"
|
|
|
|
|
class="px-8 py-3 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
|
|
|
|
font-display font-black text-sm tracking-widest hover:bg-neon-pink/20 transition-all"
|
|
|
|
|
>
|
|
|
|
|
START A FIGHT
|
|
|
|
|
</RouterLink>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-03-08 10:33:30 +00:00
|
|
|
/* ═══ Poster background effects ═══ */
|
|
|
|
|
.poster-bg {
|
|
|
|
|
background: linear-gradient(180deg,
|
|
|
|
|
rgba(15, 5, 25, 1) 0%,
|
|
|
|
|
rgba(10, 8, 20, 1) 40%,
|
|
|
|
|
rgba(8, 12, 22, 1) 100%);
|
2026-03-08 08:38:27 +00:00
|
|
|
}
|
2026-03-08 10:33:30 +00:00
|
|
|
|
|
|
|
|
/* Cross-hatched diagonal lines */
|
|
|
|
|
.poster-crosshatch {
|
|
|
|
|
background:
|
|
|
|
|
repeating-linear-gradient(
|
|
|
|
|
45deg,
|
|
|
|
|
transparent,
|
|
|
|
|
transparent 30px,
|
|
|
|
|
rgba(255, 255, 255, 0.04) 30px,
|
|
|
|
|
rgba(255, 255, 255, 0.04) 32px
|
|
|
|
|
),
|
|
|
|
|
repeating-linear-gradient(
|
|
|
|
|
-45deg,
|
|
|
|
|
transparent,
|
|
|
|
|
transparent 30px,
|
|
|
|
|
rgba(255, 255, 255, 0.04) 30px,
|
|
|
|
|
rgba(255, 255, 255, 0.04) 32px
|
|
|
|
|
);
|
2026-03-08 08:38:27 +00:00
|
|
|
}
|
2026-03-08 10:33:30 +00:00
|
|
|
|
|
|
|
|
/* CRT scan lines */
|
|
|
|
|
.poster-scanlines {
|
|
|
|
|
opacity: 0.04;
|
|
|
|
|
background: repeating-linear-gradient(
|
|
|
|
|
0deg,
|
|
|
|
|
transparent,
|
|
|
|
|
transparent 2px,
|
|
|
|
|
rgba(0, 0, 0, 0.8) 2px,
|
|
|
|
|
rgba(0, 0, 0, 0.8) 4px
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Poster name glow — 3D embossed neon */
|
|
|
|
|
.poster-name-cyan {
|
|
|
|
|
-webkit-text-stroke: 1px rgba(0, 180, 200, 0.6);
|
|
|
|
|
text-shadow:
|
|
|
|
|
2px 2px 0 rgba(0, 100, 130, 0.8),
|
|
|
|
|
0 0 10px rgba(0, 240, 255, 0.6),
|
|
|
|
|
0 0 30px rgba(0, 240, 255, 0.3),
|
|
|
|
|
0 0 60px rgba(0, 240, 255, 0.15);
|
|
|
|
|
}
|
|
|
|
|
.poster-name-pink {
|
|
|
|
|
-webkit-text-stroke: 1px rgba(200, 30, 90, 0.6);
|
|
|
|
|
text-shadow:
|
|
|
|
|
2px 2px 0 rgba(130, 20, 60, 0.8),
|
|
|
|
|
0 0 10px rgba(255, 45, 120, 0.6),
|
|
|
|
|
0 0 30px rgba(255, 45, 120, 0.3),
|
|
|
|
|
0 0 60px rgba(255, 45, 120, 0.15);
|
|
|
|
|
}
|
2026-03-08 15:25:34 +00:00
|
|
|
.poster-name-gold {
|
|
|
|
|
-webkit-text-stroke: 1px rgba(200, 160, 0, 0.6);
|
|
|
|
|
text-shadow:
|
|
|
|
|
2px 2px 0 rgba(130, 100, 0, 0.8),
|
|
|
|
|
0 0 10px rgba(255, 215, 0, 0.6),
|
|
|
|
|
0 0 30px rgba(255, 215, 0, 0.3),
|
|
|
|
|
0 0 60px rgba(255, 180, 0, 0.15);
|
|
|
|
|
}
|
2026-03-08 10:33:30 +00:00
|
|
|
.poster-title {
|
|
|
|
|
text-shadow:
|
|
|
|
|
0 0 15px rgba(255, 45, 120, 0.6),
|
|
|
|
|
0 0 40px rgba(255, 45, 120, 0.3),
|
|
|
|
|
0 0 80px rgba(255, 45, 120, 0.15);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Name block containers */
|
|
|
|
|
.name-block-cyan {
|
|
|
|
|
background: linear-gradient(135deg, rgba(0, 240, 255, 0.08) 0%, rgba(0, 240, 255, 0.02) 100%);
|
|
|
|
|
}
|
|
|
|
|
.name-block-pink {
|
|
|
|
|
background: linear-gradient(225deg, rgba(255, 45, 120, 0.08) 0%, rgba(255, 45, 120, 0.02) 100%);
|
|
|
|
|
}
|
2026-03-08 15:25:34 +00:00
|
|
|
.name-block-gold {
|
|
|
|
|
background: linear-gradient(180deg, rgba(255, 215, 0, 0.12) 0%, rgba(255, 180, 0, 0.04) 100%);
|
|
|
|
|
}
|
2026-03-08 10:33:30 +00:00
|
|
|
|
|
|
|
|
/* ═══ VS Orb — 3D circle ═══ */
|
|
|
|
|
.vs-orb {
|
|
|
|
|
width: 112px;
|
|
|
|
|
height: 112px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: radial-gradient(circle at 35% 35%, rgba(220, 160, 255, 0.9) 0%, rgba(168, 85, 247, 0.95) 40%, rgba(100, 30, 180, 1) 100%);
|
|
|
|
|
border: 3px solid rgba(255, 255, 255, 0.25);
|
|
|
|
|
box-shadow:
|
|
|
|
|
0 0 20px rgba(168, 85, 247, 0.6),
|
|
|
|
|
0 0 40px rgba(168, 85, 247, 0.3),
|
|
|
|
|
0 0 60px rgba(168, 85, 247, 0.15),
|
|
|
|
|
inset 0 -4px 8px rgba(0,0,0,0.4),
|
|
|
|
|
inset 0 4px 8px rgba(255,255,255,0.15);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: transform 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
.vs-orb { width: 144px; height: 144px; border-width: 4px; }
|
|
|
|
|
}
|
|
|
|
|
@media (min-width: 1024px) {
|
|
|
|
|
.vs-orb { width: 176px; height: 176px; border-width: 5px; }
|
|
|
|
|
}
|
|
|
|
|
.vs-orb-inner {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
border: 2px solid rgba(255,255,255,0.1);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Punch animation */
|
|
|
|
|
.vs-punching {
|
|
|
|
|
transform: scale(1.15);
|
|
|
|
|
}
|
|
|
|
|
.vs-impact {
|
|
|
|
|
animation: impactShake 0.3s ease-out;
|
|
|
|
|
filter: brightness(1.5);
|
|
|
|
|
}
|
|
|
|
|
@keyframes impactShake {
|
|
|
|
|
0%, 100% { transform: translateX(0); }
|
|
|
|
|
25% { transform: translateX(-2px) rotate(-5deg); }
|
|
|
|
|
75% { transform: translateX(2px) rotate(5deg); }
|
|
|
|
|
}
|
|
|
|
|
.glove-smash-left {
|
|
|
|
|
animation: smashLeft 0.4s ease-out !important;
|
|
|
|
|
}
|
|
|
|
|
.glove-smash-right {
|
|
|
|
|
animation: smashRight 0.4s ease-out !important;
|
|
|
|
|
}
|
|
|
|
|
@keyframes smashLeft {
|
|
|
|
|
0% { transform: rotate(12deg) translateX(-8px); }
|
|
|
|
|
40% { transform: rotate(30deg) translateX(4px); }
|
|
|
|
|
60% { transform: rotate(15deg) translateX(2px); }
|
|
|
|
|
100% { transform: rotate(12deg) translateX(0); }
|
|
|
|
|
}
|
|
|
|
|
@keyframes smashRight {
|
|
|
|
|
0% { transform: rotate(-12deg) translateX(8px); }
|
|
|
|
|
40% { transform: rotate(-30deg) translateX(-4px); }
|
|
|
|
|
60% { transform: rotate(-15deg) translateX(-2px); }
|
|
|
|
|
100% { transform: rotate(-12deg) translateX(0); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Normal glove idle */
|
|
|
|
|
.vs-glove-left {
|
|
|
|
|
animation: punchLeft 2s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
.vs-glove-right {
|
|
|
|
|
animation: punchRight 2s ease-in-out infinite 0.3s;
|
2026-03-08 08:38:27 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-08 11:03:16 +00:00
|
|
|
/* Circular glow behind fighters */
|
|
|
|
|
.fighter-glow {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 50%;
|
|
|
|
|
left: 50%;
|
|
|
|
|
width: 140%;
|
|
|
|
|
height: 140%;
|
|
|
|
|
transform: translate(-50%, -50%);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
z-index: 0;
|
|
|
|
|
}
|
|
|
|
|
.fighter-glow-cyan {
|
|
|
|
|
background: radial-gradient(circle, rgba(0, 240, 255, 0.25) 0%, rgba(0, 240, 255, 0.10) 35%, rgba(0, 240, 255, 0.03) 60%, transparent 80%);
|
|
|
|
|
box-shadow: 0 0 60px rgba(0, 240, 255, 0.15), 0 0 120px rgba(0, 240, 255, 0.05);
|
|
|
|
|
}
|
|
|
|
|
.fighter-glow-pink {
|
|
|
|
|
background: radial-gradient(circle, rgba(255, 45, 120, 0.25) 0%, rgba(255, 45, 120, 0.10) 35%, rgba(255, 45, 120, 0.03) 60%, transparent 80%);
|
|
|
|
|
box-shadow: 0 0 60px rgba(255, 45, 120, 0.15), 0 0 120px rgba(255, 45, 120, 0.05);
|
|
|
|
|
}
|
2026-03-08 15:25:34 +00:00
|
|
|
.fighter-glow-gold {
|
|
|
|
|
background: radial-gradient(circle, rgba(255, 215, 0, 0.35) 0%, rgba(255, 215, 0, 0.15) 35%, rgba(255, 180, 0, 0.05) 60%, transparent 80%);
|
|
|
|
|
box-shadow: 0 0 60px rgba(255, 215, 0, 0.25), 0 0 120px rgba(255, 215, 0, 0.10);
|
|
|
|
|
}
|
2026-03-08 11:03:16 +00:00
|
|
|
|
2026-03-08 08:38:27 +00:00
|
|
|
/* Fighter bobbing */
|
|
|
|
|
.fighter-bob {
|
|
|
|
|
animation: fighterBob 2.5s ease-in-out infinite;
|
|
|
|
|
}
|
|
|
|
|
.fighter-bob-delayed {
|
|
|
|
|
animation: fighterBob 2.5s ease-in-out infinite 0.5s;
|
|
|
|
|
}
|
|
|
|
|
@keyframes fighterBob {
|
|
|
|
|
0%, 100% { transform: translateY(0); }
|
|
|
|
|
50% { transform: translateY(-8px); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Glove punch animations */
|
|
|
|
|
@keyframes punchLeft {
|
|
|
|
|
0%, 70%, 100% { transform: rotate(12deg) translateX(0); }
|
|
|
|
|
80% { transform: rotate(25deg) translateX(6px); }
|
|
|
|
|
85% { transform: rotate(8deg) translateX(-2px); }
|
|
|
|
|
}
|
|
|
|
|
@keyframes punchRight {
|
|
|
|
|
0%, 70%, 100% { transform: rotate(-12deg) translateX(0); }
|
|
|
|
|
80% { transform: rotate(-25deg) translateX(-6px); }
|
|
|
|
|
85% { transform: rotate(-8deg) translateX(2px); }
|
|
|
|
|
}
|
|
|
|
|
</style>
|