Updated all 4 demo pages to use new action field and added navigation footers for easy cross-linking between demo pages. Changes: - demo-decisions.vue: Updated to use action field, added runner prop bindings - demo.vue: Added footer with links to other demos - demo-gameplay.vue: Added footer with links to other demos - demo-substitutions.vue: Added footer with links to other demos Demo page updates: - OffensiveDecision now uses action field instead of approach/hit_and_run/bunt - Action labels properly mapped (swing_away, steal, check_jump, etc.) - Added runner state props (runnerOnFirst, runnerOnSecond, runnerOnThird) - Added outs prop for smart filtering Footer features: - 3-column grid layout responsive to mobile - Icons and descriptions for each demo - Hover effects on links - Consistent styling across all pages Demo pages now fully connected: - /demo → Game State Demo (ScoreBoard, GameBoard, etc.) - /demo-decisions → Decision Components Demo (new action-based) - /demo-gameplay → Gameplay Components Demo (DiceRoller, ManualOutcome) - /demo-substitutions → Substitution Components Demo Files modified: - pages/demo-decisions.vue - pages/demo.vue - pages/demo-gameplay.vue - pages/demo-substitutions.vue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
436 lines
13 KiB
Vue
436 lines
13 KiB
Vue
<template>
|
|
<div class="demo-page">
|
|
<!-- Header -->
|
|
<div class="demo-header">
|
|
<h1 class="demo-title">Phase F5: Substitution Components Demo</h1>
|
|
<p class="demo-subtitle">
|
|
Pinch Hitter, Defensive Replacement, Pitching Change, and SubstitutionPanel
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="demo-tabs">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
:class="['tab-button', activeTab === tab.id ? 'tab-active' : 'tab-inactive']"
|
|
@click="activeTab = tab.id"
|
|
>
|
|
{{ tab.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="demo-content">
|
|
<!-- Tab 1: PinchHitterSelector -->
|
|
<div v-if="activeTab === 'pinch-hitter'" class="tab-panel">
|
|
<div class="section">
|
|
<h2 class="section-title">PinchHitterSelector Component</h2>
|
|
<p class="section-description">
|
|
Select a bench player to bat in place of the current batter. Filters to only show available (non-active, non-fatigued) bench players.
|
|
</p>
|
|
|
|
<div class="component-showcase">
|
|
<PinchHitterSelector
|
|
:player-out="mockCurrentBatter"
|
|
:bench-players="mockBenchPlayers"
|
|
:team-id="1"
|
|
@submit="handlePinchHitterSubmit"
|
|
@cancel="handleCancel"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="lastSubmission" class="result-box">
|
|
<h3 class="result-title">Last Submitted:</h3>
|
|
<pre class="result-code">{{ JSON.stringify(lastSubmission, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 2: DefensiveReplacementSelector -->
|
|
<div v-if="activeTab === 'defensive'" class="tab-panel">
|
|
<div class="section">
|
|
<h2 class="section-title">DefensiveReplacementSelector Component</h2>
|
|
<p class="section-description">
|
|
Replace a defensive player with a bench player at a specific position. First select position, then choose from eligible players.
|
|
</p>
|
|
|
|
<div class="component-showcase">
|
|
<DefensiveReplacementSelector
|
|
:player-out="mockDefensivePlayer"
|
|
:bench-players="mockBenchPlayers"
|
|
:team-id="1"
|
|
@submit="handleDefensiveSubmit"
|
|
@cancel="handleCancel"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="lastSubmission" class="result-box">
|
|
<h3 class="result-title">Last Submitted:</h3>
|
|
<pre class="result-code">{{ JSON.stringify(lastSubmission, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 3: PitchingChangeSelector -->
|
|
<div v-if="activeTab === 'pitching'" class="tab-panel">
|
|
<div class="section">
|
|
<h2 class="section-title">PitchingChangeSelector Component</h2>
|
|
<p class="section-description">
|
|
Bring in a relief pitcher from the bench. Shows fatigue status and prevents selection of fatigued pitchers.
|
|
</p>
|
|
|
|
<div class="component-showcase">
|
|
<PitchingChangeSelector
|
|
:current-pitcher="mockCurrentPitcher"
|
|
:bench-players="mockBenchPlayers"
|
|
:team-id="1"
|
|
@submit="handlePitchingSubmit"
|
|
@cancel="handleCancel"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="lastSubmission" class="result-box">
|
|
<h3 class="result-title">Last Submitted:</h3>
|
|
<pre class="result-code">{{ JSON.stringify(lastSubmission, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab 4: SubstitutionPanel (Full Integration) -->
|
|
<div v-if="activeTab === 'panel'" class="tab-panel">
|
|
<div class="section">
|
|
<h2 class="section-title">SubstitutionPanel Component</h2>
|
|
<p class="section-description">
|
|
Complete substitution workflow orchestration - combines all three selector types with tab navigation and state management.
|
|
</p>
|
|
|
|
<div class="component-showcase">
|
|
<SubstitutionPanel
|
|
game-id="demo-game-123"
|
|
:team-id="1"
|
|
:current-lineup="mockCurrentLineup"
|
|
:bench-players="mockBenchPlayers"
|
|
:current-pitcher="mockCurrentPitcher"
|
|
:current-batter="mockCurrentBatter"
|
|
@pinch-hitter="handlePinchHitterSubmit"
|
|
@defensive-replacement="handleDefensiveSubmit"
|
|
@pitching-change="handlePitchingSubmit"
|
|
@cancel="handleCancel"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="lastSubmission" class="result-box">
|
|
<h3 class="result-title">Last Submitted:</h3>
|
|
<pre class="result-code">{{ JSON.stringify(lastSubmission, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer with Demo Links -->
|
|
<div class="mt-12 border-t border-gray-200 dark:border-gray-700 pt-8">
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6">
|
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-4">
|
|
📚 Other Demo Pages
|
|
</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<NuxtLink
|
|
to="/demo"
|
|
class="p-4 rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
|
>
|
|
<div class="text-2xl mb-2">🎮</div>
|
|
<div class="font-semibold text-gray-900 dark:text-white">Game State Demo</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
Live game state visualization
|
|
</div>
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
to="/demo-decisions"
|
|
class="p-4 rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
|
>
|
|
<div class="text-2xl mb-2">⚔️</div>
|
|
<div class="font-semibold text-gray-900 dark:text-white">Decisions Demo</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
Decision input components
|
|
</div>
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
to="/demo-gameplay"
|
|
class="p-4 rounded-lg border-2 border-gray-300 dark:border-gray-600 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
|
>
|
|
<div class="text-2xl mb-2">🎲</div>
|
|
<div class="font-semibold text-gray-900 dark:text-white">Gameplay Demo</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
Dice rolling & manual outcomes
|
|
</div>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue'
|
|
import type { Lineup, SbaPlayer } from '~/types'
|
|
import PinchHitterSelector from '~/components/Substitutions/PinchHitterSelector.vue'
|
|
import DefensiveReplacementSelector from '~/components/Substitutions/DefensiveReplacementSelector.vue'
|
|
import PitchingChangeSelector from '~/components/Substitutions/PitchingChangeSelector.vue'
|
|
import SubstitutionPanel from '~/components/Substitutions/SubstitutionPanel.vue'
|
|
|
|
// Helper to create mock player
|
|
function createMockPlayer(id: number, name: string, positions: string[], wara = 2.5): SbaPlayer {
|
|
const player: SbaPlayer = {
|
|
id,
|
|
name,
|
|
image: `https://via.placeholder.com/150?text=${name.replace(' ', '+')}`,
|
|
pos_1: null,
|
|
pos_2: null,
|
|
pos_3: null,
|
|
pos_4: null,
|
|
pos_5: null,
|
|
pos_6: null,
|
|
pos_7: null,
|
|
pos_8: null,
|
|
wara,
|
|
team_id: 1,
|
|
team_name: 'Demo Team',
|
|
season: '2024',
|
|
strat_code: null,
|
|
bbref_id: null,
|
|
injury_rating: null,
|
|
}
|
|
|
|
positions.forEach((pos, index) => {
|
|
const key = `pos_${index + 1}` as keyof SbaPlayer
|
|
;(player as any)[key] = pos
|
|
})
|
|
|
|
return player
|
|
}
|
|
|
|
// Helper to create mock lineup entry
|
|
function createMockLineup(
|
|
id: number,
|
|
player: SbaPlayer,
|
|
isActive: boolean,
|
|
position = 'OF',
|
|
battingOrder: number | null = null,
|
|
isFatigued = false
|
|
): Lineup {
|
|
return {
|
|
id,
|
|
game_id: 'demo-game-123',
|
|
team_id: 1,
|
|
player_id: player.id,
|
|
position,
|
|
batting_order: battingOrder,
|
|
is_starter: true,
|
|
is_active: isActive,
|
|
entered_inning: 1,
|
|
replacing_id: null,
|
|
after_play: null,
|
|
is_fatigued: isFatigued,
|
|
player,
|
|
}
|
|
}
|
|
|
|
// Tab definitions
|
|
const tabs = [
|
|
{ id: 'pinch-hitter', label: 'Pinch Hitter' },
|
|
{ id: 'defensive', label: 'Defensive Replacement' },
|
|
{ id: 'pitching', label: 'Pitching Change' },
|
|
{ id: 'panel', label: 'Substitution Panel' },
|
|
]
|
|
|
|
// State
|
|
const activeTab = ref('pinch-hitter')
|
|
const lastSubmission = ref<any>(null)
|
|
|
|
// Mock data
|
|
const mockCurrentPitcher = createMockLineup(
|
|
1,
|
|
createMockPlayer(101, 'John Starter', ['P'], 3.2),
|
|
true,
|
|
'P',
|
|
null,
|
|
false
|
|
)
|
|
|
|
const mockCurrentBatter = createMockLineup(
|
|
2,
|
|
createMockPlayer(102, 'Mike Batter', ['1B', '3B'], 2.8),
|
|
true,
|
|
'1B',
|
|
3,
|
|
false
|
|
)
|
|
|
|
const mockDefensivePlayer = createMockLineup(
|
|
3,
|
|
createMockPlayer(103, 'Sally Fielder', ['OF', 'CF', 'RF'], 2.1),
|
|
true,
|
|
'CF',
|
|
5,
|
|
false
|
|
)
|
|
|
|
const mockCurrentLineup: Lineup[] = [
|
|
mockCurrentPitcher,
|
|
mockCurrentBatter,
|
|
mockDefensivePlayer,
|
|
createMockLineup(4, createMockPlayer(104, 'Joe Catcher', ['C'], 2.5), true, 'C', 1),
|
|
createMockLineup(5, createMockPlayer(105, 'Bob Second', ['2B', 'SS'], 1.9), true, '2B', 2),
|
|
createMockLineup(6, createMockPlayer(106, 'Tim Third', ['3B', '1B'], 2.3), true, '3B', 4),
|
|
createMockLineup(7, createMockPlayer(107, 'Dan Short', ['SS', '2B', '3B'], 2.7), true, 'SS', 6),
|
|
createMockLineup(8, createMockPlayer(108, 'Left Fielder', ['LF', 'CF'], 1.8), true, 'LF', 7),
|
|
createMockLineup(9, createMockPlayer(109, 'Right Fielder', ['RF', 'LF'], 2.0), true, 'RF', 8),
|
|
]
|
|
|
|
const mockBenchPlayers: Lineup[] = [
|
|
createMockLineup(10, createMockPlayer(201, 'Relief Ace', ['P'], 3.5), false),
|
|
createMockLineup(11, createMockPlayer(202, 'Setup Man', ['P'], 2.9), false),
|
|
createMockLineup(12, createMockPlayer(203, 'Closer', ['P'], 4.2), false),
|
|
createMockLineup(13, createMockPlayer(204, 'Pinch Hitter Pro', ['OF', 'DH'], 3.1), false),
|
|
createMockLineup(14, createMockPlayer(205, 'Utility Infielder', ['2B', '3B', 'SS'], 1.7), false),
|
|
createMockLineup(15, createMockPlayer(206, 'Backup Catcher', ['C', '1B'], 1.5), false),
|
|
createMockLineup(16, createMockPlayer(207, 'Outfield Reserve', ['LF', 'CF', 'RF'], 2.2), false),
|
|
createMockLineup(17, createMockPlayer(208, 'Fatigued Reliever', ['P'], 2.0), false, 'P', null, true),
|
|
]
|
|
|
|
// Event handlers
|
|
const handlePinchHitterSubmit = (payload: any) => {
|
|
lastSubmission.value = {
|
|
type: 'pinch_hitter',
|
|
...payload,
|
|
timestamp: new Date().toISOString(),
|
|
}
|
|
console.log('[Demo] Pinch hitter submitted:', payload)
|
|
}
|
|
|
|
const handleDefensiveSubmit = (payload: any) => {
|
|
lastSubmission.value = {
|
|
type: 'defensive_replacement',
|
|
...payload,
|
|
timestamp: new Date().toISOString(),
|
|
}
|
|
console.log('[Demo] Defensive replacement submitted:', payload)
|
|
}
|
|
|
|
const handlePitchingSubmit = (payload: any) => {
|
|
lastSubmission.value = {
|
|
type: 'pitching_change',
|
|
...payload,
|
|
timestamp: new Date().toISOString(),
|
|
}
|
|
console.log('[Demo] Pitching change submitted:', payload)
|
|
}
|
|
|
|
const handleCancel = () => {
|
|
console.log('[Demo] Substitution cancelled')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.demo-page {
|
|
@apply min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-gray-900;
|
|
@apply p-6;
|
|
}
|
|
|
|
.demo-header {
|
|
@apply text-center mb-8;
|
|
}
|
|
|
|
.demo-title {
|
|
@apply text-4xl font-bold text-white mb-2;
|
|
}
|
|
|
|
.demo-subtitle {
|
|
@apply text-lg text-blue-200;
|
|
}
|
|
|
|
/* Tabs */
|
|
.demo-tabs {
|
|
@apply flex gap-2 mb-6 bg-gray-800 rounded-xl p-2;
|
|
@apply overflow-x-auto;
|
|
}
|
|
|
|
.tab-button {
|
|
@apply px-6 py-3 rounded-lg font-semibold transition-all duration-200;
|
|
@apply whitespace-nowrap;
|
|
}
|
|
|
|
.tab-inactive {
|
|
@apply text-gray-400 hover:text-white hover:bg-gray-700;
|
|
}
|
|
|
|
.tab-active {
|
|
@apply bg-blue-600 text-white shadow-lg;
|
|
}
|
|
|
|
/* Content */
|
|
.demo-content {
|
|
@apply bg-gray-800 rounded-xl shadow-2xl p-6;
|
|
}
|
|
|
|
.tab-panel {
|
|
@apply space-y-6;
|
|
}
|
|
|
|
.section {
|
|
@apply space-y-4;
|
|
}
|
|
|
|
.section-title {
|
|
@apply text-2xl font-bold text-white;
|
|
}
|
|
|
|
.section-description {
|
|
@apply text-gray-300 leading-relaxed;
|
|
}
|
|
|
|
.component-showcase {
|
|
@apply bg-gray-900 rounded-xl p-6;
|
|
@apply border-2 border-gray-700;
|
|
}
|
|
|
|
/* Result Box */
|
|
.result-box {
|
|
@apply mt-6 bg-gray-900 rounded-xl p-6 border-2 border-green-500;
|
|
}
|
|
|
|
.result-title {
|
|
@apply text-lg font-bold text-green-400 mb-3;
|
|
}
|
|
|
|
.result-code {
|
|
@apply text-sm text-gray-300 bg-gray-800 rounded-lg p-4;
|
|
@apply overflow-auto;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
/* Mobile optimizations */
|
|
@media (max-width: 640px) {
|
|
.demo-page {
|
|
@apply p-4;
|
|
}
|
|
|
|
.demo-title {
|
|
@apply text-2xl;
|
|
}
|
|
|
|
.demo-subtitle {
|
|
@apply text-base;
|
|
}
|
|
|
|
.demo-tabs {
|
|
@apply flex-nowrap;
|
|
}
|
|
|
|
.tab-button {
|
|
@apply px-4 py-2 text-sm;
|
|
}
|
|
}
|
|
</style>
|