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:
parent
d65ae19f5e
commit
2dd2b530f8
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user