- Remove Count box (not needed for this game mode) - Stack Runners above Outs in compact vertical column - Fix diamond orientation: bases now at corners (matching GameBoard) - Change base markers from circles to diamond squares - Remove home plate, keep only 1st/2nd/3rd bases - Remove diamond outline for cleaner look - Remove Outs label, keep only indicator circles - Tighten spacing to align with Inning box height - Remove unused balls/strikes props 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
471 lines
15 KiB
Vue
471 lines
15 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||
<!-- Sticky ScoreBoard Header -->
|
||
<div class="sticky top-0 z-20">
|
||
<ScoreBoard
|
||
:home-score="5"
|
||
:away-score="3"
|
||
:inning="7"
|
||
:half="'bottom'"
|
||
:outs="2"
|
||
:runners="{ first: true, second: false, third: true }"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Demo Header -->
|
||
<div class="container mx-auto px-4 py-6">
|
||
<div class="bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-200 dark:border-blue-700 rounded-xl p-6 mb-6">
|
||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||
🎨 Phase F2 Component Demo
|
||
</h1>
|
||
<p class="text-gray-600 dark:text-gray-400">
|
||
Preview of all game display components with sample data. Scroll down to see everything!
|
||
</p>
|
||
<div class="mt-4 flex flex-wrap gap-3">
|
||
<button
|
||
class="px-4 py-2 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition"
|
||
@click="toggleMockData"
|
||
>
|
||
🔄 Change Data
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-semibold transition"
|
||
@click="addMockPlay"
|
||
>
|
||
➕ Add Play
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-semibold transition"
|
||
@click="testToast"
|
||
>
|
||
🧪 Test Toast
|
||
</button>
|
||
<button
|
||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-semibold transition lg:hidden"
|
||
@click="showDesktopView = !showDesktopView"
|
||
>
|
||
{{ showDesktopView ? '📱 Mobile View' : '💻 Desktop View' }}
|
||
</button>
|
||
<NuxtLink
|
||
to="/"
|
||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-semibold transition"
|
||
>
|
||
← Back Home
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- UNIFIED LAYOUT (works on all screen sizes) -->
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
<!-- Left/Main Column: Game State -->
|
||
<div class="lg:col-span-2 space-y-6">
|
||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||
<span class="lg:hidden">📱 Mobile Layout</span>
|
||
<span class="hidden lg:inline">💻 Desktop Layout</span>
|
||
</h2>
|
||
|
||
<!-- Current Situation -->
|
||
<CurrentSituation
|
||
v-if="mockBatter && mockPitcher"
|
||
:current-batter="mockBatter"
|
||
:current-pitcher="mockPitcher"
|
||
/>
|
||
|
||
<!-- Game Board -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 lg:p-6 shadow-lg">
|
||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">⚾ Baseball Diamond</h3>
|
||
<GameBoard
|
||
v-if="mockBatter && mockPitcher"
|
||
:runners="mockRunners"
|
||
:current-batter="mockBatter"
|
||
:current-pitcher="mockPitcher"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Action Panel -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 lg:p-6 shadow-lg">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">⚡ Game Actions</h3>
|
||
<div class="flex flex-col lg:flex-row flex-wrap gap-3 lg:gap-4">
|
||
<button
|
||
class="px-6 py-3 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition shadow-md hover:shadow-lg"
|
||
@click="showToast('Dice rolled! 🎲')"
|
||
>
|
||
🎲 Roll Dice
|
||
</button>
|
||
<button
|
||
class="px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg font-semibold transition"
|
||
@click="showToast('Defense set! 🛡️')"
|
||
>
|
||
🛡️ Set Defense
|
||
</button>
|
||
<button
|
||
class="px-6 py-3 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg font-semibold transition"
|
||
@click="showToast('Offense set! ⚔️')"
|
||
>
|
||
⚔️ Set Offense
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Column: Play-by-Play (Desktop) / Below (Mobile) -->
|
||
<div class="lg:col-span-1">
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl p-4 lg:p-6 shadow-lg lg:sticky lg:top-24">
|
||
<PlayByPlay
|
||
v-if="mockPlays.length > 0"
|
||
:plays="mockPlays"
|
||
:scrollable="true"
|
||
:max-height="600"
|
||
:show-filters="true"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Component Info Cards -->
|
||
<div class="mt-12">
|
||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">📊 Components Built</h2>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-md border-l-4 border-blue-500">
|
||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">ScoreBoard</h3>
|
||
<p class="text-sm text-gray-600 dark:text-gray-400">265 lines • Sticky header with live game state</p>
|
||
</div>
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-md border-l-4 border-green-500">
|
||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">GameBoard</h3>
|
||
<p class="text-sm text-gray-600 dark:text-gray-400">240 lines • Baseball diamond visualization</p>
|
||
</div>
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-md border-l-4 border-red-500">
|
||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">CurrentSituation</h3>
|
||
<p class="text-sm text-gray-600 dark:text-gray-400">205 lines • Pitcher vs Batter cards</p>
|
||
</div>
|
||
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-md border-l-4 border-yellow-500">
|
||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-2">PlayByPlay</h3>
|
||
<p class="text-sm text-gray-600 dark:text-gray-400">280 lines • Animated play feed</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Debug Info -->
|
||
<div class="mt-6 bg-gray-800 text-white rounded-lg p-4 font-mono text-xs">
|
||
<div><strong>Screen Width:</strong> {{ screenWidth }}px ({{ screenSize }})</div>
|
||
<div><strong>Mobile View Active:</strong> {{ screenWidth < 1024 }}</div>
|
||
<div><strong>Components Loaded:</strong> ✅ All 4</div>
|
||
<div><strong>Mock Data Loaded:</strong> ✅ Batter: {{ mockBatter?.player.name }}, Pitcher: {{ mockPitcher?.player.name }}</div>
|
||
<div class="mt-2 pt-2 border-t border-gray-600">
|
||
<strong>Toast Debug:</strong>
|
||
<span :class="toastMessage ? 'text-green-400' : 'text-red-400'">
|
||
{{ toastMessage ? `Active: "${toastMessage}"` : 'Inactive (no message)' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Footer with Demo Links -->
|
||
<div class="mt-12 border-t border-gray-200 dark:border-gray-700 pt-8">
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">
|
||
📚 Other Demo Pages
|
||
</h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<NuxtLink
|
||
to="/demo-decisions"
|
||
class="p-4 rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
||
>
|
||
<div class="text-2xl mb-2">⚔️</div>
|
||
<div class="font-semibold text-gray-900 dark:text-white">Decisions Demo</div>
|
||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||
Decision input components
|
||
</div>
|
||
</NuxtLink>
|
||
<NuxtLink
|
||
to="/demo-gameplay"
|
||
class="p-4 rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
||
>
|
||
<div class="text-2xl mb-2">🎲</div>
|
||
<div class="font-semibold text-gray-900 dark:text-white">Gameplay Demo</div>
|
||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||
Dice rolling & manual outcomes
|
||
</div>
|
||
</NuxtLink>
|
||
<NuxtLink
|
||
to="/demo-substitutions"
|
||
class="p-4 rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
||
>
|
||
<div class="text-2xl mb-2">🔄</div>
|
||
<div class="font-semibold text-gray-900 dark:text-white">Substitutions Demo</div>
|
||
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||
Player substitution workflow
|
||
</div>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast Notification - CENTER (Known working position) -->
|
||
<!-- TODO: Bottom positioning has a glitch - see PHASE_F2_COMPLETE.md Known Issues -->
|
||
<div
|
||
v-if="toastMessage"
|
||
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gradient-to-r from-blue-600 to-blue-700 text-white px-10 py-5 rounded-2xl shadow-2xl border-3 border-blue-300"
|
||
style="z-index: 9999;"
|
||
>
|
||
<div class="flex items-center gap-4">
|
||
<div class="w-10 h-10 bg-white/30 rounded-full flex items-center justify-center animate-pulse">
|
||
<span class="text-2xl">✓</span>
|
||
</div>
|
||
<span class="font-bold text-xl">{{ toastMessage }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
|
||
import GameBoard from '~/components/Game/GameBoard.vue'
|
||
import CurrentSituation from '~/components/Game/CurrentSituation.vue'
|
||
import PlayByPlay from '~/components/Game/PlayByPlay.vue'
|
||
import type { LineupPlayerState, PlayResult } from '~/types/game'
|
||
|
||
// Reactive screen width
|
||
const screenWidth = ref(0)
|
||
const showDesktopView = ref(false)
|
||
|
||
const screenSize = computed(() => {
|
||
if (screenWidth.value < 640) return 'xs'
|
||
if (screenWidth.value < 768) return 'sm'
|
||
if (screenWidth.value < 1024) return 'md'
|
||
if (screenWidth.value < 1280) return 'lg'
|
||
return 'xl'
|
||
})
|
||
|
||
// Update screen width on resize
|
||
onMounted(() => {
|
||
screenWidth.value = window.innerWidth
|
||
window.addEventListener('resize', () => {
|
||
screenWidth.value = window.innerWidth
|
||
})
|
||
})
|
||
|
||
// Mock data
|
||
const mockBatter = ref<LineupPlayerState>({
|
||
lineup_id: 1,
|
||
player: {
|
||
id: 101,
|
||
name: 'Mike Trout',
|
||
image: '',
|
||
team: 'Los Angeles Angels',
|
||
manager: 'Phil Nevin'
|
||
},
|
||
position: 'CF',
|
||
batting_order: 3,
|
||
is_starter: true,
|
||
is_active: true
|
||
})
|
||
|
||
const mockPitcher = ref<LineupPlayerState>({
|
||
lineup_id: 2,
|
||
player: {
|
||
id: 102,
|
||
name: 'Clayton Kershaw',
|
||
image: '',
|
||
team: 'Los Angeles Dodgers',
|
||
manager: 'Dave Roberts'
|
||
},
|
||
position: 'SP',
|
||
batting_order: null,
|
||
is_starter: true,
|
||
is_active: true
|
||
})
|
||
|
||
const mockRunners = ref({
|
||
first: true,
|
||
second: false,
|
||
third: true
|
||
})
|
||
|
||
const mockPlays = ref<PlayResult[]>([
|
||
{
|
||
play_number: 45,
|
||
outcome: 'SINGLE_1',
|
||
description: 'Mike Trout singles to center field',
|
||
runs_scored: 1,
|
||
outs_recorded: 0,
|
||
inning: 7,
|
||
new_state: {}
|
||
},
|
||
{
|
||
play_number: 44,
|
||
outcome: 'STRIKEOUT',
|
||
description: 'Shohei Ohtani strikes out swinging',
|
||
runs_scored: 0,
|
||
outs_recorded: 1,
|
||
inning: 7,
|
||
new_state: {}
|
||
},
|
||
{
|
||
play_number: 43,
|
||
outcome: 'WALK',
|
||
description: 'Aaron Judge walks',
|
||
runs_scored: 0,
|
||
outs_recorded: 0,
|
||
inning: 7,
|
||
new_state: {}
|
||
},
|
||
{
|
||
play_number: 42,
|
||
outcome: 'HOMERUN',
|
||
description: 'Giancarlo Stanton crushes a 2-run homer to left field',
|
||
runs_scored: 2,
|
||
outs_recorded: 0,
|
||
inning: 6,
|
||
new_state: {}
|
||
},
|
||
{
|
||
play_number: 41,
|
||
outcome: 'DOUBLE_2',
|
||
description: 'Mookie Betts doubles down the line',
|
||
runs_scored: 0,
|
||
outs_recorded: 0,
|
||
inning: 6,
|
||
new_state: {}
|
||
}
|
||
])
|
||
|
||
const toastMessage = ref('')
|
||
|
||
// Methods
|
||
const toggleMockData = () => {
|
||
// Toggle between different players
|
||
if (mockBatter.value.player.name === 'Mike Trout') {
|
||
mockBatter.value = {
|
||
...mockBatter.value,
|
||
player: {
|
||
id: 103,
|
||
name: 'Shohei Ohtani',
|
||
image: '',
|
||
team: 'Los Angeles Angels',
|
||
manager: 'Phil Nevin'
|
||
},
|
||
position: 'DH',
|
||
batting_order: 4
|
||
}
|
||
mockPitcher.value = {
|
||
...mockPitcher.value,
|
||
player: {
|
||
id: 104,
|
||
name: 'Gerrit Cole',
|
||
image: '',
|
||
team: 'New York Yankees',
|
||
manager: 'Aaron Boone'
|
||
}
|
||
}
|
||
} else {
|
||
mockBatter.value = {
|
||
...mockBatter.value,
|
||
player: {
|
||
id: 101,
|
||
name: 'Mike Trout',
|
||
image: '',
|
||
team: 'Los Angeles Angels',
|
||
manager: 'Phil Nevin'
|
||
},
|
||
position: 'CF',
|
||
batting_order: 3
|
||
}
|
||
mockPitcher.value = {
|
||
...mockPitcher.value,
|
||
player: {
|
||
id: 102,
|
||
name: 'Clayton Kershaw',
|
||
image: '',
|
||
team: 'Los Angeles Dodgers',
|
||
manager: 'Dave Roberts'
|
||
}
|
||
}
|
||
}
|
||
|
||
// Toggle runners
|
||
mockRunners.value = {
|
||
first: !mockRunners.value.first,
|
||
second: !mockRunners.value.second,
|
||
third: !mockRunners.value.third
|
||
}
|
||
|
||
showToast('Mock data updated! 🔄')
|
||
}
|
||
|
||
const addMockPlay = () => {
|
||
const outcomes = ['SINGLE_1', 'DOUBLE_2', 'TRIPLE', 'HOMERUN', 'STRIKEOUT', 'GROUNDOUT', 'FLYOUT', 'WALK']
|
||
const players = ['Mike Trout', 'Shohei Ohtani', 'Aaron Judge', 'Mookie Betts', 'Ronald Acuña Jr.']
|
||
const randomOutcome = outcomes[Math.floor(Math.random() * outcomes.length)]
|
||
const randomPlayer = players[Math.floor(Math.random() * players.length)]
|
||
|
||
const newPlay: PlayResult = {
|
||
play_number: mockPlays.value[0].play_number + 1,
|
||
outcome: randomOutcome,
|
||
description: `${randomPlayer} - ${randomOutcome.toLowerCase().replace(/_/g, ' ')}`,
|
||
runs_scored: randomOutcome.includes('HOMERUN') ? 1 : 0,
|
||
outs_recorded: randomOutcome.includes('OUT') ? 1 : 0,
|
||
inning: 7,
|
||
new_state: {}
|
||
}
|
||
|
||
mockPlays.value.unshift(newPlay)
|
||
showToast('New play added! ⚾')
|
||
}
|
||
|
||
const showToast = (message: string) => {
|
||
console.log('[Toast] Showing message:', message)
|
||
toastMessage.value = message
|
||
setTimeout(() => {
|
||
console.log('[Toast] Hiding message')
|
||
toastMessage.value = ''
|
||
}, 3000)
|
||
}
|
||
|
||
const testToast = () => {
|
||
console.log('[Toast Test] Button clicked!')
|
||
console.log('[Toast Test] Current toastMessage value:', toastMessage.value)
|
||
showToast('🧪 TEST TOAST - If you see this, it works!')
|
||
console.log('[Toast Test] After showToast, toastMessage value:', toastMessage.value)
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* Toast animation - Slide up from bottom with bounce */
|
||
.toast-enter-active {
|
||
animation: slideUpBounce 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
|
||
.toast-leave-active {
|
||
animation: slideDown 0.3s ease-in;
|
||
}
|
||
|
||
@keyframes slideUpBounce {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translateX(-50%) translateY(100px);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes slideDown {
|
||
0% {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
100% {
|
||
opacity: 0;
|
||
transform: translateX(-50%) translateY(20px);
|
||
}
|
||
}
|
||
|
||
/* Button press effect */
|
||
button:active {
|
||
transform: scale(0.95);
|
||
transition: transform 0.1s ease;
|
||
}
|
||
</style>
|