- ScoreBoard: Dynamic gradient using team colors (away left, home right) with dark center blend and 20% overlay for text readability - Fetch team colors from API using cached season from schedule state - Fix sticky tabs by removing overflow-auto from game layout main - Move play-by-play below gameplay panel on mobile layout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
246 lines
7.4 KiB
Vue
Executable File
246 lines
7.4 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"
|
|
/>
|
|
|
|
<!-- 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 type { SbaTeam } from '~/types/api'
|
|
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'],
|
|
})
|
|
|
|
// Config
|
|
const config = useRuntimeConfig()
|
|
|
|
// Stores
|
|
const route = useRoute()
|
|
const gameStore = useGameStore()
|
|
const authStore = useAuthStore()
|
|
const uiStore = useUiStore()
|
|
|
|
// Team data for colors
|
|
const teamsMap = ref<Map<number, SbaTeam>>(new Map())
|
|
|
|
// 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
|
|
const awayTeamColor = computed(() => {
|
|
if (!gameState.value) return undefined
|
|
return teamsMap.value.get(gameState.value.away_team_id)?.color
|
|
})
|
|
|
|
const homeTeamColor = computed(() => {
|
|
if (!gameState.value) return undefined
|
|
return teamsMap.value.get(gameState.value.home_team_id)?.color
|
|
})
|
|
|
|
// Get season from schedule state (already fetched on /games page)
|
|
const selectedSeason = useState<number>('schedule-season')
|
|
|
|
// Fetch team data when game state becomes available
|
|
watch(gameState, async (state) => {
|
|
if (!state) return
|
|
|
|
const teamIds = [state.home_team_id, state.away_team_id]
|
|
|
|
// Only fetch teams we don't have yet
|
|
const missingIds = teamIds.filter(id => !teamsMap.value.has(id))
|
|
if (missingIds.length === 0) return
|
|
|
|
try {
|
|
// Get season - use cached value or fetch current
|
|
let season = selectedSeason.value
|
|
if (!season) {
|
|
console.log('[Game Page] No cached season, fetching current...')
|
|
const currentInfo = await $fetch<{ season: number; week: number }>(`${config.public.apiUrl}/api/schedule/current`, {
|
|
credentials: 'include'
|
|
})
|
|
season = currentInfo.season
|
|
selectedSeason.value = season // Cache it
|
|
}
|
|
|
|
console.log('[Game Page] Fetching teams for season', season)
|
|
|
|
// Fetch all teams for the season and filter to the ones we need
|
|
const teams = await $fetch<SbaTeam[]>(`${config.public.apiUrl}/api/teams/`, {
|
|
credentials: 'include',
|
|
query: { season }
|
|
})
|
|
|
|
console.log('[Game Page] Got teams, looking for IDs:', teamIds)
|
|
|
|
for (const team of teams) {
|
|
if (teamIds.includes(team.id)) {
|
|
console.log('[Game Page] Found team:', team.id, team.sname, 'color:', team.color)
|
|
teamsMap.value.set(team.id, team)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn('[Game Page] Failed to fetch team data for colors:', error)
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// 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>
|