- Replace .border-l-4.border-gray-600 checks with .catcher-pill - Update isExpanded prop references to isSelected - Adjust expanded view tests for new side-by-side layout - matchup-card-blue = runner full card - matchup-card = catcher full card - Fix deselection test to check isSelected prop (Transition keeps DOM during animation) All 15 RunnersOnBase tests passing All 16 RunnerCard tests passing
109 lines
3.0 KiB
Vue
109 lines
3.0 KiB
Vue
<template>
|
|
<div
|
|
:class="[
|
|
'runner-pill',
|
|
runner ? 'occupied' : 'empty',
|
|
isSelected ? 'selected' : ''
|
|
]"
|
|
@click="handleClick"
|
|
>
|
|
<template v-if="runner">
|
|
<!-- Occupied base -->
|
|
<div class="w-8 h-8 rounded-full flex-shrink-0 overflow-hidden border-2" :style="{ borderColor: teamColor }">
|
|
<img
|
|
v-if="runnerPlayer?.headshot || runnerPlayer?.image"
|
|
:src="runnerPlayer.headshot || runnerPlayer.image"
|
|
:alt="runnerName"
|
|
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 text-xs">
|
|
{{ base }}
|
|
</div>
|
|
</div>
|
|
<div class="ml-2 flex-1 min-w-0">
|
|
<div class="text-xs font-bold text-gray-900 truncate">{{ runnerName }}</div>
|
|
<div class="text-[10px] text-gray-500">{{ base }}</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<!-- Empty base -->
|
|
<div class="w-8 h-8 rounded-full bg-gray-200 border-2 border-dashed border-gray-400 flex items-center justify-center flex-shrink-0">
|
|
<span class="text-gray-400 text-[10px] font-bold">{{ base }}</span>
|
|
</div>
|
|
<div class="ml-2 text-xs text-gray-400">Empty</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { LineupPlayerState } from '~/types/game'
|
|
import { useGameStore } from '~/store/game'
|
|
|
|
interface Props {
|
|
base: '1B' | '2B' | '3B'
|
|
runner: LineupPlayerState | null
|
|
isSelected: boolean
|
|
teamColor: string
|
|
}
|
|
|
|
const props = defineProps<Props>()
|
|
const emit = defineEmits<{
|
|
click: []
|
|
}>()
|
|
|
|
const gameStore = useGameStore()
|
|
|
|
// Resolve player data from lineup
|
|
const runnerPlayer = computed(() => {
|
|
if (!props.runner) return null
|
|
const lineupEntry = gameStore.findPlayerInLineup(props.runner.lineup_id)
|
|
return lineupEntry?.player ?? null
|
|
})
|
|
|
|
const runnerName = computed(() => {
|
|
if (!runnerPlayer.value) return 'Unknown Runner'
|
|
return runnerPlayer.value.name
|
|
})
|
|
|
|
const runnerNumber = computed(() => {
|
|
// Try to extract jersey number from player data if available
|
|
// For now, default to a placeholder based on lineup_id
|
|
return props.runner?.lineup_id?.toString().padStart(2, '0') ?? '00'
|
|
})
|
|
|
|
const getRunnerInitials = computed(() => {
|
|
if (!runnerPlayer.value) return '?'
|
|
const parts = runnerPlayer.value.name.split(' ')
|
|
if (parts.length >= 2) {
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase()
|
|
}
|
|
return runnerPlayer.value.name.substring(0, 2).toUpperCase()
|
|
})
|
|
|
|
function handleClick() {
|
|
if (props.runner) {
|
|
emit('click')
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.runner-pill {
|
|
@apply flex items-center p-2 rounded-lg bg-white border border-gray-200
|
|
shadow-sm transition-all duration-200 cursor-default;
|
|
}
|
|
|
|
.runner-pill.occupied {
|
|
@apply cursor-pointer hover:bg-blue-50;
|
|
}
|
|
|
|
.runner-pill.selected {
|
|
@apply ring-2 ring-blue-500 bg-blue-50 shadow-md;
|
|
}
|
|
|
|
.runner-pill.empty {
|
|
@apply bg-gray-50 opacity-60;
|
|
}
|
|
</style>
|