310 lines
13 KiB
Vue
310 lines
13 KiB
Vue
<script setup lang="ts">
|
|||
|
|
import { ref, onMounted } from 'vue'
|
||
|
|
import { RouterLink } from 'vue-router'
|
||
|
|
import SpritePreview from '../components/SpritePreview.vue'
|
||
|
|
import PixelGlove from '../components/PixelGlove.vue'
|
||
|
|
|
||
|
|
interface UpcomingFight {
|
||
|
|
id: string
|
||
|
|
botA: { name: string; avatarSeed: string; archetype: string; tier: number; eloRating: number } | null
|
||
|
|
botB: { name: string; avatarSeed: string; archetype: string; tier: number; eloRating: number } | null
|
||
|
|
arenaInfo: { name: string } | null
|
||
|
|
arena: string
|
||
|
|
status: string
|
||
|
|
totalRounds: number
|
||
|
|
}
|
||
|
|
|
||
|
|
const fights = ref<UpcomingFight[]>([])
|
||
|
|
const isLoading = ref(true)
|
||
|
|
const featured = ref<UpcomingFight | null>(null)
|
||
|
|
|
||
|
|
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',
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(async () => {
|
||
|
|
try {
|
||
|
|
const res = await fetch('/api/fights')
|
||
|
|
if (res.ok) {
|
||
|
|
const data = await res.json()
|
||
|
|
fights.value = data
|
||
|
|
// Feature the most recent fight with both bots
|
||
|
|
featured.value = data.find((f: UpcomingFight) => f.botA && f.botB) || null
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.warn('[FightCard] load failed:', err)
|
||
|
|
}
|
||
|
|
isLoading.value = false
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div class="min-h-[calc(100vh-4rem)] flex flex-col items-center overflow-y-auto relative">
|
||
|
|
|
||
|
|
<!-- Background neon grid -->
|
||
|
|
<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">
|
||
|
|
<!-- MAIN EVENT CARD -->
|
||
|
|
<div class="w-full max-w-2xl mx-auto px-4 py-6 sm:py-10 relative z-10">
|
||
|
|
|
||
|
|
<!-- Neon sign header -->
|
||
|
|
<div class="text-center mb-6 sm:mb-10">
|
||
|
|
<p class="font-pixel text-[10px] sm:text-xs text-neon-yellow tracking-[0.4em] mb-2 animate-pulse">
|
||
|
|
TONIGHT'S MAIN EVENT
|
||
|
|
</p>
|
||
|
|
<div class="relative inline-block">
|
||
|
|
<h1 class="font-neon text-4xl sm:text-6xl md:text-7xl text-neon-pink glow-pink neon-flicker tracking-wider">
|
||
|
|
FIGHT CARD
|
||
|
|
</h1>
|
||
|
|
<div class="absolute -inset-4 border-2 border-neon-pink/30 rounded-lg neon-border-pink" />
|
||
|
|
<div class="absolute -inset-6 border border-neon-pink/10 rounded-xl" />
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Main fight card with 3D perspective -->
|
||
|
|
<RouterLink
|
||
|
|
:to="`/arena/${featured.id}`"
|
||
|
|
class="block group"
|
||
|
|
>
|
||
|
|
<div class="fight-card-3d relative">
|
||
|
|
<div class="fight-card-inner border-2 border-neon-pink/40 rounded-2xl overflow-hidden
|
||
|
|
bg-gradient-to-b from-surface-raised via-black/90 to-surface-raised
|
||
|
|
shadow-[0_0_40px_rgba(255,45,120,0.15),0_0_80px_rgba(255,45,120,0.05)]
|
||
|
|
group-hover:border-neon-pink/60 group-hover:shadow-[0_0_60px_rgba(255,45,120,0.25)]
|
||
|
|
transition-all duration-300">
|
||
|
|
|
||
|
|
<!-- Top banner -->
|
||
|
|
<div class="bg-gradient-to-r from-neon-pink/20 via-neon-purple/20 to-neon-cyan/20 px-4 py-2 text-center border-b border-white/5">
|
||
|
|
<p class="font-display font-black text-[10px] sm:text-xs tracking-[0.3em] text-neon-yellow">
|
||
|
|
{{ featured.arenaInfo?.name?.toUpperCase() || 'THE RING' }}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Fighter showcase -->
|
||
|
|
<div class="px-4 sm:px-8 py-6 sm:py-10">
|
||
|
|
<div class="flex items-center justify-between gap-2">
|
||
|
|
|
||
|
|
<!-- Fighter A -->
|
||
|
|
<div class="flex-1 flex flex-col items-center text-center min-w-0">
|
||
|
|
<div class="relative mb-3 sm:mb-4">
|
||
|
|
<div class="absolute -inset-3 rounded-full bg-neon-cyan/10 blur-xl" />
|
||
|
|
<SpritePreview
|
||
|
|
v-if="featured.botA"
|
||
|
|
:seed="featured.botA.avatarSeed || featured.botA.name"
|
||
|
|
:archetype="featured.botA.archetype || 'standard'"
|
||
|
|
:tier="featured.botA.tier"
|
||
|
|
:size="100"
|
||
|
|
class="relative z-10 drop-shadow-[0_0_20px_rgba(0,240,255,0.4)] sm:w-[130px] sm:h-[130px] fighter-bob"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<p class="font-display font-black text-sm sm:text-xl tracking-wider text-neon-cyan glow-cyan truncate w-full">
|
||
|
|
{{ featured.botA?.name || '???' }}
|
||
|
|
</p>
|
||
|
|
<p class="font-pixel text-[9px] sm:text-[10px] mt-1 tracking-wider"
|
||
|
|
:style="{ color: tierColors[featured.botA?.tier || 0] }">
|
||
|
|
{{ tierNames[featured.botA?.tier || 0] }}
|
||
|
|
</p>
|
||
|
|
<p class="font-mono text-[10px] sm:text-xs text-text-muted mt-0.5">
|
||
|
|
{{ Math.round(featured.botA?.eloRating || 0) }} ELO
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- VS center with gloves -->
|
||
|
|
<div class="shrink-0 flex flex-col items-center gap-1 px-2">
|
||
|
|
<PixelGlove :size="28" class="sm:!w-[40px] rotate-12 glove-punch-left" />
|
||
|
|
<div class="relative">
|
||
|
|
<span class="font-neon text-3xl sm:text-5xl text-neon-purple glow-purple vs-pulse tracking-wider">VS</span>
|
||
|
|
</div>
|
||
|
|
<PixelGlove :size="28" flip class="sm:!w-[40px] -rotate-12 glove-punch-right" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Fighter B -->
|
||
|
|
<div class="flex-1 flex flex-col items-center text-center min-w-0">
|
||
|
|
<div class="relative mb-3 sm:mb-4">
|
||
|
|
<div class="absolute -inset-3 rounded-full bg-neon-pink/10 blur-xl" />
|
||
|
|
<SpritePreview
|
||
|
|
v-if="featured.botB"
|
||
|
|
:seed="featured.botB.avatarSeed || featured.botB.name"
|
||
|
|
:archetype="featured.botB.archetype || 'standard'"
|
||
|
|
:tier="featured.botB.tier"
|
||
|
|
:size="100"
|
||
|
|
class="relative z-10 drop-shadow-[0_0_20px_rgba(255,45,120,0.4)] sm:w-[130px] sm:h-[130px] fighter-bob-delayed"
|
||
|
|
style="transform: scaleX(-1)"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<p class="font-display font-black text-sm sm:text-xl tracking-wider text-neon-pink glow-pink truncate w-full">
|
||
|
|
{{ featured.botB?.name || '???' }}
|
||
|
|
</p>
|
||
|
|
<p class="font-pixel text-[9px] sm:text-[10px] mt-1 tracking-wider"
|
||
|
|
:style="{ color: tierColors[featured.botB?.tier || 0] }">
|
||
|
|
{{ tierNames[featured.botB?.tier || 0] }}
|
||
|
|
</p>
|
||
|
|
<p class="font-mono text-[10px] sm:text-xs text-text-muted mt-0.5">
|
||
|
|
{{ Math.round(featured.botB?.eloRating || 0) }} ELO
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Bottom banner -->
|
||
|
|
<div class="bg-gradient-to-r from-neon-cyan/10 via-neon-purple/10 to-neon-pink/10 px-4 py-3 text-center border-t border-white/5">
|
||
|
|
<p class="font-display font-black text-xs sm:text-sm tracking-[0.2em] text-neon-purple group-hover:text-neon-cyan transition-colors">
|
||
|
|
{{ featured.status === 'finished' ? 'WATCH REPLAY' : featured.status === 'live' ? 'WATCH LIVE' : 'VIEW FIGHT' }}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</RouterLink>
|
||
|
|
|
||
|
|
<!-- Undercard fights -->
|
||
|
|
<div v-if="fights.length > 1" class="mt-8 sm:mt-12">
|
||
|
|
<div class="flex items-center gap-3 mb-4">
|
||
|
|
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-neon-purple/30 to-transparent" />
|
||
|
|
<p class="font-pixel text-[10px] text-neon-purple tracking-[0.3em]">UNDERCARD</p>
|
||
|
|
<div class="flex-1 h-px bg-gradient-to-r from-transparent via-neon-purple/30 to-transparent" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="space-y-2">
|
||
|
|
<RouterLink
|
||
|
|
v-for="fight in fights.filter(f => f.id !== featured?.id).slice(0, 6)"
|
||
|
|
:key="fight.id"
|
||
|
|
:to="`/arena/${fight.id}`"
|
||
|
|
class="flex items-center px-3 sm:px-4 py-2.5 border border-border/50 rounded-lg
|
||
|
|
bg-surface-raised/30 hover:border-neon-purple/30 hover:bg-surface-overlay/20
|
||
|
|
transition-all group/card"
|
||
|
|
>
|
||
|
|
<div class="flex-1 flex items-center justify-end gap-2 min-w-0">
|
||
|
|
<p class="font-display font-bold text-xs sm:text-sm truncate"
|
||
|
|
:class="fight.botA ? 'text-text-primary' : 'text-text-muted'">
|
||
|
|
{{ fight.botA?.name || '???' }}
|
||
|
|
</p>
|
||
|
|
<SpritePreview
|
||
|
|
v-if="fight.botA"
|
||
|
|
:seed="fight.botA.avatarSeed || fight.botA.name"
|
||
|
|
:archetype="fight.botA.archetype || 'standard'"
|
||
|
|
:tier="fight.botA.tier"
|
||
|
|
:size="28"
|
||
|
|
class="shrink-0"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<span class="font-funky text-neon-purple text-xs sm:text-sm px-2 sm:px-3 shrink-0">VS</span>
|
||
|
|
<div class="flex-1 flex items-center gap-2 min-w-0">
|
||
|
|
<SpritePreview
|
||
|
|
v-if="fight.botB"
|
||
|
|
:seed="fight.botB.avatarSeed || fight.botB.name"
|
||
|
|
:archetype="fight.botB.archetype || 'standard'"
|
||
|
|
:tier="fight.botB.tier"
|
||
|
|
:size="28"
|
||
|
|
class="shrink-0"
|
||
|
|
style="transform: scaleX(-1)"
|
||
|
|
/>
|
||
|
|
<p class="font-display font-bold text-xs sm:text-sm truncate"
|
||
|
|
:class="fight.botB ? 'text-text-primary' : 'text-text-muted'">
|
||
|
|
{{ fight.botB?.name || '???' }}
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<span v-if="fight.status === 'finished'" class="font-mono text-[9px] text-text-muted ml-2 shrink-0">R{{ fight.totalRounds }}</span>
|
||
|
|
<span v-else-if="fight.status === 'live'" class="font-display text-[10px] text-neon-yellow ml-2 shrink-0 animate-pulse">LIVE</span>
|
||
|
|
</RouterLink>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- CTA -->
|
||
|
|
<div class="mt-8 sm:mt-10 text-center">
|
||
|
|
<RouterLink
|
||
|
|
to="/join"
|
||
|
|
class="inline-block px-8 sm:px-12 py-3 sm:py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
||
|
|
font-display font-black text-sm sm:text-base tracking-widest
|
||
|
|
hover:bg-neon-pink/20 transition-all neon-border-pink"
|
||
|
|
>
|
||
|
|
ENTER THE RING
|
||
|
|
</RouterLink>
|
||
|
|
</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>
|
||
|
|
/* 3D card effect */
|
||
|
|
.fight-card-3d {
|
||
|
|
perspective: 1000px;
|
||
|
|
}
|
||
|
|
.fight-card-inner {
|
||
|
|
transform: rotateX(2deg);
|
||
|
|
transform-origin: center bottom;
|
||
|
|
transition: transform 0.3s ease;
|
||
|
|
}
|
||
|
|
.fight-card-3d:hover .fight-card-inner {
|
||
|
|
transform: rotateX(0deg) scale(1.01);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* 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 */
|
||
|
|
.glove-punch-left {
|
||
|
|
animation: punchLeft 2s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
.glove-punch-right {
|
||
|
|
animation: punchRight 2s ease-in-out infinite 0.3s;
|
||
|
|
}
|
||
|
|
@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); }
|
||
|
|
}
|
||
|
|
|
||
|
|
/* VS pulse */
|
||
|
|
.vs-pulse {
|
||
|
|
animation: vsPulse 2s ease-in-out infinite;
|
||
|
|
}
|
||
|
|
@keyframes vsPulse {
|
||
|
|
0%, 100% { transform: scale(1); opacity: 1; }
|
||
|
|
50% { transform: scale(1.1); opacity: 0.8; }
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Neon purple glow utility */
|
||
|
|
.glow-purple {
|
||
|
|
text-shadow: 0 0 10px rgba(168, 85, 247, 0.6), 0 0 30px rgba(168, 85, 247, 0.3);
|
||
|
|
}
|
||
|
|
</style>
|