strat-gameplay-webapp/frontend-sba/pages/demo-gameplay.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

728 lines
20 KiB
Vue

<template>
<div class="demo-page">
<!-- Header -->
<div class="demo-header">
<h1 class="demo-title">Phase F4: Gameplay Components Demo</h1>
<p class="demo-subtitle">
Dice Rolling, Outcome Entry, and Play Results
</p>
</div>
<!-- Tabs -->
<div class="demo-tabs">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-button', activeTab === tab.id ? 'tab-active' : 'tab-inactive']"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</div>
<!-- Tab Content -->
<div class="demo-content">
<!-- Tab 1: DiceRoller -->
<div v-if="activeTab === 'dice'" class="tab-panel">
<div class="section">
<h2 class="section-title">DiceRoller Component</h2>
<p class="section-description">
Visual dice roller showing all 4 dice values with roll animation and special event indicators.
</p>
<div class="demo-controls">
<button class="control-button" @click="toggleDiceRoll">
{{ demoRollData ? 'Clear Roll' : 'Simulate Roll' }}
</button>
<button class="control-button" @click="rollWithWildPitch">
Roll with Wild Pitch
</button>
<button class="control-button" @click="rollWithPassedBall">
Roll with Passed Ball
</button>
</div>
<div class="component-showcase">
<DiceRoller
:can-roll="!demoRollData"
:pending-roll="demoRollData"
@roll="handleDemoRoll"
/>
</div>
</div>
</div>
<!-- Tab 2: ManualOutcomeEntry -->
<div v-if="activeTab === 'outcome'" class="tab-panel">
<div class="section">
<h2 class="section-title">ManualOutcomeEntry Component</h2>
<p class="section-description">
Outcome selection form with categorized outcomes and conditional hit location selector.
</p>
<div class="demo-controls">
<button class="control-button" @click="toggleOutcomeRoll">
{{ outcomeRollData ? 'Clear Roll' : 'Add Roll Data' }}
</button>
</div>
<div class="component-showcase">
<ManualOutcomeEntry
:roll-data="outcomeRollData"
:can-submit="!!outcomeRollData"
@submit="handleOutcomeSubmit"
@cancel="handleOutcomeCancel"
/>
</div>
<div v-if="lastSubmittedOutcome" class="result-box">
<h3 class="result-title">Last Submitted:</h3>
<pre class="result-code">{{ JSON.stringify(lastSubmittedOutcome, null, 2) }}</pre>
</div>
</div>
</div>
<!-- Tab 3: PlayResult -->
<div v-if="activeTab === 'result'" class="tab-panel">
<div class="section">
<h2 class="section-title">PlayResult Component</h2>
<p class="section-description">
Animated play result display with stats, color-coding, and runner advancement visualization.
</p>
<div class="demo-controls">
<button class="control-button" @click="showStrikeout">
Strikeout (Out)
</button>
<button class="control-button" @click="showHomerun">
Homerun (Runs)
</button>
<button class="control-button" @click="showSingle">
Single (Hit)
</button>
<button class="control-button" @click="showDoublePlay">
Double Play (2 Outs)
</button>
<button class="control-button" @click="showTripleWithRunners">
Triple (Runners Advance)
</button>
<button class="control-button" @click="clearResult">
Clear Result
</button>
</div>
<div class="component-showcase">
<PlayResultDisplay
:result="demoPlayResult"
:auto-hide="false"
@dismiss="handleResultDismiss"
/>
</div>
</div>
</div>
<!-- Tab 4: GameplayPanel (Full Integration) -->
<div v-if="activeTab === 'panel'" class="tab-panel">
<div class="section">
<h2 class="section-title">GameplayPanel Component</h2>
<p class="section-description">
Complete workflow orchestration - combines all components with state management.
</p>
<div class="demo-controls">
<div class="control-group">
<label class="control-label">
<input v-model="panelIsMyTurn" type="checkbox">
Is My Turn
</label>
<label class="control-label">
<input v-model="panelCanRoll" type="checkbox">
Can Roll Dice
</label>
<label class="control-label">
<input v-model="panelCanSubmit" type="checkbox">
Can Submit Outcome
</label>
</div>
<button class="control-button" @click="simulatePanelRoll">
Simulate Dice Roll
</button>
<button class="control-button" @click="simulatePanelResult">
Simulate Play Result
</button>
<button class="control-button" @click="resetPanelState">
Reset to Idle
</button>
</div>
<div class="component-showcase">
<GameplayPanel
game-id="demo-game-123"
:is-my-turn="panelIsMyTurn"
:can-roll-dice="panelCanRoll"
:pending-roll="panelRollData"
:last-play-result="panelPlayResult"
:can-submit-outcome="panelCanSubmit"
@roll-dice="handlePanelRollDice"
@submit-outcome="handlePanelSubmitOutcome"
@dismiss-result="handlePanelDismissResult"
/>
</div>
<div v-if="panelEvents.length > 0" class="events-log">
<h3 class="events-title">Event Log:</h3>
<div class="events-list">
<div v-for="(event, index) in panelEvents" :key="index" class="event-item">
<span class="event-time">{{ event.time }}</span>
<span class="event-type">{{ event.type }}</span>
<pre v-if="event.data" class="event-data">{{ JSON.stringify(event.data, null, 2) }}</pre>
</div>
</div>
</div>
</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"
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">Game State Demo</div>
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
Live game state visualization
</div>
</NuxtLink>
<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-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>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { RollData, PlayResult, PlayOutcome } from '~/types'
import DiceRoller from '~/components/Gameplay/DiceRoller.vue'
import ManualOutcomeEntry from '~/components/Gameplay/ManualOutcomeEntry.vue'
import PlayResultDisplay from '~/components/Gameplay/PlayResult.vue'
import GameplayPanel from '~/components/Gameplay/GameplayPanel.vue'
// Tab state
const activeTab = ref('dice')
const tabs = [
{ id: 'dice', label: 'DiceRoller' },
{ id: 'outcome', label: 'Outcome Entry' },
{ id: 'result', label: 'Play Result' },
{ id: 'panel', label: 'Full Panel' },
]
// DiceRoller demo state
const demoRollData = ref<RollData | null>(null)
const createRandomRoll = (overrides = {}): RollData => ({
roll_id: `roll-${Date.now()}`,
d6_one: Math.floor(Math.random() * 6) + 1,
d6_two_a: Math.floor(Math.random() * 6) + 1,
d6_two_b: Math.floor(Math.random() * 6) + 1,
d6_two_total: 0,
chaos_d20: Math.floor(Math.random() * 20) + 1,
resolution_d20: Math.floor(Math.random() * 20) + 1,
check_wild_pitch: false,
check_passed_ball: false,
timestamp: new Date().toISOString(),
...overrides,
})
const toggleDiceRoll = () => {
if (demoRollData.value) {
demoRollData.value = null
} else {
const roll = createRandomRoll()
roll.d6_two_total = roll.d6_two_a + roll.d6_two_b
demoRollData.value = roll
}
}
const rollWithWildPitch = () => {
const roll = createRandomRoll({ check_wild_pitch: true })
roll.d6_two_total = roll.d6_two_a + roll.d6_two_b
demoRollData.value = roll
}
const rollWithPassedBall = () => {
const roll = createRandomRoll({ check_passed_ball: true })
roll.d6_two_total = roll.d6_two_a + roll.d6_two_b
demoRollData.value = roll
}
const handleDemoRoll = () => {
const roll = createRandomRoll()
roll.d6_two_total = roll.d6_two_a + roll.d6_two_b
demoRollData.value = roll
}
// ManualOutcomeEntry demo state
const outcomeRollData = ref<RollData | null>(null)
const lastSubmittedOutcome = ref<any>(null)
const toggleOutcomeRoll = () => {
if (outcomeRollData.value) {
outcomeRollData.value = null
} else {
const roll = createRandomRoll()
roll.d6_two_total = roll.d6_two_a + roll.d6_two_b
outcomeRollData.value = roll
}
}
const handleOutcomeSubmit = (payload: { outcome: PlayOutcome; hitLocation?: string }) => {
lastSubmittedOutcome.value = payload
}
const handleOutcomeCancel = () => {
lastSubmittedOutcome.value = null
}
// PlayResult demo state
const demoPlayResult = ref<PlayResult | null>(null)
const showStrikeout = () => {
demoPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: 'STRIKEOUT',
description: 'Mike Trout strikes out swinging',
outs_recorded: 1,
runs_scored: 0,
runners_advanced: [],
batter_result: null,
new_state: {},
is_hit: false,
is_out: true,
is_walk: false,
is_strikeout: true,
}
}
const showHomerun = () => {
demoPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: 'HOMERUN',
description: 'Aaron Judge crushes a solo homerun to left field',
outs_recorded: 0,
runs_scored: 1,
runners_advanced: [
{ from: 0, to: 4, out: false },
],
batter_result: 4,
new_state: {},
is_hit: true,
is_out: false,
is_walk: false,
is_strikeout: false,
}
}
const showSingle = () => {
demoPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: 'SINGLE_1',
description: 'Mookie Betts singles to right field',
outs_recorded: 0,
runs_scored: 0,
runners_advanced: [
{ from: 0, to: 1, out: false },
],
batter_result: 1,
new_state: {},
is_hit: true,
is_out: false,
is_walk: false,
is_strikeout: false,
}
}
const showDoublePlay = () => {
demoPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: 'DOUBLE_PLAY',
description: 'Ground ball to shortstop, double play 6-4-3',
outs_recorded: 2,
runs_scored: 0,
runners_advanced: [
{ from: 1, to: 2, out: true },
{ from: 0, to: 1, out: true },
],
batter_result: null,
new_state: {},
is_hit: false,
is_out: true,
is_walk: false,
is_strikeout: false,
}
}
const showTripleWithRunners = () => {
demoPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: 'TRIPLE',
description: 'Ronald Acuna Jr. triples to the gap, clearing the bases',
outs_recorded: 0,
runs_scored: 2,
runners_advanced: [
{ from: 2, to: 4, out: false },
{ from: 1, to: 4, out: false },
{ from: 0, to: 3, out: false },
],
batter_result: 3,
new_state: {},
is_hit: true,
is_out: false,
is_walk: false,
is_strikeout: false,
}
}
const clearResult = () => {
demoPlayResult.value = null
}
const handleResultDismiss = () => {
demoPlayResult.value = null
}
// GameplayPanel demo state
const panelIsMyTurn = ref(true)
const panelCanRoll = ref(true)
const panelCanSubmit = ref(false)
const panelRollData = ref<RollData | null>(null)
const panelPlayResult = ref<PlayResult | null>(null)
const panelEvents = ref<Array<{ time: string; type: string; data?: any }>>([])
const addPanelEvent = (type: string, data?: any) => {
panelEvents.value.unshift({
time: new Date().toLocaleTimeString(),
type,
data,
})
if (panelEvents.value.length > 10) {
panelEvents.value.pop()
}
}
const handlePanelRollDice = () => {
addPanelEvent('rollDice')
// Simulate roll result
const roll = createRandomRoll()
roll.d6_two_total = roll.d6_two_a + roll.d6_two_b
panelRollData.value = roll
panelCanRoll.value = false
panelCanSubmit.value = true
}
const handlePanelSubmitOutcome = (payload: { outcome: PlayOutcome; hitLocation?: string }) => {
addPanelEvent('submitOutcome', payload)
// Simulate processing and result
panelRollData.value = null
panelCanSubmit.value = false
setTimeout(() => {
panelPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: payload.outcome,
description: `Play resolved: ${payload.outcome}${payload.hitLocation ? ` to ${payload.hitLocation}` : ''}`,
outs_recorded: payload.outcome.includes('OUT') ? 1 : 0,
runs_scored: payload.outcome === 'HOMERUN' ? 1 : 0,
runners_advanced: [],
batter_result: payload.outcome === 'HOMERUN' ? 4 : null,
new_state: {},
is_hit: payload.outcome.includes('SINGLE') || payload.outcome.includes('DOUBLE') || payload.outcome.includes('TRIPLE') || payload.outcome === 'HOMERUN',
is_out: payload.outcome.includes('OUT'),
is_walk: payload.outcome === 'WALK',
is_strikeout: payload.outcome === 'STRIKEOUT',
}
}, 1000)
}
const handlePanelDismissResult = () => {
addPanelEvent('dismissResult')
panelPlayResult.value = null
panelCanRoll.value = true
}
const simulatePanelRoll = () => {
if (!panelRollData.value && panelCanRoll.value) {
handlePanelRollDice()
}
}
const simulatePanelResult = () => {
panelPlayResult.value = {
play_number: Math.floor(Math.random() * 100) + 1,
outcome: 'SINGLE_1',
description: 'Simulated single to center field',
outs_recorded: 0,
runs_scored: 0,
runners_advanced: [{ from: 0, to: 1, out: false }],
batter_result: 1,
new_state: {},
is_hit: true,
is_out: false,
is_walk: false,
is_strikeout: false,
}
panelRollData.value = null
panelCanRoll.value = false
panelCanSubmit.value = false
}
const resetPanelState = () => {
panelIsMyTurn.value = true
panelCanRoll.value = true
panelCanSubmit.value = false
panelRollData.value = null
panelPlayResult.value = null
panelEvents.value = []
}
</script>
<style scoped>
.demo-page {
@apply min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 pb-12;
}
/* Header */
.demo-header {
@apply bg-gradient-to-r from-blue-600 to-blue-700 text-white px-6 py-12 shadow-lg;
}
.demo-title {
@apply text-4xl font-bold mb-2;
}
.demo-subtitle {
@apply text-xl text-blue-100;
}
/* Tabs */
.demo-tabs {
@apply flex gap-2 px-6 pt-6 overflow-x-auto;
}
.tab-button {
@apply px-6 py-3 rounded-t-lg font-semibold transition-all duration-200;
@apply whitespace-nowrap;
}
.tab-active {
@apply bg-white text-blue-600 shadow-md;
}
.tab-inactive {
@apply bg-blue-100 text-blue-700 hover:bg-blue-200;
}
/* Content */
.demo-content {
@apply px-6;
}
.tab-panel {
@apply bg-white rounded-b-xl rounded-tr-xl shadow-xl p-8;
}
.section {
@apply space-y-6;
}
.section-title {
@apply text-2xl font-bold text-gray-900;
}
.section-description {
@apply text-gray-600 leading-relaxed;
}
/* Controls */
.demo-controls {
@apply flex flex-wrap gap-3 p-4 bg-gray-50 rounded-lg border border-gray-200;
}
.control-button {
@apply px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg;
@apply font-medium transition-colors duration-150 shadow-sm;
}
.control-group {
@apply flex flex-wrap gap-4;
}
.control-label {
@apply flex items-center gap-2 text-sm font-medium text-gray-700;
@apply cursor-pointer;
}
.control-label input[type="checkbox"] {
@apply w-4 h-4 text-blue-600 rounded focus:ring-blue-500;
}
/* Component Showcase */
.component-showcase {
@apply p-6 bg-gradient-to-br from-gray-50 to-gray-100 rounded-xl border border-gray-200;
@apply min-h-[300px] flex items-center justify-center;
}
/* Result Box */
.result-box {
@apply mt-6 p-4 bg-green-50 border border-green-200 rounded-lg;
}
.result-title {
@apply text-lg font-semibold text-green-900 mb-2;
}
.result-code {
@apply bg-green-900 text-green-100 p-4 rounded overflow-x-auto text-sm;
}
/* Events Log */
.events-log {
@apply mt-6 p-4 bg-gray-800 text-gray-100 rounded-lg;
}
.events-title {
@apply text-lg font-semibold mb-3;
}
.events-list {
@apply space-y-3 max-h-96 overflow-y-auto;
}
.event-item {
@apply p-3 bg-gray-700 rounded space-y-2;
}
.event-time {
@apply text-xs text-gray-400;
}
.event-type {
@apply ml-3 font-semibold text-blue-400;
}
.event-data {
@apply bg-gray-900 p-2 rounded text-xs overflow-x-auto;
}
/* Mobile optimizations */
@media (max-width: 640px) {
.demo-header {
@apply px-4 py-8;
}
.demo-title {
@apply text-2xl;
}
.demo-subtitle {
@apply text-base;
}
.demo-tabs {
@apply px-4 pt-4;
}
.tab-button {
@apply px-4 py-2 text-sm;
}
.demo-content {
@apply px-4;
}
.tab-panel {
@apply p-4;
}
.section-title {
@apply text-xl;
}
.demo-controls {
@apply p-3;
}
.control-button {
@apply px-3 py-2 text-sm;
}
.component-showcase {
@apply p-4;
}
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
.demo-page {
@apply from-gray-900 to-gray-800;
}
.tab-panel {
@apply bg-gray-800;
}
.section-title {
@apply text-gray-100;
}
.section-description {
@apply text-gray-400;
}
.demo-controls {
@apply bg-gray-700 border-gray-600;
}
.control-label {
@apply text-gray-300;
}
.component-showcase {
@apply from-gray-700 to-gray-800 border-gray-600;
}
.result-box {
@apply bg-green-900 bg-opacity-20 border-green-700;
}
.result-title {
@apply text-green-400;
}
}
</style>