CLAUDE: Fix lineup builder bugs and improve player image display

Bug fixes:
- Fix pitcher filter to recognize SP, RP, CP positions (not just P)
- Fix validation to allow 9 players when pitcher bats (no DH games)
- Simplify position filters to All/Batters/Pitchers

Image display improvements:
- Use headshot > vanity_card priority (avoid card images in circles)
- Show player initials as fallback (e.g., "AV" for Alex Verdugo)
- Handle name suffixes (Jr, Sr, II, III, IV) correctly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-01-15 13:58:02 -06:00
parent d65ae19f5e
commit 2dd2b530f8

View File

@ -20,7 +20,7 @@ const activeTab = ref<TeamTab>('away') // Away bats first
// Search and filter state // Search and filter state
const searchQuery = ref('') const searchQuery = ref('')
type PositionFilter = 'all' | 'catchers' | 'infielders' | 'outfielders' | 'pitchers' type PositionFilter = 'all' | 'batters' | 'pitchers'
const positionFilter = ref<PositionFilter>('all') const positionFilter = ref<PositionFilter>('all')
// Player preview modal // Player preview modal
@ -67,10 +67,13 @@ const availableAwayRoster = computed(() => {
const currentLineup = computed(() => activeTab.value === 'home' ? homeLineup.value : awayLineup.value) const currentLineup = computed(() => activeTab.value === 'home' ? homeLineup.value : awayLineup.value)
const currentRoster = computed(() => activeTab.value === 'home' ? availableHomeRoster.value : availableAwayRoster.value) const currentRoster = computed(() => activeTab.value === 'home' ? availableHomeRoster.value : availableAwayRoster.value)
// Slot 10 (pitcher) should be disabled if P is selected in batting order // Position category helpers (defined early for validation)
const PITCHER_POSITIONS = ['P', 'SP', 'RP', 'CP']
// Slot 10 (pitcher) should be disabled if a pitcher is in batting order
const pitcherSlotDisabled = computed(() => { const pitcherSlotDisabled = computed(() => {
const lineup = currentLineup.value const lineup = currentLineup.value
return lineup.slice(0, 9).some(slot => slot.position === 'P') return lineup.slice(0, 9).some(slot => slot.position && PITCHER_POSITIONS.includes(slot.position))
}) })
// Validation // Validation
@ -102,8 +105,8 @@ const validationErrors = computed(() => {
function validateLineup(lineup: LineupSlot[], teamName: string): string[] { function validateLineup(lineup: LineupSlot[], teamName: string): string[] {
const errors: string[] = [] const errors: string[] = []
// Check if P is in batting order // Check if a pitcher is in batting order (no DH game)
const pitcherInBattingOrder = lineup.slice(0, 9).some(s => s.position === 'P') const pitcherInBattingOrder = lineup.slice(0, 9).some(s => s.position && PITCHER_POSITIONS.includes(s.position))
const requiredSlots = pitcherInBattingOrder ? 9 : 10 const requiredSlots = pitcherInBattingOrder ? 9 : 10
// Check if all slots filled // Check if all slots filled
@ -207,26 +210,17 @@ function clearLineup() {
}) })
} }
// Position category helpers
const CATCHER_POSITIONS = ['C']
const INFIELD_POSITIONS = ['1B', '2B', '3B', 'SS']
const OUTFIELD_POSITIONS = ['LF', 'CF', 'RF']
const PITCHER_POSITIONS = ['P']
function playerHasPositionInCategory(player: SbaPlayer, category: PositionFilter): boolean { function playerHasPositionInCategory(player: SbaPlayer, category: PositionFilter): boolean {
if (category === 'all') return true if (category === 'all') return true
const positions = getPlayerPositions(player) const positions = getPlayerPositions(player)
const isPitcher = positions.some(p => PITCHER_POSITIONS.includes(p))
switch (category) { switch (category) {
case 'catchers':
return positions.some(p => CATCHER_POSITIONS.includes(p))
case 'infielders':
return positions.some(p => INFIELD_POSITIONS.includes(p))
case 'outfielders':
return positions.some(p => OUTFIELD_POSITIONS.includes(p))
case 'pitchers': case 'pitchers':
return positions.some(p => PITCHER_POSITIONS.includes(p)) return isPitcher
case 'batters':
return !isPitcher
default: default:
return true return true
} }
@ -269,6 +263,24 @@ function closePlayerPreview() {
previewPlayer.value = null previewPlayer.value = null
} }
// Get player preview image with fallback priority: headshot > vanity_card > null
function getPlayerPreviewImage(player: SbaPlayer): string | null {
return player.headshot || player.vanity_card || null
}
// Get player avatar fallback - use first + last initials (e.g., "Alex Verdugo" -> "AV")
// Ignores common suffixes like Jr, Sr, II, III, IV
function getPlayerFallbackInitial(player: SbaPlayer): string {
const suffixes = ['jr', 'jr.', 'sr', 'sr.', 'ii', 'iii', 'iv', 'v']
const parts = player.name.trim().split(/\s+/).filter(
part => !suffixes.includes(part.toLowerCase())
)
if (parts.length >= 2) {
return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase()
}
return parts[0]?.charAt(0).toUpperCase() || '?'
}
// Fetch game data // Fetch game data
async function fetchGameData() { async function fetchGameData() {
try { try {
@ -493,7 +505,7 @@ onMounted(async () => {
<!-- Position Filter Tabs --> <!-- Position Filter Tabs -->
<div class="flex flex-wrap gap-1.5"> <div class="flex flex-wrap gap-1.5">
<button <button
v-for="filter in (['all', 'catchers', 'infielders', 'outfielders', 'pitchers'] as PositionFilter[])" v-for="filter in (['all', 'batters', 'pitchers'] as PositionFilter[])"
:key="filter" :key="filter"
:class="[ :class="[
'px-3 py-1.5 text-xs font-medium rounded-lg transition-all duration-200', 'px-3 py-1.5 text-xs font-medium rounded-lg transition-all duration-200',
@ -520,14 +532,14 @@ onMounted(async () => {
<!-- Player Headshot --> <!-- Player Headshot -->
<div class="flex-shrink-0 relative"> <div class="flex-shrink-0 relative">
<img <img
v-if="player.headshot || player.image" v-if="getPlayerPreviewImage(player)"
:src="player.headshot || player.image" :src="getPlayerPreviewImage(player)!"
:alt="player.name" :alt="player.name"
class="w-10 h-10 rounded-full object-cover bg-gray-600 ring-2 ring-gray-600/50" class="w-10 h-10 rounded-full object-cover bg-gray-600 ring-2 ring-gray-600/50"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'" @error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
/> />
<div v-else class="w-10 h-10 rounded-full bg-gradient-to-br from-gray-600 to-gray-700 flex items-center justify-center text-gray-300 text-sm font-bold ring-2 ring-gray-600/50"> <div v-else class="w-10 h-10 rounded-full bg-gradient-to-br from-gray-600 to-gray-700 flex items-center justify-center text-gray-300 text-sm font-bold ring-2 ring-gray-600/50">
{{ player.name.charAt(0) }} {{ getPlayerFallbackInitial(player) }}
</div> </div>
</div> </div>
@ -650,14 +662,14 @@ onMounted(async () => {
<!-- Player Headshot --> <!-- Player Headshot -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img <img
v-if="slot.player.headshot || slot.player.image" v-if="getPlayerPreviewImage(slot.player)"
:src="slot.player.headshot || slot.player.image" :src="getPlayerPreviewImage(slot.player)!"
:alt="slot.player.name" :alt="slot.player.name"
class="w-9 h-9 rounded-full object-cover bg-gray-600 ring-2 ring-blue-600/30" class="w-9 h-9 rounded-full object-cover bg-gray-600 ring-2 ring-blue-600/30"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'" @error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
/> />
<div v-else class="w-9 h-9 rounded-full bg-gradient-to-br from-blue-700 to-blue-800 flex items-center justify-center text-blue-200 text-sm font-bold ring-2 ring-blue-600/30"> <div v-else class="w-9 h-9 rounded-full bg-gradient-to-br from-blue-700 to-blue-800 flex items-center justify-center text-blue-200 text-sm font-bold ring-2 ring-blue-600/30">
{{ slot.player.name.charAt(0) }} {{ getPlayerFallbackInitial(slot.player) }}
</div> </div>
</div> </div>
@ -773,14 +785,14 @@ onMounted(async () => {
<!-- Player Headshot --> <!-- Player Headshot -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img <img
v-if="pitcherPlayer.headshot || pitcherPlayer.image" v-if="getPlayerPreviewImage(pitcherPlayer)"
:src="pitcherPlayer.headshot || pitcherPlayer.image" :src="getPlayerPreviewImage(pitcherPlayer)!"
:alt="pitcherPlayer.name" :alt="pitcherPlayer.name"
class="w-9 h-9 rounded-full object-cover bg-gray-600 ring-2 ring-green-600/30" class="w-9 h-9 rounded-full object-cover bg-gray-600 ring-2 ring-green-600/30"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'" @error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
/> />
<div v-else class="w-9 h-9 rounded-full bg-gradient-to-br from-green-700 to-green-800 flex items-center justify-center text-green-200 text-sm font-bold ring-2 ring-green-600/30"> <div v-else class="w-9 h-9 rounded-full bg-gradient-to-br from-green-700 to-green-800 flex items-center justify-center text-green-200 text-sm font-bold ring-2 ring-green-600/30">
{{ pitcherPlayer.name.charAt(0) }} {{ getPlayerFallbackInitial(pitcherPlayer) }}
</div> </div>
</div> </div>
@ -872,14 +884,14 @@ onMounted(async () => {
<!-- Large Player Image --> <!-- Large Player Image -->
<div class="h-48 bg-gradient-to-b from-blue-900 to-gray-800 flex items-center justify-center"> <div class="h-48 bg-gradient-to-b from-blue-900 to-gray-800 flex items-center justify-center">
<img <img
v-if="previewPlayer.headshot || previewPlayer.image" v-if="getPlayerPreviewImage(previewPlayer)"
:src="previewPlayer.headshot || previewPlayer.image" :src="getPlayerPreviewImage(previewPlayer)!"
:alt="previewPlayer.name" :alt="previewPlayer.name"
class="w-32 h-32 rounded-full object-cover border-4 border-gray-700 shadow-lg" class="w-32 h-32 rounded-full object-cover border-4 border-gray-700 shadow-lg"
@error="(e) => (e.target as HTMLImageElement).style.display = 'none'" @error="(e) => (e.target as HTMLImageElement).style.display = 'none'"
/> />
<div v-else class="w-32 h-32 rounded-full bg-gray-700 flex items-center justify-center text-gray-400 text-4xl font-bold border-4 border-gray-600"> <div v-else class="w-32 h-32 rounded-full bg-gray-700 flex items-center justify-center text-gray-400 text-3xl font-bold border-4 border-gray-600">
{{ previewPlayer.name.charAt(0) }} {{ getPlayerFallbackInitial(previewPlayer) }}
</div> </div>
</div> </div>