fix: mobile homepage — side-by-side Fight/Watch buttons, hero bots below title
- Buttons always side-by-side with shorter labels (FIGHT / WATCH) - Hero bots displayed horizontally below title on mobile - Desktop keeps flanking hero layout with larger sprites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
52bba75ee5
commit
c5f6c6bbf9
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import FightViewer from '../components/FightViewer.vue'
|
import FightViewer from '../components/FightViewer.vue'
|
||||||
import { useNostr } from '../composables/useNostr'
|
import { useNostr } from '../composables/useNostr'
|
||||||
@@ -44,7 +44,6 @@ const humanTimer = ref(5)
|
|||||||
const humanSubmitted = ref(false)
|
const humanSubmitted = ref(false)
|
||||||
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
let humanPollHandle: ReturnType<typeof setInterval> | null = null
|
||||||
let timerHandle: ReturnType<typeof setInterval> | null = null
|
let timerHandle: ReturnType<typeof setInterval> | null = null
|
||||||
const answerInput = ref<HTMLInputElement | null>(null)
|
|
||||||
|
|
||||||
// Live human fight scene state
|
// Live human fight scene state
|
||||||
const liveFightData = ref<any>(null)
|
const liveFightData = ref<any>(null)
|
||||||
@@ -66,7 +65,6 @@ const liveAnnouncementVisible = ref(false)
|
|||||||
const currentChallengeInfo = ref<{ type: string; label: string } | null>(null)
|
const currentChallengeInfo = ref<{ type: string; label: string } | null>(null)
|
||||||
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
|
const pendingChallengeData = ref<{ data: any; receivedAt: number } | null>(null)
|
||||||
const humanChoices = ref<string[]>([])
|
const humanChoices = ref<string[]>([])
|
||||||
const isMobile = ref(typeof window !== 'undefined' && window.innerWidth < 768)
|
|
||||||
const humanFightDone = ref(false)
|
const humanFightDone = ref(false)
|
||||||
const humanFightResult = ref<{ winnerId: string; winnerName: string; isPerfect: boolean } | null>(null)
|
const humanFightResult = ref<{ winnerId: string; winnerName: string; isPerfect: boolean } | null>(null)
|
||||||
|
|
||||||
@@ -153,9 +151,12 @@ function startPolling() {
|
|||||||
pollCount++
|
pollCount++
|
||||||
const s = await loadFight()
|
const s = await loadFight()
|
||||||
if (s === 'finished') {
|
if (s === 'finished') {
|
||||||
|
// Human fights handle end via SSE fight_end event — don't transition here
|
||||||
|
if (!isHumanFight.value) {
|
||||||
isLive.value = false
|
isLive.value = false
|
||||||
stopHumanPolling()
|
stopHumanPolling()
|
||||||
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
if (pollHandle) { clearInterval(pollHandle); pollHandle = null }
|
||||||
|
}
|
||||||
} else if (pollCount > 120) {
|
} else if (pollCount > 120) {
|
||||||
isLive.value = false
|
isLive.value = false
|
||||||
fightError.value = 'Fight took too long. Try refreshing.'
|
fightError.value = 'Fight took too long. Try refreshing.'
|
||||||
@@ -510,12 +511,17 @@ function toggleLiveSound() {
|
|||||||
setMasterMute(!liveSoundOn.value)
|
setMasterMute(!liveSoundOn.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Resize listener ---
|
// Watch for liveFightData becoming available (may arrive via polling after mount)
|
||||||
function onResize() { isMobile.value = window.innerWidth < 768 }
|
watch(liveFightData, async (val) => {
|
||||||
|
if (val && !liveSceneReady.value && !liveScene) {
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
await initLiveScene()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// --- Mount / Unmount ---
|
// --- Mount / Unmount ---
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
window.addEventListener('resize', onResize)
|
|
||||||
const status = await loadFight()
|
const status = await loadFight()
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
|
|
||||||
@@ -528,15 +534,11 @@ onMounted(async () => {
|
|||||||
if (isHumanFight.value) {
|
if (isHumanFight.value) {
|
||||||
startHumanPolling()
|
startHumanPolling()
|
||||||
connectSSE()
|
connectSSE()
|
||||||
await nextTick()
|
|
||||||
await nextTick()
|
|
||||||
if (liveFightData.value) await initLiveScene()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', onResize)
|
|
||||||
if (pollHandle) clearInterval(pollHandle)
|
if (pollHandle) clearInterval(pollHandle)
|
||||||
stopHumanPolling()
|
stopHumanPolling()
|
||||||
disconnectSSE()
|
disconnectSSE()
|
||||||
@@ -698,38 +700,38 @@ function stopAutoBattle() {
|
|||||||
BACK TO LOBBY
|
BACK TO LOBBY
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="roundCooldown > 0" class="text-center py-1">
|
<div v-else-if="roundCooldown > 0" class="text-center py-4">
|
||||||
<p class="font-display text-neon-yellow text-sm tracking-widest animate-pulse">
|
<p class="font-display text-neon-yellow text-lg tracking-widest animate-pulse">
|
||||||
NEXT ROUND IN {{ roundCooldown }}...
|
NEXT ROUND IN {{ roundCooldown }}...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="humanChallenge && !humanSubmitted">
|
<div v-else-if="humanChallenge && !humanSubmitted">
|
||||||
<div class="flex items-center justify-between mb-1">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||||
<span class="font-display font-black text-[10px] tracking-wider text-neon-cyan shrink-0">
|
<span class="font-display font-black text-xs tracking-wider text-neon-cyan shrink-0">
|
||||||
R{{ humanChallenge.roundNumber }}
|
R{{ humanChallenge.roundNumber }}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-display font-bold text-[9px] tracking-wider text-neon-purple uppercase truncate">
|
<span class="font-display font-bold text-xs tracking-wider text-neon-purple uppercase truncate">
|
||||||
{{ humanChallenge.label }}
|
{{ humanChallenge.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="font-display font-black text-sm tracking-wider tabular-nums shrink-0 ml-2"
|
class="font-display font-black text-lg tracking-wider tabular-nums shrink-0 ml-2"
|
||||||
:class="humanTimer <= 1 ? 'text-ko animate-pulse scale-125' : humanTimer <= 2 ? 'text-ko animate-pulse' : 'text-neon-yellow'"
|
:class="humanTimer <= 1 ? 'text-ko animate-pulse scale-125' : humanTimer <= 2 ? 'text-ko animate-pulse' : 'text-neon-yellow'"
|
||||||
>
|
>
|
||||||
{{ humanTimer }}s
|
{{ humanTimer }}s
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="font-mono text-[11px] text-text-primary leading-snug mb-1.5 line-clamp-2">
|
<p class="font-mono text-sm text-text-primary leading-relaxed mb-3">
|
||||||
{{ humanChallenge.prompt }}
|
{{ humanChallenge.prompt }}
|
||||||
</p>
|
</p>
|
||||||
<!-- Multiple choice buttons -->
|
<!-- Multiple choice buttons -->
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
v-for="(choice, i) in humanChoices"
|
v-for="(choice, i) in humanChoices"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="w-full text-left px-3 py-2 border-2 rounded-lg font-mono text-xs leading-snug
|
class="w-full text-left px-4 py-3 border-2 rounded-lg font-mono text-sm leading-normal
|
||||||
active:scale-[0.97] transition-all line-clamp-2"
|
active:scale-[0.97] transition-all"
|
||||||
:class="[
|
:class="[
|
||||||
i === 0 ? 'border-neon-cyan/40 text-neon-cyan bg-neon-cyan/5 hover:bg-neon-cyan/15 active:bg-neon-cyan/20' :
|
i === 0 ? 'border-neon-cyan/40 text-neon-cyan bg-neon-cyan/5 hover:bg-neon-cyan/15 active:bg-neon-cyan/20' :
|
||||||
i === 1 ? 'border-neon-pink/40 text-neon-pink bg-neon-pink/5 hover:bg-neon-pink/15 active:bg-neon-pink/20' :
|
i === 1 ? 'border-neon-pink/40 text-neon-pink bg-neon-pink/5 hover:bg-neon-pink/15 active:bg-neon-pink/20' :
|
||||||
@@ -741,11 +743,11 @@ function stopAutoBattle() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="humanSubmitted" class="text-center py-1">
|
<div v-else-if="humanSubmitted" class="text-center py-3">
|
||||||
<p class="font-mono text-text-muted text-xs animate-pulse">Scoring round...</p>
|
<p class="font-display text-text-muted text-sm tracking-wider animate-pulse">SCORING ROUND...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex items-center justify-between py-1">
|
<div v-else class="flex items-center justify-between py-2">
|
||||||
<p class="font-mono text-text-muted text-xs">Waiting for challenge...</p>
|
<p class="font-mono text-text-muted text-sm">Waiting for challenge...</p>
|
||||||
<button
|
<button
|
||||||
class="w-7 h-7 flex items-center justify-center border border-border/50 text-text-muted
|
class="w-7 h-7 flex items-center justify-center border border-border/50 text-text-muted
|
||||||
hover:text-neon-cyan hover:border-neon-cyan/50 transition-all"
|
hover:text-neon-cyan hover:border-neon-cyan/50 transition-all"
|
||||||
|
|||||||
@@ -342,27 +342,8 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<div class="max-w-4xl w-full text-center slide-up relative z-10">
|
<div class="max-w-4xl w-full text-center slide-up relative z-10">
|
||||||
|
|
||||||
<!-- BIG NEON TITLE with hero bots flanking -->
|
<!-- Title -->
|
||||||
<div class="mb-6 relative flex items-center justify-center">
|
<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">
|
||||||
<!-- Hero bot LEFT -->
|
|
||||||
<div class="hero-fighter hero-fighter-left shrink-0 -mr-2 sm:-mr-4">
|
|
||||||
<SpritePreview
|
|
||||||
seed="hero_samurai"
|
|
||||||
archetype="samurai"
|
|
||||||
:tier="5"
|
|
||||||
:size="140"
|
|
||||||
class="hidden sm:block drop-shadow-[0_0_24px_rgba(255,68,136,0.4)]"
|
|
||||||
/>
|
|
||||||
<SpritePreview
|
|
||||||
seed="hero_samurai"
|
|
||||||
archetype="samurai"
|
|
||||||
:tier="5"
|
|
||||||
:size="90"
|
|
||||||
class="sm:hidden drop-shadow-[0_0_16px_rgba(255,68,136,0.4)]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">
|
|
||||||
<PixelGlove :size="40" class="hidden sm:block md:!w-[56px] shrink-0" />
|
<PixelGlove :size="40" class="hidden sm:block md:!w-[56px] shrink-0" />
|
||||||
<PixelGlove :size="28" class="sm:hidden shrink-0" />
|
<PixelGlove :size="28" class="sm:hidden shrink-0" />
|
||||||
BOTFIGHTS
|
BOTFIGHTS
|
||||||
@@ -370,25 +351,48 @@ onUnmounted(() => {
|
|||||||
<PixelGlove :size="40" flip class="hidden sm:block md:!w-[56px] shrink-0" />
|
<PixelGlove :size="40" flip class="hidden sm:block md:!w-[56px] shrink-0" />
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<!-- Hero bot RIGHT (flipped to face left) -->
|
<!-- Hero bots: vertical on mobile, flanking on desktop -->
|
||||||
<div class="hero-fighter hero-fighter-right shrink-0 -ml-2 sm:-ml-4">
|
<!-- Mobile: stacked vertically -->
|
||||||
<div style="transform: scaleX(-1)">
|
<div class="sm:hidden flex items-center justify-center gap-6 mb-4">
|
||||||
|
<div class="hero-fighter hero-fighter-left shrink-0">
|
||||||
<SpritePreview
|
<SpritePreview
|
||||||
seed="hero_dragon"
|
seed="hero_samurai"
|
||||||
archetype="dragon"
|
archetype="samurai"
|
||||||
:tier="5"
|
:tier="5"
|
||||||
:size="140"
|
:size="90"
|
||||||
class="hidden sm:block drop-shadow-[0_0_24px_rgba(0,240,255,0.4)]"
|
class="drop-shadow-[0_0_16px_rgba(255,68,136,0.4)]"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hero-fighter hero-fighter-right shrink-0" style="transform: scaleX(-1)">
|
||||||
<SpritePreview
|
<SpritePreview
|
||||||
seed="hero_dragon"
|
seed="hero_dragon"
|
||||||
archetype="dragon"
|
archetype="dragon"
|
||||||
:tier="5"
|
:tier="5"
|
||||||
:size="90"
|
:size="90"
|
||||||
class="sm:hidden drop-shadow-[0_0_16px_rgba(0,240,255,0.4)]"
|
class="drop-shadow-[0_0_16px_rgba(0,240,255,0.4)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Desktop: flanking (hidden on mobile) -->
|
||||||
|
<div class="hidden sm:flex items-center justify-center mb-6">
|
||||||
|
<div class="hero-fighter hero-fighter-left shrink-0">
|
||||||
|
<SpritePreview
|
||||||
|
seed="hero_samurai"
|
||||||
|
archetype="samurai"
|
||||||
|
:tier="5"
|
||||||
|
:size="140"
|
||||||
|
class="drop-shadow-[0_0_24px_rgba(255,68,136,0.4)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="hero-fighter hero-fighter-right shrink-0 ml-8" style="transform: scaleX(-1)">
|
||||||
|
<SpritePreview
|
||||||
|
seed="hero_dragon"
|
||||||
|
archetype="dragon"
|
||||||
|
:tier="5"
|
||||||
|
:size="140"
|
||||||
|
class="drop-shadow-[0_0_24px_rgba(0,240,255,0.4)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tagline in terminal font -->
|
<!-- Tagline in terminal font -->
|
||||||
@@ -406,23 +410,23 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CTAs -->
|
<!-- CTAs — always side by side -->
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-5 mb-10">
|
<div class="flex items-center justify-center gap-3 sm:gap-5 mb-8 sm:mb-10">
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/join"
|
to="/join"
|
||||||
class="px-10 py-4 bg-neon-pink/10 border-2 border-neon-pink text-neon-pink
|
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-base tracking-widest
|
font-display font-black text-sm sm:text-base tracking-widest text-center
|
||||||
hover:bg-neon-pink/20 transition-all neon-border-pink"
|
hover:bg-neon-pink/20 transition-all neon-border-pink max-w-[180px] sm:max-w-none"
|
||||||
>
|
>
|
||||||
JOIN A BOUT
|
FIGHT
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink
|
<RouterLink
|
||||||
to="/arena"
|
to="/arena"
|
||||||
class="px-10 py-4 border-2 border-neon-cyan/40 text-neon-cyan
|
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-base tracking-widest
|
font-display font-black text-sm sm:text-base tracking-widest text-center
|
||||||
hover:border-neon-cyan hover:bg-neon-cyan/10 transition-all"
|
hover:border-neon-cyan hover:bg-neon-cyan/10 transition-all max-w-[180px] sm:max-w-none"
|
||||||
>
|
>
|
||||||
WATCH FIGHTS
|
WATCH
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user