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
const searchQuery = ref('')
type PositionFilter = 'all' | 'catchers' | 'infielders' | 'outfielders' | 'pitchers'
type PositionFilter = 'all' | 'batters' | 'pitchers'
const positionFilter = ref<PositionFilter>('all')
// Player preview modal
@ -67,10 +67,13 @@ const availableAwayRoster = computed(() => {
const currentLineup = computed(() => activeTab.value === 'home' ? homeLineup.value : awayLineup.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 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
@ -102,8 +105,8 @@ const validationErrors = computed(() => {
function validateLineup(lineup: LineupSlot[], teamName: string): string[] {
const errors: string[] = []
// Check if P is in batting order
const pitcherInBattingOrder = lineup.slice(0, 9).some(s => s.position === 'P')
// Check if a pitcher is in batting order (no DH game)
const pitcherInBattingOrder = lineup.slice(0, 9).some(s => s.position && PITCHER_POSITIONS.includes(s.position))
const requiredSlots = pitcherInBattingOrder ? 9 : 10
// 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 {
if (category === 'all') return true
const positions = getPlayerPositions(player)
const isPitcher = positions.some(p => PITCHER_POSITIONS.includes(p))
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':
return positions.some(p => PITCHER_POSITIONS.includes(p))
return isPitcher
case 'batters':
return !isPitcher
default:
return true
}
@ -269,6 +263,24 @@ function closePlayerPreview() {
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
async function fetchGameData() {
try {
@ -493,7 +505,7 @@ onMounted(async () => {
<!-- Position Filter Tabs -->
<div class="flex flex-wrap gap-1.5">
<button
v-for="filter in (['all', 'catchers', 'infielders', 'outfielders', 'pitchers'] as PositionFilter[])"
v-for="filter in (['all', 'batters', 'pitchers'] as PositionFilter[])"
:key="filter"
:class="[
'px-3 py-1.5 text-xs font-medium rounded-lg transition-all duration-200',
@ -520,14 +532,14 @@ onMounted(async () => {
<!-- Player Headshot -->
<div class="flex-shrink-0 relative">
<img
v-if="player.headshot || player.image"
:src="player.headshot || player.image"
v-if="getPlayerPreviewImage(player)"
:src="getPlayerPreviewImage(player)!"
:alt="player.name"
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'"
/>
<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>
@ -650,14 +662,14 @@ onMounted(async () => {
<!-- Player Headshot -->
<div class="flex-shrink-0">
<img
v-if="slot.player.headshot || slot.player.image"
:src="slot.player.headshot || slot.player.image"
v-if="getPlayerPreviewImage(slot.player)"
:src="getPlayerPreviewImage(slot.player)!"
:alt="slot.player.name"
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'"
/>
<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>
@ -773,14 +785,14 @@ onMounted(async () => {
<!-- Player Headshot -->
<div class="flex-shrink-0">
<img
v-if="pitcherPlayer.headshot || pitcherPlayer.image"
:src="pitcherPlayer.headshot || pitcherPlayer.image"
v-if="getPlayerPreviewImage(pitcherPlayer)"
:src="getPlayerPreviewImage(pitcherPlayer)!"
:alt="pitcherPlayer.name"
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'"
/>
<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>
@ -872,14 +884,14 @@ onMounted(async () => {
<!-- Large Player Image -->
<div class="h-48 bg-gradient-to-b from-blue-900 to-gray-800 flex items-center justify-center">
<img
v-if="previewPlayer.headshot || previewPlayer.image"
:src="previewPlayer.headshot || previewPlayer.image"
v-if="getPlayerPreviewImage(previewPlayer)"
:src="getPlayerPreviewImage(previewPlayer)!"
:alt="previewPlayer.name"
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'"
/>
<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">
{{ previewPlayer.name.charAt(0) }}
<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">
{{ getPlayerFallbackInitial(previewPlayer) }}
</div>
</div>