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
|
// 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>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user