CLAUDE: Add team color gradient to scoreboard and fix sticky tabs
- 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>
This commit is contained in:
parent
3a91a5d477
commit
ff3f1746d6
@ -75,15 +75,6 @@
|
|||||||
:current-pitcher="gameState?.current_pitcher"
|
:current-pitcher="gameState?.current_pitcher"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Play-by-Play Feed -->
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">
|
|
||||||
<PlayByPlay
|
|
||||||
:plays="playHistory"
|
|
||||||
:limit="5"
|
|
||||||
:compact="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Decision Panel (Phase F3) -->
|
<!-- Decision Panel (Phase F3) -->
|
||||||
<DecisionPanel
|
<DecisionPanel
|
||||||
v-if="showDecisions"
|
v-if="showDecisions"
|
||||||
@ -116,6 +107,15 @@
|
|||||||
@submit-outcome="handleSubmitOutcome"
|
@submit-outcome="handleSubmitOutcome"
|
||||||
@dismiss-result="handleDismissResult"
|
@dismiss-result="handleDismissResult"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Play-by-Play Feed (below gameplay on mobile) -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg p-4 shadow-md">
|
||||||
|
<PlayByPlay
|
||||||
|
:plays="playHistory"
|
||||||
|
:limit="5"
|
||||||
|
:compact="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Desktop Layout (Grid) -->
|
<!-- Desktop Layout (Grid) -->
|
||||||
|
|||||||
@ -1,6 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-gradient-to-r from-primary to-blue-600 text-white shadow-lg">
|
<div class="relative text-white shadow-lg overflow-hidden">
|
||||||
<div class="container mx-auto px-3 py-4">
|
<!-- Team colors gradient background -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0"
|
||||||
|
:style="gradientStyle"
|
||||||
|
/>
|
||||||
|
<!-- Dark overlay for text readability -->
|
||||||
|
<div class="absolute inset-0 bg-black/20" />
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="relative container mx-auto px-3 py-4">
|
||||||
<!-- Mobile Layout (default) -->
|
<!-- Mobile Layout (default) -->
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
<!-- Score Display with Game Situation -->
|
<!-- Score Display with Game Situation -->
|
||||||
@ -156,6 +164,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import type { InningHalf } from '~/types/game'
|
import type { InningHalf } from '~/types/game'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -169,6 +178,8 @@ interface Props {
|
|||||||
second: boolean
|
second: boolean
|
||||||
third: boolean
|
third: boolean
|
||||||
}
|
}
|
||||||
|
awayTeamColor?: string
|
||||||
|
homeTeamColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@ -177,7 +188,21 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
inning: 1,
|
inning: 1,
|
||||||
half: 'top',
|
half: 'top',
|
||||||
outs: 0,
|
outs: 0,
|
||||||
runners: () => ({ first: false, second: false, third: false })
|
runners: () => ({ first: false, second: false, third: false }),
|
||||||
|
awayTeamColor: undefined,
|
||||||
|
homeTeamColor: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// Generate gradient style from team colors
|
||||||
|
// Uses Option 7: Solid blocks with center blend (away 30% -> dark center 50% -> home 70%)
|
||||||
|
const gradientStyle = computed(() => {
|
||||||
|
const awayColor = props.awayTeamColor || '#1e40af' // Default: SBA blue
|
||||||
|
const homeColor = props.homeTeamColor || '#1e40af' // Default: SBA blue
|
||||||
|
const centerColor = '#1f2937' // gray-800
|
||||||
|
|
||||||
|
return {
|
||||||
|
background: `linear-gradient(to right, ${awayColor} 30%, ${centerColor} 50%, ${homeColor} 70%)`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -55,7 +55,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Game Content (Full Width, No Container) -->
|
<!-- Game Content (Full Width, No Container) -->
|
||||||
<main class="flex-1 overflow-auto">
|
<main class="flex-1">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@ -1,40 +1,39 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<!-- Sticky Header: ScoreBoard + Tabs -->
|
<!-- ScoreBoard (scrolls with content) -->
|
||||||
<div class="sticky top-0 z-30">
|
<ScoreBoard
|
||||||
<!-- ScoreBoard -->
|
:home-score="gameState?.home_score"
|
||||||
<ScoreBoard
|
:away-score="gameState?.away_score"
|
||||||
:home-score="gameState?.home_score"
|
:inning="gameState?.inning"
|
||||||
:away-score="gameState?.away_score"
|
:half="gameState?.half"
|
||||||
:inning="gameState?.inning"
|
:outs="gameState?.outs"
|
||||||
:half="gameState?.half"
|
:runners="runnersState"
|
||||||
:outs="gameState?.outs"
|
:away-team-color="awayTeamColor"
|
||||||
:runners="runnersState"
|
:home-team-color="homeTeamColor"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Tab Navigation -->
|
<!-- Tab Navigation (sticky below header) -->
|
||||||
<div class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
|
<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="container mx-auto">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button
|
<button
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:class="[
|
:class="[
|
||||||
'flex-1 py-3 px-4 text-sm font-medium text-center transition-colors relative',
|
'flex-1 py-3 px-4 text-sm font-medium text-center transition-colors relative',
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'text-primary dark:text-blue-400'
|
? 'text-primary dark:text-blue-400'
|
||||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||||
]"
|
]"
|
||||||
@click="activeTab = tab.id"
|
@click="activeTab = tab.id"
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
<!-- Active indicator -->
|
<!-- Active indicator -->
|
||||||
<span
|
<span
|
||||||
v-if="activeTab === tab.id"
|
v-if="activeTab === tab.id"
|
||||||
class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary dark:bg-blue-400"
|
class="absolute bottom-0 left-0 right-0 h-0.5 bg-primary dark:bg-blue-400"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,6 +67,7 @@
|
|||||||
import { useGameStore } from '~/store/game'
|
import { useGameStore } from '~/store/game'
|
||||||
import { useAuthStore } from '~/store/auth'
|
import { useAuthStore } from '~/store/auth'
|
||||||
import { useUiStore } from '~/store/ui'
|
import { useUiStore } from '~/store/ui'
|
||||||
|
import type { SbaTeam } from '~/types/api'
|
||||||
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
|
import ScoreBoard from '~/components/Game/ScoreBoard.vue'
|
||||||
import GamePlay from '~/components/Game/GamePlay.vue'
|
import GamePlay from '~/components/Game/GamePlay.vue'
|
||||||
import LineupBuilder from '~/components/Game/LineupBuilder.vue'
|
import LineupBuilder from '~/components/Game/LineupBuilder.vue'
|
||||||
@ -78,12 +78,18 @@ definePageMeta({
|
|||||||
middleware: ['auth'],
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Config
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
|
||||||
// Stores
|
// Stores
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const gameStore = useGameStore()
|
const gameStore = useGameStore()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const uiStore = useUiStore()
|
const uiStore = useUiStore()
|
||||||
|
|
||||||
|
// Team data for colors
|
||||||
|
const teamsMap = ref<Map<number, SbaTeam>>(new Map())
|
||||||
|
|
||||||
// Game ID from route
|
// Game ID from route
|
||||||
const gameId = computed(() => route.params.id as string)
|
const gameId = computed(() => route.params.id as string)
|
||||||
|
|
||||||
@ -115,6 +121,63 @@ const runnersState = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 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
|
// Check if user is a manager of either team in this game
|
||||||
const isUserManager = computed(() => {
|
const isUserManager = computed(() => {
|
||||||
if (!gameState.value) return false
|
if (!gameState.value) return false
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user