Removed all references to the defensive alignment field across frontend codebase after backend removal in Session 1. The alignment field was determined to be unused and was removed from DefensiveDecision model. Changes: - types/websocket.ts: Removed alignment from DefensiveDecisionRequest interface - composables/useGameActions.ts: Removed alignment from submit handler - pages/demo-decisions.vue: Updated demo state and summary text (alignment → depths) - pages/games/[id].vue: Updated decision history text for both defensive and offensive * Defensive: Now shows "infield depth, outfield depth" instead of "alignment, infield" * Offensive: Updated to use new action field with proper labels (swing_away, hit_and_run, etc.) - Test files (3): Updated all test cases to remove alignment references * tests/unit/composables/useGameActions.spec.ts * tests/unit/store/game-decisions.spec.ts * tests/unit/components/Decisions/DefensiveSetup.spec.ts Also updated offensive decision handling to match Session 2 changes (approach/hit_and_run/bunt_attempt → action field). Total: 7 files modified, all alignment references removed Verified: Zero remaining alignment references in .ts/.vue/.js files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
438 lines
16 KiB
Vue
438 lines
16 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50 dark:from-gray-900 dark:to-gray-800 py-8">
|
||
<div class="container mx-auto px-4 max-w-6xl">
|
||
<!-- Header -->
|
||
<div class="text-center mb-8">
|
||
<h1 class="text-4xl font-bold text-gray-900 dark:text-white mb-2">
|
||
⚾ Phase F3 Decision Components Demo
|
||
</h1>
|
||
<p class="text-gray-600 dark:text-gray-400">
|
||
Interactive preview of all decision input components
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Demo Controls -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 mb-8">
|
||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Demo Controls</h2>
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Active State
|
||
</label>
|
||
<ToggleSwitch
|
||
v-model="demoControls.isActive"
|
||
label="Components Active"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Runners on Base
|
||
</label>
|
||
<ToggleSwitch
|
||
v-model="demoControls.hasRunners"
|
||
label="Runners Present"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Decision Phase
|
||
</label>
|
||
<select
|
||
v-model="demoControls.phase"
|
||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||
>
|
||
<option value="idle">Idle</option>
|
||
<option value="defensive">Defensive</option>
|
||
<option value="offensive">Offensive</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Navigation -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg mb-8 overflow-hidden">
|
||
<div class="flex border-b border-gray-200 dark:border-gray-700">
|
||
<button
|
||
v-for="tab in tabs"
|
||
:key="tab.id"
|
||
@click="activeTab = tab.id"
|
||
:class="[
|
||
'flex-1 px-6 py-4 text-sm font-medium transition-colors',
|
||
activeTab === tab.id
|
||
? 'bg-primary text-white border-b-2 border-primary'
|
||
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50 dark:hover:bg-gray-700'
|
||
]"
|
||
>
|
||
{{ tab.icon }} {{ tab.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tab Content -->
|
||
<div class="space-y-8">
|
||
<!-- UI Components Tab -->
|
||
<div v-if="activeTab === 'ui'">
|
||
<div class="space-y-8">
|
||
<!-- ActionButton Demo -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">ActionButton</h3>
|
||
<div class="space-y-6">
|
||
<!-- Variants -->
|
||
<div>
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Variants</h4>
|
||
<div class="flex flex-wrap gap-3">
|
||
<ActionButton variant="primary" @click="showToast('Primary clicked!')">
|
||
Primary
|
||
</ActionButton>
|
||
<ActionButton variant="secondary" @click="showToast('Secondary clicked!')">
|
||
Secondary
|
||
</ActionButton>
|
||
<ActionButton variant="success" @click="showToast('Success clicked!')">
|
||
Success
|
||
</ActionButton>
|
||
<ActionButton variant="danger" @click="showToast('Danger clicked!')">
|
||
Danger
|
||
</ActionButton>
|
||
<ActionButton variant="warning" @click="showToast('Warning clicked!')">
|
||
Warning
|
||
</ActionButton>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sizes -->
|
||
<div>
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Sizes</h4>
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<ActionButton size="sm">Small</ActionButton>
|
||
<ActionButton size="md">Medium</ActionButton>
|
||
<ActionButton size="lg">Large</ActionButton>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- States -->
|
||
<div>
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">States</h4>
|
||
<div class="flex flex-wrap gap-3">
|
||
<ActionButton :loading="true">Loading</ActionButton>
|
||
<ActionButton :disabled="true">Disabled</ActionButton>
|
||
<ActionButton full-width>Full Width</ActionButton>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ButtonGroup Demo -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">ButtonGroup</h3>
|
||
<div class="space-y-6">
|
||
<!-- Horizontal -->
|
||
<div>
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Horizontal</h4>
|
||
<ButtonGroup
|
||
v-model="demoState.selectedOption"
|
||
:options="[
|
||
{ value: 'opt1', label: 'Option 1', icon: '🎯' },
|
||
{ value: 'opt2', label: 'Option 2', icon: '⚡' },
|
||
{ value: 'opt3', label: 'Option 3', icon: '🚀' },
|
||
]"
|
||
/>
|
||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||
Selected: {{ demoState.selectedOption }}
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Vertical -->
|
||
<div>
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Vertical</h4>
|
||
<ButtonGroup
|
||
v-model="demoState.selectedVertical"
|
||
:options="[
|
||
{ value: 'a', label: 'Option A' },
|
||
{ value: 'b', label: 'Option B' },
|
||
{ value: 'c', label: 'Option C' },
|
||
]"
|
||
vertical
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ToggleSwitch Demo -->
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-4">ToggleSwitch</h3>
|
||
<div class="space-y-4">
|
||
<ToggleSwitch
|
||
v-model="demoState.toggle1"
|
||
label="Enable feature 1"
|
||
size="sm"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="demoState.toggle2"
|
||
label="Enable feature 2 (medium)"
|
||
size="md"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="demoState.toggle3"
|
||
label="Enable feature 3 (large)"
|
||
size="lg"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="demoState.toggle4"
|
||
label="Disabled toggle"
|
||
:disabled="true"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Defensive Setup Tab -->
|
||
<div v-if="activeTab === 'defensive'">
|
||
<DefensiveSetup
|
||
game-id="demo-game-123"
|
||
:is-active="demoControls.isActive"
|
||
:current-setup="demoState.defensiveSetup"
|
||
@submit="handleDefensiveSubmit"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Stolen Base Tab -->
|
||
<div v-if="activeTab === 'steals'">
|
||
<StolenBaseInputs
|
||
:runners="demoRunners"
|
||
:is-active="demoControls.isActive"
|
||
:current-attempts="demoState.stealAttempts"
|
||
@submit="handleStealSubmit"
|
||
@cancel="handleStealCancel"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Offensive Approach Tab -->
|
||
<div v-if="activeTab === 'offensive'">
|
||
<OffensiveApproach
|
||
game-id="demo-game-123"
|
||
:is-active="demoControls.isActive"
|
||
:current-decision="demoState.offensiveDecision"
|
||
:has-runners-on-base="demoControls.hasRunners"
|
||
:runner-on-first="!!demoRunners.first"
|
||
:runner-on-second="!!demoRunners.second"
|
||
:runner-on-third="!!demoRunners.third"
|
||
:outs="0"
|
||
@submit="handleOffensiveSubmit"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Decision Panel Tab -->
|
||
<div v-if="activeTab === 'panel'">
|
||
<DecisionPanel
|
||
game-id="demo-game-123"
|
||
current-team="home"
|
||
:is-my-turn="demoControls.isActive"
|
||
:phase="demoControls.phase"
|
||
:runners="demoRunners"
|
||
:current-defensive-setup="demoState.defensiveSetup"
|
||
:current-offensive-decision="demoState.offensiveDecision"
|
||
:current-steal-attempts="demoState.stealAttempts"
|
||
:decision-history="demoState.decisionHistory"
|
||
@defensive-submit="handleDefensiveSubmit"
|
||
@offensive-submit="handleOffensiveSubmit"
|
||
@steal-attempts-submit="handleStealSubmit"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast Notification -->
|
||
<Transition
|
||
enter-active-class="transition ease-out duration-300"
|
||
enter-from-class="opacity-0 translate-y-4"
|
||
enter-to-class="opacity-100 translate-y-0"
|
||
leave-active-class="transition ease-in duration-200"
|
||
leave-from-class="opacity-100 translate-y-0"
|
||
leave-to-class="opacity-0 translate-y-4"
|
||
>
|
||
<div
|
||
v-if="toastMessage"
|
||
class="fixed bottom-8 right-8 bg-green-600 text-white px-6 py-4 rounded-lg shadow-2xl z-50 max-w-md"
|
||
>
|
||
<div class="flex items-center gap-3">
|
||
<span class="text-2xl">✅</span>
|
||
<div>
|
||
<p class="font-semibold">{{ toastMessage }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
|
||
<!-- 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-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>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import type { DefensiveDecision, OffensiveDecision } from '~/types/game'
|
||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||
import ButtonGroup from '~/components/UI/ButtonGroup.vue'
|
||
import ToggleSwitch from '~/components/UI/ToggleSwitch.vue'
|
||
import DefensiveSetup from '~/components/Decisions/DefensiveSetup.vue'
|
||
import StolenBaseInputs from '~/components/Decisions/StolenBaseInputs.vue'
|
||
import OffensiveApproach from '~/components/Decisions/OffensiveApproach.vue'
|
||
import DecisionPanel from '~/components/Decisions/DecisionPanel.vue'
|
||
|
||
// Tab navigation
|
||
const activeTab = ref('ui')
|
||
const tabs = [
|
||
{ id: 'ui', label: 'UI Components', icon: '🎨' },
|
||
{ id: 'defensive', label: 'Defensive Setup', icon: '🛡️' },
|
||
{ id: 'steals', label: 'Stolen Bases', icon: '🏃' },
|
||
{ id: 'offensive', label: 'Offensive Approach', icon: '⚔️' },
|
||
{ id: 'panel', label: 'Decision Panel', icon: '🎯' },
|
||
]
|
||
|
||
// Demo controls
|
||
const demoControls = ref({
|
||
isActive: true,
|
||
hasRunners: true,
|
||
phase: 'offensive' as 'idle' | 'defensive' | 'offensive',
|
||
})
|
||
|
||
// Demo state
|
||
const demoState = ref({
|
||
selectedOption: 'opt1',
|
||
selectedVertical: 'a',
|
||
toggle1: false,
|
||
toggle2: true,
|
||
toggle3: false,
|
||
toggle4: true,
|
||
defensiveSetup: {
|
||
infield_depth: 'normal',
|
||
outfield_depth: 'normal',
|
||
hold_runners: [],
|
||
} as DefensiveDecision,
|
||
offensiveDecision: {
|
||
action: 'swing_away',
|
||
} as Omit<OffensiveDecision, 'steal_attempts'>,
|
||
stealAttempts: [] as number[],
|
||
decisionHistory: [
|
||
{
|
||
type: 'Defensive' as const,
|
||
summary: 'normal infield, normal outfield',
|
||
timestamp: '10:45:23',
|
||
},
|
||
{
|
||
type: 'Offensive' as const,
|
||
summary: 'Swing Away',
|
||
timestamp: '10:45:18',
|
||
},
|
||
],
|
||
})
|
||
|
||
// Demo runners
|
||
const demoRunners = computed(() => {
|
||
if (!demoControls.value.hasRunners) {
|
||
return { first: null, second: null, third: null }
|
||
}
|
||
return {
|
||
first: 101,
|
||
second: 102,
|
||
third: 103,
|
||
}
|
||
})
|
||
|
||
// Toast notification
|
||
const toastMessage = ref('')
|
||
const showToast = (message: string) => {
|
||
toastMessage.value = message
|
||
setTimeout(() => {
|
||
toastMessage.value = ''
|
||
}, 3000)
|
||
}
|
||
|
||
// Event handlers
|
||
const handleDefensiveSubmit = (decision: DefensiveDecision) => {
|
||
demoState.value.defensiveSetup = decision
|
||
const summary = `${decision.infield_depth} infield, ${decision.outfield_depth} outfield`
|
||
demoState.value.decisionHistory.unshift({
|
||
type: 'Defensive',
|
||
summary,
|
||
timestamp: new Date().toLocaleTimeString(),
|
||
})
|
||
showToast(`Defensive setup submitted: ${summary}`)
|
||
}
|
||
|
||
const handleOffensiveSubmit = (decision: Omit<OffensiveDecision, 'steal_attempts'>) => {
|
||
demoState.value.offensiveDecision = decision
|
||
const actionLabels: Record<string, string> = {
|
||
swing_away: 'Swing Away',
|
||
steal: 'Steal',
|
||
check_jump: 'Check Jump',
|
||
hit_and_run: 'Hit and Run',
|
||
sac_bunt: 'Sacrifice Bunt',
|
||
squeeze_bunt: 'Squeeze Bunt',
|
||
}
|
||
const summary = actionLabels[decision.action] || decision.action
|
||
demoState.value.decisionHistory.unshift({
|
||
type: 'Offensive',
|
||
summary,
|
||
timestamp: new Date().toLocaleTimeString(),
|
||
})
|
||
showToast(`Offensive action selected: ${summary}`)
|
||
}
|
||
|
||
const handleStealSubmit = (attempts: number[]) => {
|
||
demoState.value.stealAttempts = attempts
|
||
const bases = attempts.map(a => {
|
||
if (a === 2) return '2nd'
|
||
if (a === 3) return '3rd'
|
||
if (a === 4) return 'Home'
|
||
return a
|
||
}).join(', ')
|
||
showToast(`Steal attempts: ${bases || 'None'}`)
|
||
}
|
||
|
||
const handleStealCancel = () => {
|
||
showToast('Steal attempts canceled')
|
||
}
|
||
</script>
|