strat-gameplay-webapp/frontend-sba/components/Game/CurrentSituation.vue
Cal Corum b15f80310b CLAUDE: Add LineupService and SBA API client for player data integration
Created centralized services for SBA player data fetching at lineup creation:

Backend - New Services:
- app/services/sba_api_client.py: REST client for SBA API (api.sba.manticorum.com)
  with batch player fetching and caching support
- app/services/lineup_service.py: High-level service combining DB operations
  with API calls for complete lineup entries with player data

Backend - Refactored Components:
- app/core/game_engine.py: Replaced raw API calls with LineupService,
  reduced _prepare_next_play() from ~50 lines to ~15 lines
- app/core/substitution_manager.py: Updated pinch_hit(), defensive_replace(),
  change_pitcher() to use lineup_service.get_sba_player_data()
- app/models/game_models.py: Added player_name/player_image to LineupPlayerState
- app/services/__init__.py: Exported new LineupService components
- app/websocket/handlers.py: Enhanced lineup state handling

Frontend - SBA League:
- components/Game/CurrentSituation.vue: Restored player images with fallback
  badges (P/B letters) for both mobile and desktop layouts
- components/Game/GameBoard.vue: Enhanced game board visualization
- composables/useGameActions.ts: Updated game action handling
- composables/useWebSocket.ts: Improved WebSocket state management
- pages/games/[id].vue: Enhanced game page with better state handling
- types/game.ts: Updated type definitions
- types/websocket.ts: Added WebSocket type support

Architecture Improvement:
All SBA player data fetching now goes through LineupService:
- Lineup creation: add_sba_player_to_lineup()
- Lineup loading: load_team_lineup_with_player_data()
- Substitutions: get_sba_player_data()

All 739 unit tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 11:55:18 -06:00

231 lines
8.5 KiB
Vue

