strat-gameplay-webapp/frontend-sba/pages/games/[id].vue
Cal Corum d60b7a2d60 CLAUDE: Store team display info in DB and fix lineup auto-start
Backend:
- Add game_metadata to create_game() and quick_create_game() endpoints
- Fetch team display info (lname, sname, abbrev, color, thumbnail) from
  SBA API at game creation time and store in DB
- Populate GameState with team display fields from game_metadata
- Fix submit_team_lineup to cache lineup in state_manager after DB write
  so auto-start correctly detects both teams ready

Frontend:
- Read team colors/names/thumbnails from gameState instead of useState
- Remove useState approach that failed across SSR navigation
- Fix create.vue redirect from legacy /games/lineup/[id] to /games/[id]
- Update game.vue header to show team names from gameState

Docs:
- Update CLAUDE.md to note dev mode has broken auth, always use prod

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 08:43:26 -06:00

204 lines
6.3 KiB
Vue
Executable File

<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- ScoreBoard (scrolls with content) -->
<ScoreBoard
:home-score="gameState?.home_score"
:away-score="gameState?.away_score"
:inning="gameState?.inning"
:half="gameState?.half"
:outs="gameState?.outs"
:runners="runnersState"
:away-team-color="awayTeamColor"
:home-team-color="homeTeamColor"
:away-team-thumbnail="awayTeamThumbnail"
:home-team-thumbnail="homeTeamThumbnail"
/>
<!-- Tab Navigation (sticky below header) -->
<div class="sticky top-[52px] z-30 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div class="container mx-auto">
<div class="flex">
<button
v-for="tab in tabs"
:key="tab.id"
:class="[
'flex-1 py-3 px-4 text-sm font-medium text-center transition-colors relative',
activeTab === tab.id
? 'text-primary dark:text-blue-400'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
]"
@click="activeTab = tab.id"
>
{{ tab.label }}
<!-- Active indicator -->
<span
v-if="activeTab === tab.id"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary dark:bg-blue-400"
/>
</button>
</div>
</div>
</div>
<!-- Tab Content -->
<div class="tab-content">
<!-- Game Tab (use v-show to keep WebSocket connected) -->
<GamePlay
v-show="activeTab === 'game'"
:game-id="gameId"
/>
<!-- Lineups Tab (use v-show to preserve state when switching tabs) -->
<div v-show="activeTab === 'lineups'" class="container mx-auto px-4 py-6">
<LineupBuilder
:game-id="gameId"
:team-id="myManagedTeamId"
@lineups-submitted="handleLineupsSubmitted"
/>
</div>
<!-- Stats Tab -->
<div v-show="activeTab === 'stats'" class="container mx-auto px-4 py-6">
<GameStats :game-id="gameId" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useGameStore } from '~/store/game'
import { useAuthStore } from '~/store/auth'
import { useUiStore } from '~/store/ui'
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
import GamePlay from '~/components/Game/GamePlay.vue'
import LineupBuilder from '~/components/Game/LineupBuilder.vue'
import GameStats from '~/components/Game/GameStats.vue'
definePageMeta({
layout: 'game',
middleware: ['auth'],
})
// Stores
const route = useRoute()
const gameStore = useGameStore()
const authStore = useAuthStore()
const uiStore = useUiStore()
// Note: Team display info now comes directly from gameState (stored in DB at game creation)
// Game ID from route
const gameId = computed(() => route.params.id as string)
// Tab state
type GameTab = 'game' | 'lineups' | 'stats'
const activeTab = ref<GameTab>('game')
const defaultTabSet = ref(false)
// Tab definitions
const tabs = [
{ id: 'game' as const, label: 'Game' },
{ id: 'lineups' as const, label: 'Lineups' },
{ id: 'stats' as const, label: 'Stats' },
]
// Game state from store (populated by GamePlay component via WebSocket)
const gameState = computed(() => gameStore.gameState)
// Runners state for ScoreBoard
const runnersState = computed(() => {
if (!gameState.value) {
return { first: false, second: false, third: false }
}
return {
first: gameState.value.on_first !== null,
second: gameState.value.on_second !== null,
third: gameState.value.on_third !== null
}
})
// Team colors for ScoreBoard gradient (from gameState, stored in DB at creation)
const awayTeamColor = computed(() => {
const color = gameState.value?.away_team_color
// Add # prefix if color exists but doesn't have it
return color ? (color.startsWith('#') ? color : `#${color}`) : undefined
})
const homeTeamColor = computed(() => {
const color = gameState.value?.home_team_color
// Add # prefix if color exists but doesn't have it
return color ? (color.startsWith('#') ? color : `#${color}`) : undefined
})
// Team thumbnails for ScoreBoard
const awayTeamThumbnail = computed(() => gameState.value?.away_team_thumbnail)
const homeTeamThumbnail = computed(() => gameState.value?.home_team_thumbnail)
// Check if user is a manager of either team in this game
const isUserManager = computed(() => {
if (!gameState.value) return false
const userTeams = authStore.userTeamIds
return userTeams.includes(gameState.value.home_team_id) ||
userTeams.includes(gameState.value.away_team_id)
})
// Determine which team the user manages (if any)
const myManagedTeamId = computed(() => {
if (!gameState.value) return null
const userTeams = authStore.userTeamIds
// Check home team first
if (userTeams.includes(gameState.value.home_team_id)) {
return gameState.value.home_team_id
}
// Then away team
if (userTeams.includes(gameState.value.away_team_id)) {
return gameState.value.away_team_id
}
return null
})
// Set default tab based on game status and user role
watch(gameState, (state) => {
if (!state) return
// Only set default once (don't override user navigation)
if (defaultTabSet.value) return
defaultTabSet.value = true
// Completed games → Stats tab
if (state.status === 'completed') {
console.log('[Game Page] Game completed - defaulting to Stats tab')
activeTab.value = 'stats'
}
// Pending games + manager → Lineups tab
else if (state.status === 'pending' && isUserManager.value) {
console.log('[Game Page] Pending game + manager - defaulting to Lineups tab')
activeTab.value = 'lineups'
}
// All other cases → Game tab (already default)
else {
console.log('[Game Page] Defaulting to Game tab (status:', state.status, ', isManager:', isUserManager.value, ')')
}
}, { immediate: true })
// Handle lineup submission
const handleLineupsSubmitted = (result: any) => {
console.log('[Game Page] Lineups submitted:', result)
uiStore.showSuccess('Lineups submitted successfully!')
// Switch to game tab after submitting lineups
activeTab.value = 'game'
}
</script>
<style scoped>
/* Tab content fills available space */
.tab-content {
min-height: calc(100vh - 128px); /* Subtract scoreboard + tab bar height */
}
</style>