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:
Cal Corum 2026-01-16 23:14:46 -06:00
parent 3a91a5d477
commit ff3f1746d6
4 changed files with 135 additions and 47 deletions

View File

@ -75,15 +75,6 @@
: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) -->
<DecisionPanel
v-if="showDecisions"
@ -116,6 +107,15 @@
@submit-outcome="handleSubmitOutcome"
@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>
<!-- Desktop Layout (Grid) -->

View File

@ -1,6 +1,14 @@
<template>
<div class="bg-gradient-to-r from-primary to-blue-600 text-white shadow-lg">
<div class="container mx-auto px-3 py-4">
<div class="relative text-white shadow-lg overflow-hidden">
<!-- 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) -->
<div class="lg:hidden">
<!-- Score Display with Game Situation -->
@ -156,6 +164,7 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { InningHalf } from '~/types/game'
interface Props {
@ -169,6 +178,8 @@ interface Props {
second: boolean
third: boolean
}
awayTeamColor?: string
homeTeamColor?: string
}
const props = withDefaults(defineProps<Props>(), {
@ -177,7 +188,21 @@ const props = withDefaults(defineProps<Props>(), {
inning: 1,
half: 'top',
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>

View File

@ -55,7 +55,7 @@
</header>
<!-- Game Content (Full Width, No Container) -->
<main class="flex-1 overflow-auto">
<main class="flex-1">
<slot />
</main>

View File

@ -1,40 +1,39 @@
<template>
<div class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Sticky Header: ScoreBoard + Tabs -->
<div class="sticky top-0 z-30">
<!-- ScoreBoard -->
<ScoreBoard
:home-score="gameState?.home_score"
:away-score="gameState?.away_score"
:inning="gameState?.inning"
:half="gameState?.half"
:outs="gameState?.outs"
:runners="runnersState"
/>
<!-- 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 -->
<div class="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>
<!-- 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>
@ -68,6 +67,7 @@
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'
@ -78,12 +78,18 @@ definePageMeta({
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)
@ -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
const isUserManager = computed(() => {
if (!gameState.value) return false