strat-gameplay-webapp/frontend-sba/components/Game/ScoreBoard.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

263 lines
9.5 KiB
Vue

<template>
<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 -->
<div class="flex items-center justify-between">
<!-- Away Team -->
<div class="flex-1 text-center relative">
<img
v-if="awayTeamThumbnail"
:src="awayTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-xs font-medium text-blue-100 mb-1">AWAY</div>
<div class="text-4xl font-bold tabular-nums">{{ awayScore }}</div>
</div>
</div>
<!-- Center: Inning + Runners/Outs -->
<div class="flex items-center gap-2">
<!-- Inning Indicator -->
<div class="bg-white/20 backdrop-blur rounded-lg px-3 py-2 text-center">
<div class="text-xs font-medium text-blue-100">INNING</div>
<div class="text-2xl font-bold">{{ inning }}</div>
<div class="text-xs font-medium">
<span
class="px-2 py-0.5 rounded-full text-xs font-bold"
:class="half === 'top' ? 'bg-blue-200 text-blue-900' : 'bg-yellow-400 text-yellow-900'"
>
{{ half === 'top' ? '▲ TOP' : '▼ BOT' }}
</span>
</div>
</div>
<!-- Runners + Outs Column -->
<div class="flex flex-col gap-0.5 w-20">
<!-- Runners (Mini Diamond) -->
<div class="bg-white/10 backdrop-blur rounded-lg px-2 py-1">
<div class="flex items-center justify-center">
<div class="relative w-11 h-11">
<!-- 2nd Base (Top) -->
<div
class="absolute top-0.5 left-1/2 -translate-x-1/2 w-5 h-5 rotate-45 transition-all"
:class="runners.second ? 'bg-yellow-400 shadow-lg shadow-yellow-500/50' : 'bg-white/20'"
/>
<!-- 1st Base (Right) -->
<div
class="absolute top-[64%] -right-1 -translate-y-1/2 w-5 h-5 rotate-45 transition-all"
:class="runners.first ? 'bg-yellow-400 shadow-lg shadow-yellow-500/50' : 'bg-white/20'"
/>
<!-- 3rd Base (Left) -->
<div
class="absolute top-[64%] -left-1 -translate-y-1/2 w-5 h-5 rotate-45 transition-all"
:class="runners.third ? 'bg-yellow-400 shadow-lg shadow-yellow-500/50' : 'bg-white/20'"
/>
</div>
</div>
</div>
<!-- Outs -->
<div class="bg-white/10 backdrop-blur rounded-lg px-2 py-1.5">
<div class="flex items-center justify-center gap-1.5">
<div
v-for="i in 3"
:key="i"
class="w-3 h-3 rounded-full transition-all"
:class="i <= outs ? 'bg-red-400 shadow-lg shadow-red-500/50' : 'bg-white/30'"
/>
</div>
</div>
</div>
</div>
<!-- Home Team -->
<div class="flex-1 text-center relative">
<img
v-if="homeTeamThumbnail"
:src="homeTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-xs font-medium text-blue-100 mb-1">HOME</div>
<div class="text-4xl font-bold tabular-nums">{{ homeScore }}</div>
</div>
</div>
</div>
</div>
<!-- Desktop Layout (lg and up) -->
<div class="hidden lg:flex items-center justify-between">
<!-- Left: Away Team Score -->
<div class="flex items-center gap-4">
<div class="text-center min-w-[100px] relative">
<img
v-if="awayTeamThumbnail"
:src="awayTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-sm font-medium text-blue-100">AWAY</div>
<div class="text-5xl font-bold tabular-nums">{{ awayScore }}</div>
</div>
</div>
</div>
<!-- Center: Game Situation -->
<div class="flex items-center gap-6">
<!-- Inning -->
<div class="bg-white/20 backdrop-blur rounded-lg px-6 py-3 text-center min-w-[120px]">
<div class="text-sm font-medium text-blue-100">INNING</div>
<div class="text-3xl font-bold">{{ inning }}</div>
<div class="mt-1">
<span
class="px-3 py-1 rounded-full text-sm font-bold"
:class="half === 'top' ? 'bg-blue-200 text-blue-900' : 'bg-yellow-400 text-yellow-900'"
>
{{ half === 'top' ? '▲ TOP' : '▼ BOTTOM' }}
</span>
</div>
</div>
<!-- Runners + Outs Column -->
<div class="flex flex-col gap-1 w-28">
<!-- Runners (Mini Diamond) - Fixed positioning -->
<div class="bg-white/10 backdrop-blur rounded-lg px-4 py-1.5">
<div class="flex items-center justify-center">
<div class="relative w-14 h-14">
<!-- 2nd Base (Top) -->
<div
class="absolute top-0.5 left-1/2 -translate-x-1/2 w-6 h-6 rotate-45 transition-all"
:class="runners.second ? 'bg-yellow-400 shadow-lg shadow-yellow-500/50 animate-pulse' : 'bg-white/20'"
/>
<!-- 1st Base (Right) -->
<div
class="absolute top-[60%] -right-1 -translate-y-1/2 w-6 h-6 rotate-45 transition-all"
:class="runners.first ? 'bg-yellow-400 shadow-lg shadow-yellow-500/50 animate-pulse' : 'bg-white/20'"
/>
<!-- 3rd Base (Left) -->
<div
class="absolute top-[60%] -left-1 -translate-y-1/2 w-6 h-6 rotate-45 transition-all"
:class="runners.third ? 'bg-yellow-400 shadow-lg shadow-yellow-500/50 animate-pulse' : 'bg-white/20'"
/>
</div>
</div>
</div>
<!-- Outs -->
<div class="bg-white/10 backdrop-blur rounded-lg px-4 py-2 flex items-center justify-center">
<div class="flex gap-2">
<div
v-for="i in 3"
:key="i"
class="w-4 h-4 rounded-full transition-all"
:class="i <= outs ? 'bg-red-400 shadow-lg shadow-red-500/50' : 'bg-white/30'"
/>
</div>
</div>
</div>
</div>
<!-- Right: Home Team Score -->
<div class="flex items-center gap-4">
<div class="text-center min-w-[100px] relative">
<img
v-if="homeTeamThumbnail"
:src="homeTeamThumbnail"
alt=""
class="absolute inset-0 w-full h-full object-contain opacity-15 pointer-events-none"
>
<div class="relative">
<div class="text-sm font-medium text-blue-100">HOME</div>
<div class="text-5xl font-bold tabular-nums">{{ homeScore }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { InningHalf } from '~/types/game'
interface Props {
homeScore?: number
awayScore?: number
inning?: number
half?: InningHalf
outs?: number
runners?: {
first: boolean
second: boolean
third: boolean
}
awayTeamColor?: string
homeTeamColor?: string
awayTeamThumbnail?: string | null
homeTeamThumbnail?: string | null
}
const props = withDefaults(defineProps<Props>(), {
homeScore: 0,
awayScore: 0,
inning: 1,
half: 'top',
outs: 0,
runners: () => ({ first: false, second: false, third: false }),
awayTeamColor: undefined,
homeTeamColor: undefined,
awayTeamThumbnail: undefined,
homeTeamThumbnail: 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>
<style scoped>
/* Ensure tabular numbers for consistent score display */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Pulse animation for runners */
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.1);
}
}
</style>