CLAUDE: Add tabbed Recent/Scoring views to PlayByPlay component

Enhanced PlayByPlay with a tabbed interface:
- "Recent" tab shows plays from current half inning only
- "Scoring" tab shows all plays where runs were scored
- Badge counts on each tab show number of matching plays
- Tab-aware empty states with contextual messaging
- Footer shows total game plays count

Removed unused showFilters prop and showAllPlays toggle.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-28 12:18:56 -06:00
parent af598b3dee
commit 57121b62bd

View File

@ -8,15 +8,45 @@
</svg> </svg>
Play-by-Play Play-by-Play
</h2> </h2>
</div>
<!-- Filter Toggle (Mobile) --> <!-- Tabs -->
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
<button <button
v-if="showFilters" class="flex-1 py-2 px-4 text-sm font-medium border-b-2 transition-colors"
class="lg:hidden text-xs px-3 py-1 rounded-full transition-colors" :class="activeTab === 'recent'
:class="showAllPlays ? 'bg-gray-200 text-gray-700' : 'bg-primary/10 text-primary font-medium'" ? 'text-primary border-primary'
@click="showAllPlays = !showAllPlays" : 'text-gray-500 dark:text-gray-400 border-transparent hover:text-gray-700 dark:hover:text-gray-300'"
@click="activeTab = 'recent'"
> >
{{ showAllPlays ? 'Show Recent' : 'Show All' }} Recent
<span
v-if="recentPlays.length > 0"
class="ml-1.5 px-1.5 py-0.5 text-xs rounded-full"
:class="activeTab === 'recent'
? 'bg-primary/20 text-primary'
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400'"
>
{{ recentPlays.length }}
</span>
</button>
<button
class="flex-1 py-2 px-4 text-sm font-medium border-b-2 transition-colors"
:class="activeTab === 'scoring'
? 'text-green-600 border-green-600'
: 'text-gray-500 dark:text-gray-400 border-transparent hover:text-gray-700 dark:hover:text-gray-300'"
@click="activeTab = 'scoring'"
>
Scoring
<span
v-if="scoringPlays.length > 0"
class="ml-1.5 px-1.5 py-0.5 text-xs rounded-full"
:class="activeTab === 'scoring'
? 'bg-green-100 dark:bg-green-900/30 text-green-600'
: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-gray-400'"
>
{{ scoringPlays.length }}
</span>
</button> </button>
</div> </div>
@ -28,16 +58,23 @@
> >
<!-- Empty State --> <!-- Empty State -->
<div <div
v-if="!plays || plays.length === 0" v-if="displayedPlays.length === 0"
class="text-center py-12 px-4" class="text-center py-12 px-4"
> >
<div class="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center"> <div class="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg v-if="activeTab === 'recent'" xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</div> </div>
<p class="text-gray-500 dark:text-gray-400 font-medium">No plays yet</p> <p class="text-gray-500 dark:text-gray-400 font-medium">
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">The game will start soon...</p> {{ activeTab === 'recent' ? 'No plays this half inning' : 'No runs scored yet' }}
</p>
<p class="text-sm text-gray-400 dark:text-gray-500 mt-1">
{{ activeTab === 'recent' ? 'Plays will appear here as they happen' : 'Scoring plays will appear here' }}
</p>
</div> </div>
<!-- Play Items --> <!-- Play Items -->
@ -141,17 +178,14 @@
</TransitionGroup> </TransitionGroup>
</div> </div>
<!-- Load More (if showing recent) --> <!-- Total play count footer -->
<div <div
v-if="!showAllPlays && plays && plays.length > limit" v-if="plays && plays.length > 0"
class="text-center mt-4" class="text-center mt-4 pt-3 border-t border-gray-200 dark:border-gray-700"
> >
<button <span class="text-xs text-gray-400 dark:text-gray-500">
class="text-sm text-primary hover:text-blue-700 font-medium transition" {{ plays.length }} total {{ plays.length === 1 ? 'play' : 'plays' }} this game
@click="showAllPlays = true" </span>
>
View All {{ plays.length }} Plays
</button>
</div> </div>
</div> </div>
</template> </template>
@ -167,7 +201,6 @@ interface Props {
compact?: boolean compact?: boolean
scrollable?: boolean scrollable?: boolean
maxHeight?: number maxHeight?: number
showFilters?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -175,15 +208,19 @@ const props = withDefaults(defineProps<Props>(), {
limit: 10, limit: 10,
compact: false, compact: false,
scrollable: true, scrollable: true,
maxHeight: 0, maxHeight: 0
showFilters: true
}) })
// Store for player name lookup // Store for player name lookup and current game state
const gameStore = useGameStore() const gameStore = useGameStore()
// State // State
const showAllPlays = ref(false) type TabType = 'recent' | 'scoring'
const activeTab = ref<TabType>('recent')
// Current game state for filtering recent plays
const currentInning = computed(() => gameStore.currentInning)
const currentHalf = computed(() => gameStore.currentHalf)
// Helper functions for player names // Helper functions for player names
const getPlayerName = (lineupId: number | undefined): string | null => { const getPlayerName = (lineupId: number | undefined): string | null => {
@ -233,15 +270,30 @@ const hasRunnerMovements = (play: PlayResult): boolean => {
return play.runners_advanced && play.runners_advanced.length > 0 return play.runners_advanced && play.runners_advanced.length > 0
} }
// Computed // Computed - Filtered play lists
const displayedPlays = computed(() => { const recentPlays = computed(() => {
if (!props.plays || props.plays.length === 0) return [] if (!props.plays || props.plays.length === 0) return []
// Sort by play number descending (most recent first) // Filter plays from current half inning
const sorted = [...props.plays].sort((a, b) => b.play_number - a.play_number) return props.plays.filter(play =>
play.inning === currentInning.value && play.half === currentHalf.value
).sort((a, b) => b.play_number - a.play_number)
})
// Return limited or all const scoringPlays = computed(() => {
return showAllPlays.value ? sorted : sorted.slice(0, props.limit) if (!props.plays || props.plays.length === 0) return []
// Filter plays where runs were scored
return props.plays.filter(play => play.runs_scored > 0)
.sort((a, b) => b.play_number - a.play_number)
})
const displayedPlays = computed(() => {
if (activeTab.value === 'recent') {
return recentPlays.value
} else {
return scoringPlays.value
}
}) })
// Methods // Methods