strat-gameplay-webapp/frontend-sba/pages/games/index.vue
Cal Corum a87d149788 CLAUDE: Implement game creation and lineup submission workflow
Complete implementation of pre-game setup flow allowing players to create games
and submit lineups before gameplay starts.

Backend Changes:
- Extended games.py with create game, lineup submission, and game start endpoints
- Added teams.py roster endpoint with season filtering
- Enhanced SBA API client with player data fetching and caching
- Comprehensive validation for lineup submission (position conflicts, DH rules)

Frontend Changes:
- Redesigned create.vue with improved team selection and game options
- Enhanced index.vue with active/pending game filtering and navigation
- Added lineup/[id].vue for interactive lineup builder with drag-and-drop
- Implemented auth.client.ts plugin for client-side auth initialization
- Added comprehensive TypeScript types for API contracts
- Updated middleware for better auth handling

Key Features:
- Game creation with home/away team selection
- Full lineup builder with position assignment and batting order
- DH rule validation (pitcher can be excluded from batting order)
- Season-based roster filtering (Season 3)
- Auto-start game when both lineups submitted
- Real-time game list updates

Workflow:
1. Create game → select teams → set options
2. Submit home lineup → validate positions/order
3. Submit away lineup → validate positions/order
4. Game auto-starts → navigates to game page
5. WebSocket connection → loads game state

Ready for Phase F4 - connecting gameplay UI to complete the at-bat loop.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 23:57:03 -06:00

236 lines
7.7 KiB
Vue
Executable File

<template>
<div>
<div class="mb-8 flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">My Games</h1>
<p class="text-gray-600">
View and manage your active and completed games
</p>
</div>
<NuxtLink
to="/games/create"
class="px-6 py-3 bg-primary hover:bg-blue-700 text-white font-semibold rounded-lg shadow-lg hover:shadow-xl transition"
>
Create New Game
</NuxtLink>
</div>
<!-- Tabs -->
<div class="mb-6 border-b border-gray-200">
<nav class="flex space-x-8">
<button
@click="activeTab = 'active'"
:class="[
'py-4 px-1 border-b-2 font-medium text-sm transition',
activeTab === 'active'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
Active Games
</button>
<button
@click="activeTab = 'completed'"
:class="[
'py-4 px-1 border-b-2 font-medium text-sm transition',
activeTab === 'completed'
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]"
>
Completed
</button>
</nav>
</div>
<!-- Loading State -->
<div v-if="loading" class="bg-white rounded-lg shadow-md p-12 text-center">
<div class="w-16 h-16 mx-auto mb-4 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
<p class="text-gray-900 font-semibold">Loading games...</p>
</div>
<!-- Error State -->
<div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-6">
<p class="text-red-800 font-semibold">Failed to load games</p>
<p class="text-red-600 text-sm mt-2">{{ error }}</p>
<button
@click="fetchGames"
class="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
>
Retry
</button>
</div>
<!-- Games List -->
<div v-else-if="activeTab === 'active'">
<!-- Games List -->
<div v-if="activeGames.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<NuxtLink
v-for="game in activeGames"
:key="game.game_id"
:to="`/games/${game.game_id}`"
class="bg-white rounded-lg shadow-md hover:shadow-xl transition p-6"
>
<div class="flex justify-between items-start mb-4">
<span :class="[
'px-3 py-1 rounded-full text-sm font-semibold',
game.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
]">
{{ game.status === 'active' ? 'In Progress' : 'Pending Lineups' }}
</span>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-gray-600">Away</span>
<span class="font-bold">Team {{ game.away_team_id }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">Home</span>
<span class="font-bold">Team {{ game.home_team_id }}</span>
</div>
</div>
</NuxtLink>
</div>
<!-- Empty State -->
<div v-else class="bg-white rounded-lg shadow-md p-12 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 mx-auto text-gray-400 mb-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</svg>
<h3 class="text-xl font-bold text-gray-900 mb-2">
No Active Games
</h3>
<p class="text-gray-600 mb-6">
You don't have any active games right now. Create a new game to get started!
</p>
<NuxtLink
to="/games/create"
class="inline-block px-6 py-3 bg-primary hover:bg-blue-700 text-white font-semibold rounded-lg transition"
>
Create Your First Game
</NuxtLink>
</div>
</div>
<div v-else-if="activeTab === 'completed'">
<!-- Completed Games List -->
<div v-if="completedGames.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<NuxtLink
v-for="game in completedGames"
:key="game.game_id"
:to="`/games/${game.game_id}`"
class="bg-white rounded-lg shadow-md hover:shadow-xl transition p-6"
>
<div class="flex justify-between items-start mb-4">
<span class="px-3 py-1 rounded-full text-sm font-semibold bg-gray-100 text-gray-800">
Completed
</span>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-gray-600">Away</span>
<span class="font-bold">Team {{ game.away_team_id }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600">Home</span>
<span class="font-bold">Team {{ game.home_team_id }}</span>
</div>
</div>
</NuxtLink>
</div>
<!-- Empty State -->
<div v-else class="bg-white rounded-lg shadow-md p-12 text-center">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-16 mx-auto text-gray-400 mb-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
<h3 class="text-xl font-bold text-gray-900 mb-2">
No Completed Games
</h3>
<p class="text-gray-600">
You haven't completed any games yet.
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
definePageMeta({
middleware: ['auth'], // Require authentication
})
import { useAuthStore } from '~/store/auth'
const activeTab = ref<'active' | 'completed'>('active')
const config = useRuntimeConfig()
const authStore = useAuthStore()
// Games data
const games = ref<any[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
// Fetch games from API (client-side only)
async function fetchGames() {
try {
loading.value = true
error.value = null
const response = await $fetch<any[]>(`${config.public.apiUrl}/api/games/`, {
headers: {
Authorization: `Bearer ${authStore.token}`
}
})
games.value = response
console.log('[Games Page] Fetched games:', games.value)
} catch (err: any) {
console.error('[Games Page] Failed to fetch games:', err)
error.value = err.message || 'Failed to load games'
} finally {
loading.value = false
}
}
// Filter games by status
const activeGames = computed(() => {
return games.value?.filter(g => g.status === 'active' || g.status === 'pending') || []
})
const completedGames = computed(() => {
return games.value?.filter(g => g.status === 'completed' || g.status === 'final') || []
})
// Fetch on mount (client-side)
onMounted(() => {
fetchGames()
})
</script>
<style scoped>
/* Additional component styles if needed */
</style>