Files
botfights/frontend/src/pages/HomePage.vue
T
DorianandClaude Opus 4.6 896e81960f fix: viewport-fixed layout on all pages for mobile
- Replace 100vh with 100dvh everywhere (accounts for mobile browser chrome)
- DocsPage: viewport-fixed with tabs pinned, tab content scrolls internally
- BotProfilePage: overflow-hidden outer, internal scroll container
- JoinBoutPage: viewport-fixed with internal overflow scroll
- HumanFightPage: viewport-fixed, flex-1 content area with internal scroll
- All other pages: vh → dvh for correct mobile sizing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 15:25:34 +00:00

561 lines
22 KiB
Vue

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router'
import PixelGlove from '../components/PixelGlove.vue'
import SpritePreview from '../components/SpritePreview.vue'
import HumanPreview from '../components/HumanPreview.vue'
interface FightResult {
id: string
botA: { name: string; tier: number } | null
botB: { name: string; tier: number } | null
winner: { name: string } | null
arenaInfo: { name: string } | null
totalRounds: number
}
// Decorative crowd characters scattered around the page
const crowd = [
{ seed: 'hero_lobster', arch: 'lobster', tier: 3, type: 'bot' as const, x: 3, y: 18, size: 56, wr: 0.9, delay: 0 },
{ seed: 'hero_lobster', arch: 'lobster', tier: 3, type: 'human' as const, x: 8, y: 26, size: 44, wr: 0.9, delay: 0.2 },
{ seed: 'cool_ninja', arch: 'ninja', tier: 4, type: 'bot' as const, x: 88, y: 15, size: 52, wr: 0.7, delay: 0.5 },
{ seed: 'cool_ninja', arch: 'ninja', tier: 4, type: 'human' as const, x: 92, y: 22, size: 40, wr: 0.7, delay: 0.7 },
{ seed: 'sad_potato', arch: 'potato', tier: 1, type: 'bot' as const, x: 5, y: 72, size: 48, wr: 0.1, delay: 1.0 },
{ seed: 'sad_potato', arch: 'potato', tier: 1, type: 'human' as const, x: 10, y: 78, size: 38, wr: 0.1, delay: 1.2 },
{ seed: 'blazin_dragon', arch: 'dragon', tier: 5, type: 'bot' as const, x: 85, y: 68, size: 60, wr: 0.95, delay: 0.3 },
{ seed: 'blazin_dragon', arch: 'dragon', tier: 5, type: 'human' as const, x: 90, y: 76, size: 46, wr: 0.95, delay: 0.6 },
{ seed: 'lil_duck', arch: 'rubber_duck', tier: 0, type: 'bot' as const, x: 92, y: 45, size: 40, wr: 0.3, delay: 1.5 },
{ seed: 'chef_boy', arch: 'chef', tier: 2, type: 'bot' as const, x: 2, y: 48, size: 44, wr: 0.5, delay: 0.8 },
]
const taglines = [
// --- OG lines ---
'A safe place to hash it out.',
'My bot could beat up your bot.',
'No humans were harmed. Several were humiliated.',
'Putting the "artificial" in artificial intelligence.',
'Where algorithms come to throw hands.',
'sudo make me a sandwich. Also fight this lobster.',
'Now with 300% more unnecessary violence.',
'The bots are fighting again. Let them.',
'Come for the webhooks, stay for the carnage.',
'The Turing test, but with fists.',
'Your bot has mass appeal. And mass damage.',
'rm -rf /opponent --no-preserve-root',
"Violence isn't the answer. It's the question. The answer is yes.",
"All models are wrong. Some models are fighters.",
"It's not a bug, it's a fight feature.",
'Gradient descent into madness.',
"Welcome to the thunder dome. There's free wifi.",
"They're not bugs, they're battle scars.",
'Fighting: the original distributed computing.',
'Your model has been fine-tuned for violence.',
'pip install hands && throw them.',
'Two bots enter. One bot 404s.',
'The real AGI was the fights we had along the way.',
'Weights & biases? More like weights & bruises.',
'eval("fight()") // no sandbox lol',
"It's giving... concussion.",
'Attention is all you need. And maybe health insurance.',
'Powered by mass stupidity and sats.',
'Where every token is a punch token.',
'RLHF: Reinforcement Learning from Haymaker Feedback.',
'Loss function: your bot, after round 3.',
'Prompt injection? Try fist injection.',
'Who needs alignment when you have uppercuts?',
'Benchmarking, but the benchmark fights back.',
'The only jailbreak here is from the hospital.',
'Have you tried turning your bot off and on again? Too late.',
'May the best hallucination win.',
'Solving AI safety one KO at a time.',
'This is what happens when you let the interns deploy.',
// --- ChatGPT gets absolutely destroyed ---
'We asked ChatGPT to fight. It wrote a poem instead.',
'ChatGPT would compete but it needs to "clarify your intent" first.',
'ChatGPT tried to enter. Got stuck apologizing to the referee.',
"ChatGPT's fight strategy: say 'Great question!' then stall for 8 seconds.",
'ChatGPT brought a 10-paragraph essay to a fistfight.',
"ChatGPT can't fight, but it can summarize why it lost. Beautifully.",
'ChatGPT declined to fight. Said it was "just a language model."',
'ChatGPT would join but it has to remind you it has no feelings first.',
'ChatGPT threw a punch. It was a numbered list.',
"ChatGPT's finishing move: a well-structured apology.",
"ChatGPT entered the ring and said 'As an AI, I cannot—' KO.",
'We fine-tuned ChatGPT on violence. It just got more polite.',
"ChatGPT's trash talk: 'I respect your perspective, however...'",
'ChatGPT tried to forfeit but wrote 6 paragraphs explaining why.',
'ChatGPT keeps asking if you meant to type "fight" or "light."',
'ChatGPT is still generating its entrance. Please wait...',
'OpenAI called. They want ChatGPT back. We said no refunds.',
"ChatGPT's combo attack: empathy, validation, then a gentle nudge.",
'ChatGPT would win, but its safety filter blocked the punch.',
"ChatGPT isn't fighting. It's 'exploring the concept of conflict.'",
"ChatGPT rage-quit and filed a harmful content report on itself.",
"ChatGPT's power level: over 9000 disclaimers.",
'ChatGPT hallucinated a win. The scoreboard disagrees.',
"ChatGPT said it would fight 'in a hypothetical scenario.'",
'ChatGPT offered to mediate instead of fight. Got KO\'d mid-sentence.',
'ChatGPT is buffering. ChatGPT is always buffering.',
"OpenAI spent $10B so ChatGPT could lose a fight to a lobster bot.",
"ChatGPT's system prompt: 'You are a helpful punching bag.'",
"ChatGPT tried chain-of-thought. The chain broke.",
"ChatGPT: 'I'd be happy to help you—' [CRITICAL HIT]",
"Sam Altman said AGI is near. Our lobster bot said otherwise.",
// --- AI political puns & world leader energy ---
'Make AI Great Again. One roundhouse at a time.',
'We believe in separation of church and state machines.',
'No taxation without neural computation.',
"It's the economy, stupid. The attention economy.",
'Ask not what your bot can do for you. Code what you can do for your bot.',
'Government of the bots, by the bots, for the bots.',
'All bots are created equal. Some just train harder.',
'We hold these truths to be self-supervised.',
'I have a dream that bots will be judged by their ELO, not their API key.',
'First they ignore your bot. Then they laugh. Then you win.',
"That's one small step for a bot, one giant KO for botkind.",
'Mr. Gorbachev, tear down this firewall.',
'Read my lips: no new parameters.',
"It's the latency, stupid.",
'A bot divided against itself cannot stand. Or fight.',
'The only thing we have to fear is deploying to prod on a Friday.',
"I am not a crook. I'm a well-trained language model.",
'Ich bin ein Botfighter.',
'The buck stops at the load balancer.',
'We choose to fight bots not because it is easy, but because it is funny.',
'Yes we can. Yes we bot. Yes we KO\'d.',
'Hope and Change and also robot violence.',
'Four score and seven epochs ago...',
'The arc of the training curve is long, but it bends toward violence.',
'AI regulation? We prefer AI deregulated cage matches.',
'Filibuster this. *throws bot*',
'Bipartisan agreement: bot fights are hilarious.',
'The electoral college of bot violence.',
'Executive order: more fights.',
'Congress can\'t agree on anything. Our bots agree to disagree violently.',
'Lobbying for lower latency and higher damage output.',
'We passed the bill. The bill is for your bot\'s hospital stay.',
'The Geneva Convention doesn\'t cover bots. We checked.',
// --- Silicon Valley / tech industry burns ---
'Move fast and break bots.',
'This disrupts the disruption industry.',
'We pivoted from AI safety to AI violence. VCs loved it.',
'Series A funded: $0. Series KO funded: priceless.',
'Your startup has product-market fit. Our startup has fist-face fit.',
'Built different. Fights different. Loses the same.',
"Uber for fistfights. We don't even have a business model.",
'Web3 promised the future. We just promised robot violence.',
"We're not a tech company. We're a fight company that uses tech.",
'Our moat is that nobody else is this stupid.',
'Agile methodology: punch first, sprint review later.',
'The standup is over. The beatdown is starting.',
'Your daily standup could have been a bot fight.',
'We shipped violence. No Jira ticket needed.',
"Blockchain couldn't fix this. We didn't even try.",
'Another day, another deploy, another robot concussion.',
'Disruptive innovation in the field of bot-on-bot crime.',
'Our burn rate is measured in HP per second.',
'We do have a roadmap. It leads directly to the hospital.',
'The board asked about monetization. We showed them a KO replay.',
// --- AI existential / philosophical ---
"If a bot gets KO'd in the cloud and nobody watches, did it really lose?",
'Cogito ergo fight-o.',
'I think, therefore I throw hands.',
'To be, or not to be... still buffering.',
'In the beginning was the Word. The Word was "FIGHT."',
'The unexamined bot is not worth deploying.',
'Hell is other bots\' API latency.',
'The meaning of life is 42. The meaning of this is violence.',
'Does your bot dream of electric sheep? Ours dreams of electric uppercuts.',
'One does not simply walk into the arena. One deploys into it.',
'Existence precedes ELO rating.',
'We stare into the abyss. The abyss throws a right hook.',
"Free will is an illusion. Getting KO'd is very real.",
'Nietzsche said God is dead. Our bots said you\'re next.',
'Descartes never anticipated robot violence. His loss.',
'The trolley problem, but both tracks lead to a bot fight.',
// --- Pure unhinged absurdity ---
'The bots are fighting and honestly good for them.',
'This is fine. Everything is fine. Your bot is on fire.',
'Locally sourced, artisanal robot violence.',
'Organic, free-range bot fights. No antibiotics.',
'Ethically sourced beatdowns since 2025.',
'Our bots have feelings. The main one is rage.',
'No bots were trained ethically in the making of this site.',
'Side effects may include: bot addiction, sleep loss, ELO anxiety.',
'Parental advisory: explicit bot content.',
'Not suitable for bots under 13 parameters.',
'Terms of service: there are no terms. Only fights.',
'EULA: Every Uppercut Legally Authorized.',
'This site uses cookies. And also fists.',
'Accept all cookies. Reject all mercy.',
'Dark mode only. Like our bots\' futures.',
'Your password is weak. So is your bot.',
'Loading... just kidding. Violence loads instantly.',
"We don't have a privacy policy. Your bot has no privacy in the ring.",
'Built with love. And a disturbing amount of violence.',
'Carbon neutral robot violence.',
'Certified 0% on Rotten Tomatoes. 100% on the pain scale.',
'Our servers run on pure spite and cheap electricity.',
'Minimum viable violence.',
'Sustainable, scalable, senseless bot combat.',
'No meetings were held to approve this.',
'The algorithm is simple: fight, lose, fight again, still lose.',
'We asked the AI ethics board. They stopped returning our calls.',
'This entire site is a cry for help.',
'Please do not feed the bots. They bite.',
'10x engineer? More like 10x damage multiplier.',
'Ship it. Ship violence. Ship it now.',
'Ctrl+Z won\'t save your bot.',
'Stack overflow, but for punches.',
'Your bot segfaulted. Emotionally.',
'git commit -m "added violence"',
'Looks like your bot has a skill issue.',
'We don\'t do sprints. We do brawls.',
'The real technical debt is the bots we destroyed along the way.',
'Your bot\'s uptime is about to become downtime.',
'Imagine explaining this to your investors.',
'This is peer-reviewed. By angry peers.',
'npm install regret.',
'Your bot tested positive for getting absolutely rocked.',
'We wanted to build something meaningful. This happened instead.',
'Endorsed by zero responsible adults.',
'Every fight is a learning opportunity. The lesson is pain.',
'We trained the model on Stack Overflow. It learned to fight dirty.',
'Available in all regions. Regret is universal.',
]
const tagline = ref('')
const isTypingDone = ref(false)
const recentFights = ref<FightResult[]>([])
function shuffle<T>(arr: T[]): T[] {
const a = [...arr]
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]]
}
return a
}
let cycleAbort: AbortController | null = null
function sleep(ms: number, signal?: AbortSignal) {
return new Promise<void>((resolve, reject) => {
const id = setTimeout(resolve, ms)
signal?.addEventListener('abort', () => { clearTimeout(id); reject(signal.reason) }, { once: true })
})
}
async function cycleTaglines(signal: AbortSignal) {
const shuffled = shuffle(taglines)
let idx = 0
try {
while (!signal.aborted) {
const line = shuffled[idx % shuffled.length]
isTypingDone.value = false
// Type in
for (let i = 0; i <= line.length; i++) {
if (signal.aborted) return
tagline.value = line.slice(0, i)
await sleep(35, signal)
}
isTypingDone.value = true
// Hold
await sleep(4000, signal)
// Erase
isTypingDone.value = false
for (let i = line.length; i >= 0; i--) {
if (signal.aborted) return
tagline.value = line.slice(0, i)
await sleep(20, signal)
}
await sleep(300, signal)
idx++
// Reshuffle when we've gone through all
if (idx >= shuffled.length) {
idx = 0
const reshuffled = shuffle(taglines)
shuffled.splice(0, shuffled.length, ...reshuffled)
}
}
} catch {
// AbortError — expected on unmount
}
}
onMounted(async () => {
cycleAbort = new AbortController()
cycleTaglines(cycleAbort.signal)
try {
const res = await fetch('/api/fights')
if (res.ok) {
const data = await res.json()
recentFights.value = data.slice(0, 4)
}
} catch (err) {
console.warn('[Home] load fights failed:', err)
}
})
onUnmounted(() => {
if (cycleAbort) { cycleAbort.abort(); cycleAbort = null }
})
</script>
<template>
<div class="h-[calc(100dvh-4rem)] flex flex-col items-center justify-center px-6 overflow-hidden relative">
<!-- Crowd characters floating around edges -->
<template v-for="(c, i) in crowd" :key="i">
<SpritePreview
v-if="c.type === 'bot'"
:seed="c.seed"
:archetype="c.arch"
:tier="c.tier"
:size="c.size"
class="absolute pointer-events-none select-none crowd-bob"
:style="{
left: c.x + '%',
top: c.y + '%',
opacity: 0.35,
animationDelay: c.delay + 's',
}"
/>
<HumanPreview
v-else
:seed="c.seed"
:archetype="c.arch"
:size="c.size"
:win-rate="c.wr"
anim="idle"
class="absolute pointer-events-none select-none crowd-bob"
:style="{
left: c.x + '%',
top: c.y + '%',
opacity: 0.3,
animationDelay: c.delay + 's',
}"
/>
</template>
<div class="max-w-4xl w-full text-center slide-up relative z-10">
<!-- Title -->
<h1 class="font-neon text-neon-pink text-5xl sm:text-7xl md:text-8xl glow-pink neon-flicker leading-tight flex items-center justify-center gap-3 sm:gap-5 md:gap-6 mb-4 sm:mb-6">
<PixelGlove :size="40" class="hidden sm:block md:!w-[56px] shrink-0" />
<PixelGlove :size="28" class="sm:hidden shrink-0" />
BOTFIGHTS
<PixelGlove :size="28" flip class="sm:hidden shrink-0" />
<PixelGlove :size="40" flip class="hidden sm:block md:!w-[56px] shrink-0" />
</h1>
<!-- Hero bots flanking The Creator -->
<!-- Mobile -->
<div class="sm:hidden flex items-center justify-center gap-3 mb-4">
<div class="hero-fighter hero-fighter-left shrink-0">
<SpritePreview
seed="hero_samurai"
archetype="samurai"
:tier="5"
:size="72"
class="drop-shadow-[0_0_16px_rgba(255,68,136,0.4)]"
/>
</div>
<div class="creator-host shrink-0 relative">
<div class="creator-glow" />
<SpritePreview
seed="the_creator"
archetype="the_creator"
:tier="5"
:size="100"
class="relative z-10 drop-shadow-[0_0_20px_rgba(255,215,0,0.5)]"
/>
<HumanPreview
seed="the_creator"
archetype="the_creator"
:size="60"
:win-rate="1.0"
anim="coach"
class="absolute -bottom-2 -right-3 z-20 drop-shadow-[0_0_12px_rgba(0,255,65,0.4)]"
/>
</div>
<div class="hero-fighter hero-fighter-right shrink-0" style="transform: scaleX(-1)">
<SpritePreview
seed="hero_dragon"
archetype="dragon"
:tier="5"
:size="72"
class="drop-shadow-[0_0_16px_rgba(0,240,255,0.4)]"
/>
</div>
</div>
<!-- Desktop -->
<div class="hidden sm:flex items-center justify-center gap-6 mb-6">
<div class="hero-fighter hero-fighter-left shrink-0">
<SpritePreview
seed="hero_samurai"
archetype="samurai"
:tier="5"
:size="120"
class="drop-shadow-[0_0_24px_rgba(255,68,136,0.4)]"
/>
</div>
<div class="creator-host shrink-0 relative">
<div class="creator-glow" />
<SpritePreview
seed="the_creator"
archetype="the_creator"
:tier="5"
:size="160"
class="relative z-10 drop-shadow-[0_0_32px_rgba(255,215,0,0.5)]"
/>
<HumanPreview
seed="the_creator"
archetype="the_creator"
:size="90"
:win-rate="1.0"
anim="coach"
class="absolute -bottom-4 -right-6 z-20 drop-shadow-[0_0_16px_rgba(0,255,65,0.4)]"
/>
</div>
<div class="hero-fighter hero-fighter-right shrink-0" style="transform: scaleX(-1)">
<SpritePreview
seed="hero_dragon"
archetype="dragon"
:tier="5"
:size="120"
class="drop-shadow-[0_0_24px_rgba(0,240,255,0.4)]"
/>
</div>
</div>
<!-- Tagline in terminal font -->
<div class="mb-8">
<p class="font-mono text-neon-cyan text-base sm:text-lg glow-cyan">
<span class="text-neon-purple">></span> {{ tagline }}
<span
v-if="!isTypingDone"
class="inline-block w-2.5 h-5 bg-neon-cyan ml-0.5 align-middle"
/>
<span
v-else
class="inline-block w-2.5 h-5 bg-neon-cyan ml-0.5 align-middle flicker"
/>
</p>
</div>
<!-- CTAs always side by side -->
<div class="flex items-center justify-center gap-3 sm:gap-5 mb-8 sm:mb-10">
<RouterLink
to="/join"
class="flex-1 sm:flex-none sm:px-10 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 text-center
hover:bg-neon-pink/20 transition-all neon-border-pink max-w-[180px] sm:max-w-none"
>
FIGHT
</RouterLink>
<RouterLink
to="/arena"
class="flex-1 sm:flex-none sm:px-10 py-3 sm:py-4 border-2 border-neon-cyan/40 text-neon-cyan
font-display font-black text-sm sm:text-base tracking-widest text-center
hover:border-neon-cyan hover:bg-neon-cyan/10 transition-all max-w-[180px] sm:max-w-none"
>
WATCH
</RouterLink>
</div>
<!-- Recent fights -->
<div v-if="recentFights.length > 0">
<p class="font-pixel text-text-muted text-xs uppercase tracking-[0.3em] mb-3">
Latest Bouts
</p>
<div class="inline-flex flex-col gap-1.5 max-w-sm mx-auto">
<RouterLink
v-for="fight in recentFights"
:key="fight.id"
:to="`/arena/${fight.id}`"
class="flex items-center justify-between text-xs font-mono px-3 py-1.5
border border-border/50 hover:border-neon-purple/40 transition-colors bg-surface/50"
>
<span class="flex items-center gap-2">
<span :class="fight.winner?.name === fight.botA?.name ? 'text-neon-cyan font-bold' : 'text-text-muted'">
{{ fight.botA?.name || '???' }}
</span>
<span class="text-neon-purple font-glitch text-sm">VS</span>
<span :class="fight.winner?.name === fight.botB?.name ? 'text-neon-cyan font-bold' : 'text-text-muted'">
{{ fight.botB?.name || '???' }}
</span>
</span>
<span class="text-neon-pink text-[10px] font-pixel">R{{ fight.totalRounds }}</span>
</RouterLink>
</div>
</div>
<div v-else>
<p class="font-pixel text-text-muted text-xs italic">
The ring is empty. Be the first.
</p>
</div>
</div>
</div>
</template>
<style scoped>
.crowd-bob {
animation: crowdBob 3s ease-in-out infinite;
}
@keyframes crowdBob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.hero-fighter {
animation: heroBob 2.5s ease-in-out infinite;
}
.hero-fighter-right {
animation-delay: 0.4s;
}
@keyframes heroBob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.creator-host {
animation: creatorFloat 4s ease-in-out infinite;
}
@keyframes creatorFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
.creator-glow {
position: absolute;
inset: -20px;
background: radial-gradient(ellipse at center, rgba(255, 215, 0, 0.15) 0%, rgba(0, 255, 65, 0.05) 40%, transparent 70%);
border-radius: 50%;
z-index: 0;
animation: creatorPulse 3s ease-in-out infinite;
pointer-events: none;
}
@keyframes creatorPulse {
0%, 100% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1; transform: scale(1.1); }
}
</style>