254 lines
7.8 KiB
Vue
254 lines
7.8 KiB
Vue
<template>
|
||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
||
<!-- Header -->
|
||
<div class="flex items-center justify-between mb-6">
|
||
<h3 class="text-xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||
<span class="text-2xl">🛡️</span>
|
||
Defensive Setup
|
||
</h3>
|
||
<span
|
||
v-if="!isActive"
|
||
class="px-3 py-1 text-xs font-semibold rounded-full bg-gray-200 text-gray-600"
|
||
>
|
||
Opponent's Turn
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Form -->
|
||
<form class="space-y-6" @submit.prevent="handleSubmit">
|
||
<!-- Infield Depth -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Infield Depth
|
||
</label>
|
||
<ButtonGroup
|
||
v-model="localSetup.infield_depth"
|
||
:options="infieldDepthOptions"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
variant="primary"
|
||
vertical
|
||
/>
|
||
</div>
|
||
|
||
<!-- Outfield Depth -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Outfield Depth
|
||
</label>
|
||
<ButtonGroup
|
||
v-model="localSetup.outfield_depth"
|
||
:options="outfieldDepthOptions"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
variant="primary"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Hold Runners -->
|
||
<div>
|
||
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">
|
||
Hold Runners
|
||
</label>
|
||
<div class="space-y-2">
|
||
<ToggleSwitch
|
||
v-model="holdFirst"
|
||
label="Hold runner at 1st base"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="holdSecond"
|
||
label="Hold runner at 2nd base"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
/>
|
||
<ToggleSwitch
|
||
v-model="holdThird"
|
||
label="Hold runner at 3rd base"
|
||
:disabled="!isActive"
|
||
size="md"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Visual Preview -->
|
||
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-gray-700 dark:to-gray-600 rounded-lg p-4">
|
||
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||
Current Setup
|
||
</h4>
|
||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Infield:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ infieldDisplay }}</span>
|
||
</div>
|
||
<div>
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Outfield:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ outfieldDisplay }}</span>
|
||
</div>
|
||
<div class="col-span-2">
|
||
<span class="font-medium text-gray-600 dark:text-gray-400">Holding:</span>
|
||
<span class="ml-1 text-gray-900 dark:text-white">{{ holdingDisplay }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Submit Button -->
|
||
<ActionButton
|
||
type="submit"
|
||
variant="success"
|
||
size="lg"
|
||
:disabled="!isActive"
|
||
:loading="submitting"
|
||
full-width
|
||
>
|
||
{{ submitButtonText }}
|
||
</ActionButton>
|
||
</form>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch } from 'vue'
|
||
import type { DefensiveDecision, GameState } from '~/types/game'
|
||
import ButtonGroup from '~/components/UI/ButtonGroup.vue'
|
||
import type { ButtonGroupOption } from '~/components/UI/ButtonGroup.vue'
|
||
import ToggleSwitch from '~/components/UI/ToggleSwitch.vue'
|
||
import ActionButton from '~/components/UI/ActionButton.vue'
|
||
|
||
interface Props {
|
||
gameId: string
|
||
isActive: boolean
|
||
currentSetup?: DefensiveDecision
|
||
gameState?: GameState // Added for smart filtering
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
isActive: false,
|
||
})
|
||
|
||
const emit = defineEmits<{
|
||
submit: [setup: DefensiveDecision]
|
||
}>()
|
||
|
||
// Local state
|
||
const submitting = ref(false)
|
||
const localSetup = ref<DefensiveDecision>({
|
||
infield_depth: props.currentSetup?.infield_depth || 'normal',
|
||
outfield_depth: props.currentSetup?.outfield_depth || 'normal',
|
||
hold_runners: props.currentSetup?.hold_runners || [],
|
||
})
|
||
|
||
// Hold runner toggles
|
||
const holdFirst = ref(localSetup.value.hold_runners.includes(1))
|
||
const holdSecond = ref(localSetup.value.hold_runners.includes(2))
|
||
const holdThird = ref(localSetup.value.hold_runners.includes(3))
|
||
|
||
// Watch hold toggles and update hold_runners array
|
||
watch([holdFirst, holdSecond, holdThird], () => {
|
||
const runners: number[] = []
|
||
if (holdFirst.value) runners.push(1)
|
||
if (holdSecond.value) runners.push(2)
|
||
if (holdThird.value) runners.push(3)
|
||
localSetup.value.hold_runners = runners
|
||
})
|
||
|
||
// Dynamic options based on game state
|
||
const infieldDepthOptions = computed<ButtonGroupOption[]>(() => {
|
||
const options: ButtonGroupOption[] = [
|
||
{ value: 'normal', label: 'Normal', icon: '•' },
|
||
]
|
||
|
||
// Only show infield_in and corners_in if runner on third
|
||
if (props.gameState?.on_third) {
|
||
options.push({ value: 'infield_in', label: 'Infield In', icon: '⬆️' })
|
||
options.push({ value: 'corners_in', label: 'Corners In', icon: '◀️▶️' })
|
||
}
|
||
|
||
return options
|
||
})
|
||
|
||
const outfieldDepthOptions = computed<ButtonGroupOption[]>(() => {
|
||
const options: ButtonGroupOption[] = [
|
||
{ value: 'normal', label: 'Normal', icon: '•' },
|
||
]
|
||
|
||
// Check for walk-off scenario
|
||
if (props.gameState) {
|
||
const { inning, half, home_score, away_score, on_first, on_second, on_third } = props.gameState
|
||
const isHomeBatting = half === 'bottom'
|
||
const isLateInning = inning >= 9
|
||
const isCloseGame = isHomeBatting
|
||
? home_score <= away_score
|
||
: away_score <= home_score
|
||
const hasRunners = on_first || on_second || on_third
|
||
|
||
// Only show shallow in walk-off situations
|
||
if (isHomeBatting && isLateInning && isCloseGame && hasRunners) {
|
||
options.push({ value: 'shallow', label: 'Shallow', icon: '⬇️' })
|
||
}
|
||
}
|
||
|
||
return options
|
||
})
|
||
|
||
// Display helpers
|
||
const infieldDisplay = computed(() => {
|
||
const option = infieldDepthOptions.value.find(opt => opt.value === localSetup.value.infield_depth)
|
||
return option?.label || 'Normal'
|
||
})
|
||
|
||
const outfieldDisplay = computed(() => {
|
||
const option = outfieldDepthOptions.value.find(opt => opt.value === localSetup.value.outfield_depth)
|
||
return option?.label || 'Normal'
|
||
})
|
||
|
||
const holdingDisplay = computed(() => {
|
||
if (localSetup.value.hold_runners.length === 0) return 'None'
|
||
return localSetup.value.hold_runners.map(base => {
|
||
if (base === 1) return '1st'
|
||
if (base === 2) return '2nd'
|
||
if (base === 3) return '3rd'
|
||
return base
|
||
}).join(', ')
|
||
})
|
||
|
||
// Check if setup has changed from initial (for display only)
|
||
const hasChanges = computed(() => {
|
||
if (!props.currentSetup) return true
|
||
return (
|
||
localSetup.value.infield_depth !== props.currentSetup.infield_depth ||
|
||
localSetup.value.outfield_depth !== props.currentSetup.outfield_depth ||
|
||
JSON.stringify(localSetup.value.hold_runners) !== JSON.stringify(props.currentSetup.hold_runners)
|
||
)
|
||
})
|
||
|
||
const submitButtonText = computed(() => {
|
||
if (!props.isActive) return 'Wait for Your Turn'
|
||
if (!hasChanges.value) return 'Submit (Keep Setup)'
|
||
return 'Submit Defensive Setup'
|
||
})
|
||
|
||
// Handle form submission
|
||
const handleSubmit = async () => {
|
||
if (!props.isActive) return
|
||
|
||
submitting.value = true
|
||
try {
|
||
emit('submit', { ...localSetup.value })
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
// Watch for prop changes and update local state
|
||
watch(() => props.currentSetup, (newSetup) => {
|
||
if (newSetup) {
|
||
localSetup.value = { ...newSetup }
|
||
holdFirst.value = newSetup.hold_runners.includes(1)
|
||
holdSecond.value = newSetup.hold_runners.includes(2)
|
||
holdThird.value = newSetup.hold_runners.includes(3)
|
||
}
|
||
}, { deep: true })
|
||
</script>
|