Updated outcomesNeedingHitLocation array to only include outcomes that trigger defensive plays or advancement decisions. Before (11 outcomes): - GROUNDOUT, FLYOUT, LINEOUT ✅ - SINGLE_1, SINGLE_2, SINGLE_UNCAPPED ❌ (too broad) - DOUBLE_2, DOUBLE_3, DOUBLE_UNCAPPED ❌ (too broad) - TRIPLE ❌ (too broad) - ERROR ✅ After (6 outcomes): - GROUNDOUT (all groundouts) - FLYOUT (all flyouts) - LINEOUT (all lineouts) - SINGLE_UNCAPPED (decision tree hits) - DOUBLE_UNCAPPED (decision tree hits) - ERROR (defensive plays) Rationale: Standard hits (SINGLE_1, SINGLE_2, DOUBLE_2, etc.) have fixed runner advancement and don't need hit location selection. Session 1 Part 2 - Change #5 complete Part of cleanup work from demo review 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
375 lines
9.2 KiB
Vue
375 lines
9.2 KiB
Vue
<template>
|
|
<div class="manual-outcome-entry">
|
|
<div class="entry-container">
|
|
<!-- Header -->
|
|
<div class="entry-header">
|
|
<h3 class="text-lg font-bold text-gray-900">Select Outcome</h3>
|
|
<div class="text-sm text-gray-600">
|
|
Read your player card and select the result
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Outcome Selection -->
|
|
<div class="outcome-section">
|
|
<label class="form-label">Outcome Type</label>
|
|
|
|
<!-- Outcome Categories -->
|
|
<div class="outcome-categories">
|
|
<div
|
|
v-for="category in outcomeCategories"
|
|
:key="category.name"
|
|
class="outcome-category"
|
|
>
|
|
<div class="category-header">{{ category.name }}</div>
|
|
<div class="category-buttons">
|
|
<button
|
|
v-for="outcome in category.outcomes"
|
|
:key="outcome"
|
|
:class="[
|
|
'outcome-button',
|
|
selectedOutcome === outcome ? 'outcome-button-selected' : 'outcome-button-default'
|
|
]"
|
|
@click="selectOutcome(outcome)"
|
|
>
|
|
{{ formatOutcome(outcome) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hit Location (conditional) -->
|
|
<div v-if="needsHitLocation" class="hit-location-section">
|
|
<label class="form-label">Hit Location</label>
|
|
<div class="location-grid">
|
|
<div class="location-group">
|
|
<div class="location-group-label">Infield</div>
|
|
<div class="location-buttons">
|
|
<button
|
|
v-for="position in infieldPositions"
|
|
:key="position"
|
|
:class="[
|
|
'location-button',
|
|
selectedHitLocation === position ? 'location-button-selected' : 'location-button-default'
|
|
]"
|
|
@click="selectHitLocation(position)"
|
|
>
|
|
{{ position }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="location-group">
|
|
<div class="location-group-label">Outfield</div>
|
|
<div class="location-buttons">
|
|
<button
|
|
v-for="position in outfieldPositions"
|
|
:key="position"
|
|
:class="[
|
|
'location-button',
|
|
selectedHitLocation === position ? 'location-button-selected' : 'location-button-default'
|
|
]"
|
|
@click="selectHitLocation(position)"
|
|
>
|
|
{{ position }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="action-buttons">
|
|
<button
|
|
class="button button-cancel"
|
|
@click="handleCancel"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
:disabled="!canSubmitForm"
|
|
:class="[
|
|
'button',
|
|
canSubmitForm ? 'button-submit' : 'button-submit-disabled'
|
|
]"
|
|
@click="handleSubmit"
|
|
>
|
|
Submit Outcome
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import type { PlayOutcome, RollData } from '~/types'
|
|
|
|
interface Props {
|
|
rollData: RollData | null
|
|
canSubmit: boolean
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
|
|
const emit = defineEmits<{
|
|
submit: [{ outcome: PlayOutcome; hitLocation?: string }]
|
|
cancel: []
|
|
}>()
|
|
|
|
// Local state
|
|
const selectedOutcome = ref<PlayOutcome | null>(null)
|
|
const selectedHitLocation = ref<string | null>(null)
|
|
|
|
// Outcome categories
|
|
const outcomeCategories = [
|
|
{
|
|
name: 'Outs',
|
|
outcomes: ['STRIKEOUT', 'GROUNDOUT', 'FLYOUT', 'LINEOUT', 'POPOUT', 'DOUBLE_PLAY'] as PlayOutcome[],
|
|
},
|
|
{
|
|
name: 'Hits',
|
|
outcomes: ['SINGLE_1', 'SINGLE_2', 'SINGLE_UNCAPPED', 'DOUBLE_2', 'DOUBLE_3', 'DOUBLE_UNCAPPED', 'TRIPLE', 'HOMERUN'] as PlayOutcome[],
|
|
},
|
|
{
|
|
name: 'Walks / HBP',
|
|
outcomes: ['WALK', 'INTENTIONAL_WALK', 'HIT_BY_PITCH'] as PlayOutcome[],
|
|
},
|
|
{
|
|
name: 'Special',
|
|
outcomes: ['ERROR'] as PlayOutcome[],
|
|
},
|
|
{
|
|
name: 'Interrupts',
|
|
outcomes: ['STOLEN_BASE', 'CAUGHT_STEALING', 'WILD_PITCH', 'PASSED_BALL', 'BALK', 'PICK_OFF'] as PlayOutcome[],
|
|
},
|
|
]
|
|
|
|
// Hit location options
|
|
const infieldPositions = ['P', 'C', '1B', '2B', '3B', 'SS']
|
|
const outfieldPositions = ['LF', 'CF', 'RF']
|
|
|
|
// Outcomes that require hit location
|
|
// Only outcomes that affect defensive plays require location
|
|
const outcomesNeedingHitLocation = [
|
|
'GROUNDOUT', // All groundouts
|
|
'FLYOUT', // All flyouts
|
|
'LINEOUT', // All lineouts
|
|
'SINGLE_UNCAPPED', // Decision tree hits
|
|
'DOUBLE_UNCAPPED', // Decision tree hits
|
|
'ERROR', // Defensive plays
|
|
]
|
|
|
|
// Computed
|
|
const needsHitLocation = computed(() => {
|
|
return selectedOutcome.value !== null && outcomesNeedingHitLocation.includes(selectedOutcome.value)
|
|
})
|
|
|
|
const canSubmitForm = computed(() => {
|
|
if (!selectedOutcome.value) return false
|
|
if (needsHitLocation.value && !selectedHitLocation.value) return false
|
|
return props.canSubmit && props.rollData !== null
|
|
})
|
|
|
|
// Methods
|
|
const selectOutcome = (outcome: PlayOutcome) => {
|
|
selectedOutcome.value = outcome
|
|
// Clear hit location if the new outcome doesn't need it
|
|
if (!outcomesNeedingHitLocation.includes(outcome)) {
|
|
selectedHitLocation.value = null
|
|
}
|
|
}
|
|
|
|
const selectHitLocation = (location: string) => {
|
|
selectedHitLocation.value = location
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
if (!canSubmitForm.value || !selectedOutcome.value) return
|
|
|
|
emit('submit', {
|
|
outcome: selectedOutcome.value,
|
|
hitLocation: selectedHitLocation.value || undefined,
|
|
})
|
|
|
|
// Reset form
|
|
selectedOutcome.value = null
|
|
selectedHitLocation.value = null
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
selectedOutcome.value = null
|
|
selectedHitLocation.value = null
|
|
emit('cancel')
|
|
}
|
|
|
|
const formatOutcome = (outcome: string): string => {
|
|
// Convert outcome to readable format
|
|
return outcome
|
|
.split('_')
|
|
.map(word => word.charAt(0) + word.slice(1).toLowerCase())
|
|
.join(' ')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.manual-outcome-entry {
|
|
@apply w-full;
|
|
}
|
|
|
|
.entry-container {
|
|
@apply bg-white rounded-xl shadow-lg p-6 space-y-6;
|
|
}
|
|
|
|
/* Header */
|
|
.entry-header {
|
|
@apply pb-4 border-b border-gray-200;
|
|
}
|
|
|
|
/* Form Sections */
|
|
.outcome-section,
|
|
.hit-location-section {
|
|
@apply space-y-3;
|
|
}
|
|
|
|
.form-label {
|
|
@apply block text-sm font-semibold text-gray-700 uppercase tracking-wide mb-2;
|
|
}
|
|
|
|
/* Outcome Categories */
|
|
.outcome-categories {
|
|
@apply space-y-4;
|
|
}
|
|
|
|
.outcome-category {
|
|
@apply space-y-2;
|
|
}
|
|
|
|
.category-header {
|
|
@apply text-xs font-bold text-gray-600 uppercase tracking-wider;
|
|
}
|
|
|
|
.category-buttons {
|
|
@apply flex flex-wrap gap-2;
|
|
}
|
|
|
|
/* Outcome Buttons */
|
|
.outcome-button {
|
|
@apply px-4 py-2 rounded-lg text-sm font-medium transition-all duration-150;
|
|
@apply border-2 min-h-[44px];
|
|
}
|
|
|
|
.outcome-button-default {
|
|
@apply bg-white border-gray-300 text-gray-700;
|
|
@apply hover:border-blue-400 hover:bg-blue-50;
|
|
}
|
|
|
|
.outcome-button-selected {
|
|
@apply bg-gradient-to-r from-blue-500 to-blue-600 border-blue-600 text-white;
|
|
@apply shadow-md;
|
|
}
|
|
|
|
/* Hit Location Grid */
|
|
.location-grid {
|
|
@apply grid grid-cols-1 gap-4;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.location-grid {
|
|
@apply grid-cols-2;
|
|
}
|
|
}
|
|
|
|
.location-group {
|
|
@apply space-y-2;
|
|
}
|
|
|
|
.location-group-label {
|
|
@apply text-xs font-bold text-gray-600 uppercase tracking-wider;
|
|
}
|
|
|
|
.location-buttons {
|
|
@apply grid grid-cols-3 gap-2;
|
|
}
|
|
|
|
/* Location Buttons */
|
|
.location-button {
|
|
@apply px-3 py-3 rounded-lg text-sm font-bold transition-all duration-150;
|
|
@apply border-2 min-h-[44px];
|
|
}
|
|
|
|
.location-button-default {
|
|
@apply bg-white border-gray-300 text-gray-700;
|
|
@apply hover:border-green-400 hover:bg-green-50;
|
|
}
|
|
|
|
.location-button-selected {
|
|
@apply bg-gradient-to-r from-green-500 to-green-600 border-green-600 text-white;
|
|
@apply shadow-md;
|
|
}
|
|
|
|
/* Action Buttons */
|
|
.action-buttons {
|
|
@apply flex gap-3 pt-4 border-t border-gray-200;
|
|
}
|
|
|
|
.button {
|
|
@apply flex-1 px-6 py-3 rounded-lg font-bold text-base transition-all duration-200;
|
|
@apply shadow-md min-h-[52px];
|
|
}
|
|
|
|
.button-cancel {
|
|
@apply bg-white border-2 border-gray-300 text-gray-700;
|
|
@apply hover:border-gray-400 hover:bg-gray-50;
|
|
}
|
|
|
|
.button-submit {
|
|
@apply bg-gradient-to-r from-blue-500 to-blue-600 text-white;
|
|
@apply hover:from-blue-600 hover:to-blue-700 hover:shadow-lg;
|
|
@apply active:scale-95;
|
|
}
|
|
|
|
.button-submit-disabled {
|
|
@apply bg-gray-300 text-gray-500 cursor-not-allowed;
|
|
}
|
|
|
|
/* Dark mode support */
|
|
@media (prefers-color-scheme: dark) {
|
|
.entry-container {
|
|
@apply bg-gray-800 border border-gray-700;
|
|
}
|
|
|
|
.entry-header {
|
|
@apply border-gray-700;
|
|
}
|
|
|
|
.entry-header h3,
|
|
.form-label {
|
|
@apply text-gray-100;
|
|
}
|
|
|
|
.entry-header .text-sm {
|
|
@apply text-gray-400;
|
|
}
|
|
|
|
.category-header,
|
|
.location-group-label {
|
|
@apply text-gray-400;
|
|
}
|
|
|
|
.outcome-button-default,
|
|
.location-button-default {
|
|
@apply bg-gray-700 border-gray-600 text-gray-200;
|
|
@apply hover:border-blue-400 hover:bg-gray-600;
|
|
}
|
|
|
|
.button-cancel {
|
|
@apply bg-gray-700 border-gray-600 text-gray-200;
|
|
@apply hover:border-gray-500 hover:bg-gray-600;
|
|
}
|
|
|
|
.action-buttons {
|
|
@apply border-gray-700;
|
|
}
|
|
}
|
|
</style>
|