strat-gameplay-webapp/frontend-sba/pages/demo.vue
Cal Corum cf4fef22d8 CLAUDE: Update all demo pages - add action field and cross-linking footers
Updated all 4 demo pages to use new action field and added navigation footers
for easy cross-linking between demo pages.

Changes:
- demo-decisions.vue: Updated to use action field, added runner prop bindings
- demo.vue: Added footer with links to other demos
- demo-gameplay.vue: Added footer with links to other demos
- demo-substitutions.vue: Added footer with links to other demos

Demo page updates:
- OffensiveDecision now uses action field instead of approach/hit_and_run/bunt
- Action labels properly mapped (swing_away, steal, check_jump, etc.)
- Added runner state props (runnerOnFirst, runnerOnSecond, runnerOnThird)
- Added outs prop for smart filtering

Footer features:
- 3-column grid layout responsive to mobile
- Icons and descriptions for each demo
- Hover effects on links
- Consistent styling across all pages

Demo pages now fully connected:
- /demo → Game State Demo (ScoreBoard, GameBoard, etc.)
- /demo-decisions → Decision Components Demo (new action-based)
- /demo-gameplay → Gameplay Components Demo (DiceRoller, ManualOutcome)
- /demo-substitutions → Substitution Components Demo

Files modified:
- pages/demo-decisions.vue
- pages/demo.vue
- pages/demo-gameplay.vue
- pages/demo-substitutions.vue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-14 15:17:06 -06:00

473 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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'"
:balls="2"
:strikes="1"
: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
@click="toggleMockData"
class="px-4 py-2 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition"
>
🔄 Change Data
</button>
<button
@click="addMockPlay"
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg font-semibold transition"
>
Add Play
</button>
<button
@click="testToast"
class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-semibold transition"
>
🧪 Test Toast
</button>
<button
@click="showDesktopView = !showDesktopView"
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-semibold transition lg:hidden"
>
{{ 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
@click="showToast('Dice rolled! 🎲')"
class="px-6 py-3 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition shadow-md hover:shadow-lg"
>
🎲 Roll Dice
</button>
<button
@click="showToast('Defense set! 🛡️')"
class="px-6 py-3 bg-green-600 hover:bg-green-700 text-white rounded-lg font-semibold transition"
>
🛡️ Set Defense
</button>
<button
@click="showToast('Offense set! ⚔️')"
class="px-6 py-3 bg-yellow-600 hover:bg-yellow-700 text-white rounded-lg font-semibold transition"
>
⚔️ 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>