CLAUDE: Move games list to index page for better UX
SBA users are already league members - they don't need a marketing page. Moving the games list to "/" reduces friction by eliminating a redirect. Changes: - pages/index.vue: Now shows games list (was marketing/dashboard) - pages/games/index.vue: Redirects to / for backwards compatibility - Updated all internal links from /games to / - Auth callback redirects to / after login User flow is now: - Not logged in: / → auth middleware → Discord OAuth → / - Logged in: / shows games list directly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
39cea607dd
commit
581bc33f15
@ -17,7 +17,7 @@
|
|||||||
<template v-if="authStore.isAuthenticated">
|
<template v-if="authStore.isAuthenticated">
|
||||||
<!-- Authenticated Navigation -->
|
<!-- Authenticated Navigation -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/games"
|
to="/"
|
||||||
class="hover:text-gray-200 transition text-sm font-medium"
|
class="hover:text-gray-200 transition text-sm font-medium"
|
||||||
>
|
>
|
||||||
Games
|
Games
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<!-- Back to Games -->
|
<!-- Back to Games -->
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/games"
|
to="/"
|
||||||
class="flex items-center space-x-2 hover:text-gray-300 transition text-sm"
|
class="flex items-center space-x-2 hover:text-gray-300 transition text-sm"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@ -119,11 +119,11 @@ onMounted(async () => {
|
|||||||
if (isAuth) {
|
if (isAuth) {
|
||||||
success.value = true
|
success.value = true
|
||||||
|
|
||||||
// Short delay to show success message, then redirect to /games
|
// Short delay to show success message, then redirect to games list
|
||||||
// Using navigateTo with external:false ensures proper client-side navigation
|
// Using navigateTo with external:false ensures proper client-side navigation
|
||||||
// which carries the authenticated state
|
// which carries the authenticated state
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigateTo('/games', { replace: true })
|
navigateTo('/', { replace: true })
|
||||||
}, 500)
|
}, 500)
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'Authentication verification failed. Please try again.'
|
errorMessage.value = 'Authentication verification failed. Please try again.'
|
||||||
|
|||||||
@ -299,7 +299,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
class="px-6 py-3 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition shadow-md"
|
class="px-6 py-3 bg-primary hover:bg-blue-700 text-white rounded-lg font-semibold transition shadow-md"
|
||||||
@click="navigateTo('/games')"
|
@click="navigateTo('/')"
|
||||||
>
|
>
|
||||||
Back to Games
|
Back to Games
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -86,7 +86,7 @@
|
|||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
to="/games"
|
to="/"
|
||||||
class="px-6 py-3 text-gray-700 hover:text-gray-900 font-medium transition"
|
class="px-6 py-3 text-gray-700 hover:text-gray-900 font-medium transition"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@ -1,319 +1,16 @@
|
|||||||
<template>
|
|
||||||
<div class="bg-white rounded-xl shadow-md border border-gray-200 p-6">
|
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
||||||
<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>
|
|
||||||
<div class="flex flex-wrap gap-3">
|
|
||||||
<button
|
|
||||||
@click="handleQuickCreate"
|
|
||||||
:disabled="isCreatingQuickGame"
|
|
||||||
class="px-5 py-2.5 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white font-medium rounded-lg shadow hover:shadow-md transition disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{{ isCreatingQuickGame ? 'Creating...' : 'Quick Start Demo' }}
|
|
||||||
</button>
|
|
||||||
<NuxtLink
|
|
||||||
to="/games/create"
|
|
||||||
class="px-5 py-2.5 bg-primary hover:bg-blue-700 text-white font-medium rounded-lg shadow hover:shadow-md transition"
|
|
||||||
>
|
|
||||||
Create New Game
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabs -->
|
|
||||||
<div class="mb-6 border-b border-gray-200">
|
|
||||||
<nav class="flex space-x-8">
|
|
||||||
<button
|
|
||||||
: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'
|
|
||||||
]"
|
|
||||||
@click="activeTab = 'active'"
|
|
||||||
>
|
|
||||||
Active Games
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
: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'
|
|
||||||
]"
|
|
||||||
@click="activeTab = 'completed'"
|
|
||||||
>
|
|
||||||
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"/>
|
|
||||||
<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
|
|
||||||
class="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
|
||||||
@click="refresh"
|
|
||||||
>
|
|
||||||
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>
|
|
||||||
<!-- Inning indicator for active games -->
|
|
||||||
<span v-if="game.status === 'active' && game.inning" class="text-sm text-gray-500">
|
|
||||||
{{ game.half === 'top' ? 'Top' : 'Bot' }} {{ game.inning }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- Score display -->
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-xs text-gray-400 w-8">Away</span>
|
|
||||||
<span class="font-semibold">{{ game.away_team_name || game.away_team_abbrev || `Team ${game.away_team_id}` }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xl font-bold tabular-nums">{{ game.away_score }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-xs text-gray-400 w-8">Home</span>
|
|
||||||
<span class="font-semibold">{{ game.home_team_name || game.home_team_abbrev || `Team ${game.home_team_id}` }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xl font-bold tabular-nums">{{ game.home_score }}</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">
|
|
||||||
Final
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- Score display -->
|
|
||||||
<div class="space-y-3">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-xs text-gray-400 w-8">Away</span>
|
|
||||||
<span class="font-semibold">{{ game.away_team_name || game.away_team_abbrev || `Team ${game.away_team_id}` }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xl font-bold tabular-nums">{{ game.away_score }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span class="text-xs text-gray-400 w-8">Home</span>
|
|
||||||
<span class="font-semibold">{{ game.home_team_name || game.home_team_abbrev || `Team ${game.home_team_id}` }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="text-xl font-bold tabular-nums">{{ game.home_score }}</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">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '~/store/auth'
|
// Redirect /games to / for backwards compatibility
|
||||||
|
// The games list is now at the index page
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['auth'], // Require authentication
|
middleware: ['auth'],
|
||||||
})
|
})
|
||||||
|
|
||||||
const activeTab = ref<'active' | 'completed'>('active')
|
// Redirect immediately
|
||||||
const config = useRuntimeConfig()
|
navigateTo('/', { replace: true })
|
||||||
const authStore = useAuthStore()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// Games data - loading/error managed separately, games comes from useAsyncData
|
|
||||||
const loading = ref(true)
|
|
||||||
const error = ref<string | null>(null)
|
|
||||||
const isCreatingQuickGame = ref(false)
|
|
||||||
|
|
||||||
// Quick-create a demo game with pre-configured lineups
|
|
||||||
async function handleQuickCreate() {
|
|
||||||
try {
|
|
||||||
isCreatingQuickGame.value = true
|
|
||||||
error.value = null
|
|
||||||
|
|
||||||
console.log('[Games Page] Quick-creating demo game...')
|
|
||||||
|
|
||||||
const response = await $fetch<{ game_id: string; message: string; status: string }>(
|
|
||||||
`${config.public.apiUrl}/api/games/quick-create`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
credentials: 'include', // Send HttpOnly cookies
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('[Games Page] Quick-create response:', response)
|
|
||||||
|
|
||||||
// Redirect to game page
|
|
||||||
router.push(`/games/${response.game_id}`)
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error('[Games Page] Quick-create failed:', err)
|
|
||||||
error.value = err.data?.detail || err.message || 'Failed to create demo game'
|
|
||||||
} finally {
|
|
||||||
isCreatingQuickGame.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch games - uses internal URL for SSR, public URL for client
|
|
||||||
const apiUrl = useApiUrl()
|
|
||||||
const { data: games, pending, error: fetchError, refresh } = await useAsyncData(
|
|
||||||
'games-list',
|
|
||||||
async () => {
|
|
||||||
const headers: Record<string, string> = {}
|
|
||||||
|
|
||||||
// Forward cookies for SSR requests
|
|
||||||
if (import.meta.server) {
|
|
||||||
const event = useRequestEvent()
|
|
||||||
const cookieHeader = event?.node.req.headers.cookie
|
|
||||||
if (cookieHeader) {
|
|
||||||
headers['Cookie'] = cookieHeader
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await $fetch<any[]>(`${apiUrl}/api/games/`, {
|
|
||||||
credentials: 'include',
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
return response
|
|
||||||
},
|
|
||||||
{
|
|
||||||
server: true, // SSR enabled - uses internal Docker URL
|
|
||||||
lazy: false, // Block rendering until done
|
|
||||||
default: () => [] as any[], // Prevent null during hydration
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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') || []
|
|
||||||
})
|
|
||||||
|
|
||||||
// Re-fetch on client if data is stale (handles post-OAuth client-side navigation)
|
|
||||||
onMounted(async () => {
|
|
||||||
// Only re-fetch if we have no games but are authenticated
|
|
||||||
// This handles the case where client-side navigation doesn't trigger SSR
|
|
||||||
if ((!games.value || games.value.length === 0) && authStore.isAuthenticated) {
|
|
||||||
console.log('[Games Page] No games on mount, re-fetching...')
|
|
||||||
await refresh()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sync loading state with pending
|
|
||||||
watch(pending, (isPending) => {
|
|
||||||
loading.value = isPending
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
// Sync error state
|
|
||||||
watch(fetchError, (err) => {
|
|
||||||
if (err) {
|
|
||||||
error.value = err.message || 'Failed to load games'
|
|
||||||
}
|
|
||||||
}, { immediate: true })
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<template>
|
||||||
/* Additional component styles if needed */
|
<div class="flex items-center justify-center min-h-[50vh]">
|
||||||
</style>
|
<div class="text-gray-500">Redirecting...</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@ -1,177 +1,207 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="bg-white rounded-xl shadow-md border border-gray-200 p-6">
|
||||||
<!-- Loading state during SSR/hydration -->
|
<!-- Page Header -->
|
||||||
<div v-if="!isMounted" class="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100 flex items-center justify-center">
|
<div class="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
<div class="text-gray-500">Loading...</div>
|
<div>
|
||||||
</div>
|
<h1 class="text-3xl font-bold text-gray-900 mb-2">My Games</h1>
|
||||||
|
<p class="text-gray-600">
|
||||||
<!-- Guest View: Landing Page -->
|
View and manage your active and completed games
|
||||||
<div v-else-if="!authStore.isAuthenticated" class="min-h-screen bg-gradient-to-br from-blue-50 to-blue-100">
|
</p>
|
||||||
<!-- Hero Section -->
|
</div>
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
<div class="flex flex-wrap gap-3">
|
||||||
<div class="text-center">
|
<button
|
||||||
<h1 class="text-5xl font-bold text-gray-900 mb-4">
|
@click="handleQuickCreate"
|
||||||
Welcome to <span class="text-primary">SBa</span>
|
:disabled="isCreatingQuickGame"
|
||||||
</h1>
|
class="px-5 py-2.5 bg-green-600 hover:bg-green-700 disabled:bg-gray-400 text-white font-medium rounded-lg shadow hover:shadow-md transition disabled:cursor-not-allowed"
|
||||||
<p class="text-xl text-gray-600 mb-8 max-w-2xl mx-auto">
|
>
|
||||||
Experience the thrill of Strat-O-Matic Baseball in real-time.
|
{{ isCreatingQuickGame ? 'Creating...' : 'Quick Start Demo' }}
|
||||||
Manage your team, make strategic decisions, and compete with friends.
|
</button>
|
||||||
</p>
|
<NuxtLink
|
||||||
<a
|
to="/games/create"
|
||||||
:href="discordLoginUrl"
|
class="px-5 py-2.5 bg-primary hover:bg-blue-700 text-white font-medium rounded-lg shadow hover:shadow-md transition"
|
||||||
class="inline-block px-8 py-4 bg-primary hover:bg-blue-700 text-white font-bold text-lg rounded-lg shadow-lg hover:shadow-xl transition transform hover:-translate-y-0.5 no-underline"
|
>
|
||||||
>
|
Create New Game
|
||||||
Sign in with Discord
|
</NuxtLink>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Features Grid -->
|
|
||||||
<div class="mt-24 grid md:grid-cols-3 gap-8">
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-8 text-center hover:shadow-lg transition">
|
|
||||||
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Real-Time Gameplay</h3>
|
|
||||||
<p class="text-gray-600">
|
|
||||||
Live WebSocket updates keep you in sync with every pitch, swing, and strategic decision.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-8 text-center hover:shadow-lg transition">
|
|
||||||
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Strategic Depth</h3>
|
|
||||||
<p class="text-gray-600">
|
|
||||||
Defensive positioning, substitutions, and tactical decisions - you control every aspect.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-8 text-center hover:shadow-lg transition">
|
|
||||||
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-xl font-bold text-gray-900 mb-2">Multiplayer</h3>
|
|
||||||
<p class="text-gray-600">
|
|
||||||
Compete head-to-head with friends or challenge the AI opponent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Authenticated View: Dashboard -->
|
<!-- Tabs -->
|
||||||
<div v-else class="min-h-screen">
|
<div class="mb-6 border-b border-gray-200">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
<nav class="flex space-x-8">
|
||||||
<!-- Welcome Header -->
|
<button
|
||||||
<div class="mb-8">
|
:class="[
|
||||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">
|
'py-4 px-1 border-b-2 font-medium text-sm transition',
|
||||||
Welcome back, {{ authStore.currentUser?.username || 'Manager' }}!
|
activeTab === 'active'
|
||||||
</h1>
|
? 'border-primary text-primary'
|
||||||
<p class="text-gray-600">
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
Ready to lead your team to victory?
|
]"
|
||||||
</p>
|
@click="activeTab = 'active'"
|
||||||
</div>
|
>
|
||||||
|
Active Games
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
: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'
|
||||||
|
]"
|
||||||
|
@click="activeTab = 'completed'"
|
||||||
|
>
|
||||||
|
Completed
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Loading State -->
|
||||||
<div class="grid md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
<div v-if="loading" class="bg-white rounded-lg shadow-md p-12 text-center">
|
||||||
<NuxtLink
|
<div class="w-16 h-16 mx-auto mb-4 border-4 border-primary border-t-transparent rounded-full animate-spin"/>
|
||||||
to="/games/create"
|
<p class="text-gray-900 font-semibold">Loading games...</p>
|
||||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition cursor-pointer border-2 border-transparent hover:border-primary"
|
</div>
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-bold text-gray-900 mb-1">New Game</h3>
|
|
||||||
<p class="text-sm text-gray-600">Start a fresh matchup</p>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<NuxtLink
|
<!-- Error State -->
|
||||||
to="/games"
|
<div v-else-if="error" class="bg-red-50 border border-red-200 rounded-lg p-6">
|
||||||
class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition cursor-pointer border-2 border-transparent hover:border-primary"
|
<p class="text-red-800 font-semibold">Failed to load games</p>
|
||||||
>
|
<p class="text-red-600 text-sm mt-2">{{ error }}</p>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<button
|
||||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
class="mt-4 px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition"
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
@click="() => refresh()"
|
||||||
<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>
|
Retry
|
||||||
</div>
|
</button>
|
||||||
<span class="text-2xl font-bold text-gray-900">0</span>
|
</div>
|
||||||
</div>
|
|
||||||
<h3 class="text-lg font-bold text-gray-900 mb-1">Active Games</h3>
|
|
||||||
<p class="text-sm text-gray-600">Your ongoing matches</p>
|
|
||||||
</NuxtLink>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<!-- Games List -->
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div v-else-if="activeTab === 'active'">
|
||||||
<div class="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center">
|
<!-- Games List -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div v-if="activeGames.length > 0" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
|
<NuxtLink
|
||||||
</svg>
|
v-for="game in activeGames"
|
||||||
</div>
|
:key="game.game_id"
|
||||||
<span class="text-2xl font-bold text-gray-900">0</span>
|
:to="`/games/${game.game_id}`"
|
||||||
</div>
|
class="bg-white rounded-lg shadow-md hover:shadow-xl transition p-6"
|
||||||
<h3 class="text-lg font-bold text-gray-900 mb-1">Wins</h3>
|
>
|
||||||
<p class="text-sm text-gray-600">Season record</p>
|
<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>
|
||||||
|
<!-- Inning indicator for active games -->
|
||||||
|
<span v-if="game.status === 'active' && game.inning" class="text-sm text-gray-500">
|
||||||
|
{{ game.half === 'top' ? 'Top' : 'Bot' }} {{ game.inning }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Score display -->
|
||||||
<div class="bg-white rounded-lg shadow-md p-6">
|
<div class="space-y-3">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between">
|
||||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
<div class="flex items-center gap-2">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<span class="text-xs text-gray-400 w-8">Away</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<span class="font-semibold">{{ game.away_team_name || game.away_team_abbrev || `Team ${game.away_team_id}` }}</span>
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="text-2xl font-bold text-gray-900">-</span>
|
<span class="text-xl font-bold tabular-nums">{{ game.away_score }}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-bold text-gray-900 mb-1">Last Played</h3>
|
<div class="flex items-center justify-between">
|
||||||
<p class="text-sm text-gray-600">Never</p>
|
<div class="flex items-center gap-2">
|
||||||
</div>
|
<span class="text-xs text-gray-400 w-8">Home</span>
|
||||||
</div>
|
<span class="font-semibold">{{ game.home_team_name || game.home_team_abbrev || `Team ${game.home_team_id}` }}</span>
|
||||||
|
|
||||||
<!-- Recent Activity / Getting Started -->
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-8">
|
|
||||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Getting Started</h2>
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center font-bold">
|
|
||||||
1
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<h3 class="font-semibold text-gray-900">Create Your First Game</h3>
|
|
||||||
<p class="text-gray-600">Click "New Game" to set up your first matchup and choose your teams.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-gray-300 text-white rounded-full flex items-center justify-center font-bold">
|
|
||||||
2
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<h3 class="font-semibold text-gray-900">Make Strategic Decisions</h3>
|
|
||||||
<p class="text-gray-600">Control defensive positioning, batting order, and substitutions throughout the game.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-shrink-0 w-8 h-8 bg-gray-300 text-white rounded-full flex items-center justify-center font-bold">
|
|
||||||
3
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<h3 class="font-semibold text-gray-900">Watch Your Team Win</h3>
|
|
||||||
<p class="text-gray-600">Follow the action in real-time as plays unfold and your strategy comes to life.</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="text-xl font-bold tabular-nums">{{ game.home_score }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
||||||
|
Final
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- Score display -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-400 w-8">Away</span>
|
||||||
|
<span class="font-semibold">{{ game.away_team_name || game.away_team_abbrev || `Team ${game.away_team_id}` }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xl font-bold tabular-nums">{{ game.away_score }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-gray-400 w-8">Home</span>
|
||||||
|
<span class="font-semibold">{{ game.home_team_name || game.home_team_abbrev || `Team ${game.home_team_id}` }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-xl font-bold tabular-nums">{{ game.home_score }}</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -180,23 +210,110 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '~/store/auth'
|
import { useAuthStore } from '~/store/auth'
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
definePageMeta({
|
||||||
const config = useRuntimeConfig()
|
middleware: ['auth'], // Require authentication
|
||||||
|
|
||||||
// Track if we're mounted (client-side) to avoid SSR hydration mismatch
|
|
||||||
const isMounted = ref(false)
|
|
||||||
|
|
||||||
// Compute Discord login URL for direct OAuth (bypasses /auth/login page)
|
|
||||||
const discordLoginUrl = computed(() => {
|
|
||||||
return `${config.public.apiUrl}/api/auth/discord/login?return_url=${encodeURIComponent('/games')}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
const activeTab = ref<'active' | 'completed'>('active')
|
||||||
isMounted.value = true
|
const config = useRuntimeConfig()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
// Auto-redirect authenticated users to /games
|
// Games data - loading/error managed separately, games comes from useAsyncData
|
||||||
if (authStore.isAuthenticated) {
|
const loading = ref(true)
|
||||||
navigateTo('/games')
|
const error = ref<string | null>(null)
|
||||||
|
const isCreatingQuickGame = ref(false)
|
||||||
|
|
||||||
|
// Quick-create a demo game with pre-configured lineups
|
||||||
|
async function handleQuickCreate() {
|
||||||
|
try {
|
||||||
|
isCreatingQuickGame.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
console.log('[Games Page] Quick-creating demo game...')
|
||||||
|
|
||||||
|
const response = await $fetch<{ game_id: string; message: string; status: string }>(
|
||||||
|
`${config.public.apiUrl}/api/games/quick-create`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include', // Send HttpOnly cookies
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('[Games Page] Quick-create response:', response)
|
||||||
|
|
||||||
|
// Redirect to game page
|
||||||
|
router.push(`/games/${response.game_id}`)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[Games Page] Quick-create failed:', err)
|
||||||
|
error.value = err.data?.detail || err.message || 'Failed to create demo game'
|
||||||
|
} finally {
|
||||||
|
isCreatingQuickGame.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch games - uses internal URL for SSR, public URL for client
|
||||||
|
const apiUrl = useApiUrl()
|
||||||
|
const { data: games, pending, error: fetchError, refresh } = await useAsyncData(
|
||||||
|
'games-list',
|
||||||
|
async () => {
|
||||||
|
const headers: Record<string, string> = {}
|
||||||
|
|
||||||
|
// Forward cookies for SSR requests
|
||||||
|
if (import.meta.server) {
|
||||||
|
const event = useRequestEvent()
|
||||||
|
const cookieHeader = event?.node.req.headers.cookie
|
||||||
|
if (cookieHeader) {
|
||||||
|
headers['Cookie'] = cookieHeader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await $fetch<any[]>(`${apiUrl}/api/games/`, {
|
||||||
|
credentials: 'include',
|
||||||
|
headers,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response
|
||||||
|
},
|
||||||
|
{
|
||||||
|
server: true, // SSR enabled - uses internal Docker URL
|
||||||
|
lazy: false, // Block rendering until done
|
||||||
|
default: () => [] as any[], // Prevent null during hydration
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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') || []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Re-fetch on client if data is stale (handles post-OAuth client-side navigation)
|
||||||
|
onMounted(async () => {
|
||||||
|
// Only re-fetch if we have no games but are authenticated
|
||||||
|
// This handles the case where client-side navigation doesn't trigger SSR
|
||||||
|
if ((!games.value || games.value.length === 0) && authStore.isAuthenticated) {
|
||||||
|
console.log('[Games Page] No games on mount, re-fetching...')
|
||||||
|
await refresh()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sync loading state with pending
|
||||||
|
watch(pending, (isPending) => {
|
||||||
|
loading.value = isPending
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Sync error state
|
||||||
|
watch(fetchError, (err) => {
|
||||||
|
if (err) {
|
||||||
|
error.value = err.message || 'Failed to load games'
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Additional component styles if needed */
|
||||||
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user