<template>
<div class="current-situation">
<!-- Mobile Layout (Stacked) -->
<div class="lg:hidden space-y-3">
<!-- Current Pitcher Card -->
<div
v-if="currentPitcher"
class="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-xl p-4 border-2 border-blue-200 dark:border-blue-700 shadow-md"
>
<div class="flex items-center gap-3">
<!-- Pitcher Image/Badge -->
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
<img
v-if="pitcherPlayer?.image"
:src="pitcherPlayer.image"
:alt="pitcherName"
class="w-full h-full object-cover"
/>
<div v-else class="w-full h-full bg-blue-500 flex items-center justify-center text-white font-bold text-lg">
P
</div>
</div>
<!-- Pitcher Info -->
<div class="flex-1 min-w-0">
<div class="text-xs font-semibold text-blue-600 dark:text-blue-400 uppercase tracking-wide mb-0.5">
Pitching
</div>
<div class="text-base font-bold text-gray-900 dark:text-white truncate">
{{ pitcherName }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ currentPitcher.position }}
</div>
</div>
</div>
</div>
<!-- VS Indicator -->
<div class="flex items-center justify-center">
<div class="px-4 py-1 bg-gray-800 dark:bg-gray-700 text-white rounded-full text-xs font-bold shadow-lg">
VS
</div>
</div>
<!-- Current Batter Card -->
<div
v-if="currentBatter"
class="bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 rounded-xl p-4 border-2 border-red-200 dark:border-red-700 shadow-md"
>
<div class="flex items-center gap-3">
<!-- Batter Image/Badge -->
<div class="w-12 h-12 rounded-full flex items-center justify-center shadow-lg flex-shrink-0 overflow-hidden">
<img
v-if="batterPlayer?.image"
:src="batterPlayer.image"
:alt="batterName"
class="w-full h-full object-cover"
/>
<div v-else class="w-full h-full bg-red-500 flex items-center justify-center text-white font-bold text-lg">
B
</div>
</div>
<!-- Batter Info -->
<div class="flex-1 min-w-0">
<div class="text-xs font-semibold text-red-600 dark:text-red-400 uppercase tracking-wide mb-0.5">
At Bat
</div>
<div class="text-base font-bold text-gray-900 dark:text-white truncate">
{{ batterName }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ currentBatter.position }}
<span v-if="currentBatter.batting_order" class="ml-1">• Batting {{ currentBatter.batting_order }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Desktop Layout (Side-by-Side) -->
<div class="hidden lg:grid lg:grid-cols-2 gap-6">
<!-- Current Pitcher Card -->
<div
v-if="currentPitcher"
class="bg-gradient-to-br from-blue-50 to-blue-100 dark:from-blue-900/20 dark:to-blue-800/20 rounded-xl p-6 border-2 border-blue-200 dark:border-blue-700 shadow-lg"
>
<div class="flex items-start gap-4">
<!-- Pitcher Image/Badge -->
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
<img
v-if="pitcherPlayer?.image"
:src="pitcherPlayer.image"
:alt="pitcherName"
class="w-full h-full object-cover"
/>
<div v-else class="w-full h-full bg-blue-500 flex items-center justify-center text-white font-bold text-2xl">
P
</div>
</div>
<!-- Pitcher Details -->
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-blue-600 dark:text-blue-400 uppercase tracking-wide mb-1">
Pitching
</div>
<div class="text-xl font-bold text-gray-900 dark:text-white mb-1 truncate">
{{ pitcherName }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ currentPitcher.position }}
</div>
</div>
</div>
</div>
<!-- Current Batter Card -->
<div
v-if="currentBatter"
class="bg-gradient-to-br from-red-50 to-red-100 dark:from-red-900/20 dark:to-red-800/20 rounded-xl p-6 border-2 border-red-200 dark:border-red-700 shadow-lg"
>
<div class="flex items-start gap-4">
<!-- Batter Image/Badge -->
<div class="w-16 h-16 rounded-full flex items-center justify-center shadow-xl flex-shrink-0 overflow-hidden">
<img
v-if="batterPlayer?.image"
:src="batterPlayer.image"
:alt="batterName"
class="w-full h-full object-cover"
/>
<div v-else class="w-full h-full bg-red-500 flex items-center justify-center text-white font-bold text-2xl">
B
</div>
</div>
<!-- Batter Details -->
<div class="flex-1 min-w-0">
<div class="text-sm font-semibold text-red-600 dark:text-red-400 uppercase tracking-wide mb-1">
At Bat
</div>
<div class="text-xl font-bold text-gray-900 dark:text-white mb-1 truncate">
{{ batterName }}
</div>
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ currentBatter.position }}
<span v-if="currentBatter.batting_order" class="ml-2">• Batting {{ currentBatter.batting_order }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div
v-if="!currentBatter && !currentPitcher"
class="text-center py-12 px-4 bg-gray-50 dark:bg-gray-800 rounded-xl border-2 border-dashed border-gray-300 dark:border-gray-700"
>
<div class="w-16 h-16 mx-auto mb-4 bg-gray-200 dark:bg-gray-700 rounded-full flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<p class="text-gray-500 dark:text-gray-400 font-medium">Waiting for game to start...</p>
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Players will appear here once the game begins.</p>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { LineupPlayerState } from '~/types/game'
import { useGameStore } from '~/store/game'
interface Props {
currentBatter?: LineupPlayerState | null
currentPitcher?: LineupPlayerState | null
}
const props = withDefaults(defineProps<Props>(), {
currentBatter: null,
currentPitcher: null
})
const gameStore = useGameStore()
// Resolve player data from lineup using lineup_id
const batterPlayer = computed(() => {
if (!props.currentBatter) return null
const lineupEntry = gameStore.findPlayerInLineup(props.currentBatter.lineup_id)
return lineupEntry?.player ?? null
})
const pitcherPlayer = computed(() => {
if (!props.currentPitcher) return null
const lineupEntry = gameStore.findPlayerInLineup(props.currentPitcher.lineup_id)
return lineupEntry?.player ?? null
})
// Computed properties for player names with fallback
const batterName = computed(() => {
if (batterPlayer.value?.name) return batterPlayer.value.name
if (!props.currentBatter) return 'Unknown Batter'
return `Player #${props.currentBatter.card_id || props.currentBatter.lineup_id}`
})
const pitcherName = computed(() => {
if (pitcherPlayer.value?.name) return pitcherPlayer.value.name
if (!props.currentPitcher) return 'Unknown Pitcher'
return `Player #${props.currentPitcher.card_id || props.currentPitcher.lineup_id}`
})
</script>
<style scoped>
/* Optional: Add subtle animations */
.current-situation > div {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>