strat-gameplay-webapp/frontend-sba/components/Game/RunnersOnBase.vue

208 lines
6.5 KiB
Vue

<template>
<!-- TODO: Spruce up the appearance of this component - improve styling, colors, animations, and visual polish -->
<div v-if="hasRunners" class="runners-on-base-container">
<!-- Split Layout: Runners List | Catcher Card -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-0 bg-white/50 rounded-lg shadow-md overflow-hidden">
<!-- LEFT: Runners List (with expandable cards) -->
<div class="border-r border-gray-200 p-4">
<h3 class="text-xs font-semibold text-gray-500 mb-3 uppercase tracking-wide">
Runners on Base
</h3>
<div class="space-y-2">
<!-- 1st Base -->
<RunnerCard
base="1B"
:runner="runners.first"
:is-expanded="selectedRunner === 'first'"
:team-color="battingTeamColor"
@click="toggleRunner('first')"
/>
<!-- 2nd Base -->
<RunnerCard
base="2B"
:runner="runners.second"
:is-expanded="selectedRunner === 'second'"
:team-color="battingTeamColor"
@click="toggleRunner('second')"
/>
<!-- 3rd Base -->
<RunnerCard
base="3B"
:runner="runners.third"
:is-expanded="selectedRunner === 'third'"
:team-color="battingTeamColor"
@click="toggleRunner('third')"
/>
</div>
</div>
<!-- RIGHT: Catcher Card -->
<div class="p-4 bg-gray-50/30">
<h3 class="text-xs font-semibold text-gray-500 mb-3 uppercase tracking-wide">
Catcher
</h3>
<!-- Collapsed state - minimal card -->
<div
v-if="!hasSelection"
class="bg-white border-l-4 border-gray-600 rounded-lg p-3 shadow-sm transition-all duration-300"
>
<div class="flex items-center gap-3">
<div class="w-12 h-12 rounded-full overflow-hidden border-2 border-gray-600 flex-shrink-0">
<img
v-if="catcherPlayer?.headshot || catcherPlayer?.image"
:src="catcherPlayer.headshot || catcherPlayer.image"
:alt="catcherName"
class="w-full h-full object-cover"
>
<div v-else class="w-full h-full bg-gray-300 flex items-center justify-center text-gray-600 font-bold">
C
</div>
</div>
<div class="flex-1">
<div class="text-sm font-bold text-gray-900">{{ catcherName }}</div>
<div class="text-xs text-gray-600">#{{ catcherNumber }} • C</div>
</div>
</div>
<div class="mt-3 text-xs text-gray-500 text-center">
Click a runner to see matchup →
</div>
</div>
<!-- Expanded state - full card (when runner selected) -->
<div
v-else
class="matchup-card bg-gradient-to-b from-green-900 to-green-950 border-2 border-green-600 rounded-xl overflow-hidden shadow-lg fade-in"
>
<!-- Card Header -->
<div class="bg-green-800/80 px-3 py-2 flex items-center gap-2 text-white text-sm font-semibold">
<span class="font-bold text-white/90" :style="{ color: fieldingTeamColor }">
{{ fieldingTeamAbbrev }}
</span>
<span class="text-white/70">C</span>
<span class="truncate flex-1 text-right font-bold">{{ catcherName }}</span>
</div>
<!-- Card Image -->
<div class="p-0">
<img
v-if="catcherPlayer?.image"
:src="catcherPlayer.image"
:alt="`${catcherName} card`"
class="w-full h-auto"
>
<div v-else class="w-full aspect-[4/5.5] bg-gradient-to-br from-green-700 to-green-900 flex items-center justify-center">
<span class="text-5xl font-bold text-white/60">{{ getCatcherInitials }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { LineupPlayerState } from '~/types/game'
import type { Lineup } from '~/types/player'
import { useGameStore } from '~/store/game'
import RunnerCard from './RunnerCard.vue'
interface Props {
runners: {
first: LineupPlayerState | null
second: LineupPlayerState | null
third: LineupPlayerState | null
}
fieldingLineup?: Lineup[]
battingTeamColor?: string
fieldingTeamColor?: string
battingTeamAbbrev?: string
fieldingTeamAbbrev?: string
}
const props = withDefaults(defineProps<Props>(), {
fieldingLineup: () => [],
battingTeamColor: '#3b82f6',
fieldingTeamColor: '#10b981',
battingTeamAbbrev: '',
fieldingTeamAbbrev: '',
})
const gameStore = useGameStore()
const selectedRunner = ref<'first' | 'second' | 'third' | null>(null)
// Check if any runners on base
const hasRunners = computed(() => {
return !!(props.runners.first || props.runners.second || props.runners.third)
})
const hasSelection = computed(() => selectedRunner.value !== null)
// Get catcher from fielding lineup
const catcherLineup = computed(() => {
return props.fieldingLineup.find(p => p.position === 'C')
})
const catcherPlayer = computed(() => catcherLineup.value?.player ?? null)
const catcherName = computed(() => catcherPlayer.value?.name ?? 'Unknown Catcher')
const catcherNumber = computed(() => {
// Try to extract jersey number from player data if available
// For now, default to a placeholder
return '00'
})
const getCatcherInitials = computed(() => {
if (!catcherPlayer.value) return 'C'
const parts = catcherPlayer.value.name.split(' ')
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
}
return catcherPlayer.value.name.substring(0, 2).toUpperCase()
})
function toggleRunner(base: 'first' | 'second' | 'third') {
// Can't select empty base
if (!props.runners[base]) return
// Toggle selection
if (selectedRunner.value === base) {
selectedRunner.value = null
} else {
selectedRunner.value = base
}
}
</script>
<style scoped>
.runners-on-base-container {
@apply mb-6;
}
.matchup-card {
animation: pulseGlowGreen 2s ease-in-out infinite;
}
@keyframes pulseGlowGreen {
0%, 100% {
box-shadow: 0 0 15px 2px rgba(16, 185, 129, 0.5), 0 10px 25px -5px rgba(0, 0, 0, 0.3);
}
50% {
box-shadow: 0 0 30px 8px rgba(16, 185, 129, 0.7), 0 10px 25px -5px rgba(0, 0, 0, 0.3);
}
}
.fade-in {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>