Implements schedule viewing from SBA production API with week navigation and game creation from scheduled matchups. Groups games by team matchup horizontally with games stacked vertically for space efficiency. Backend: - Add schedule routes (/api/schedule/current, /api/schedule/games) - Add SBA API client methods for schedule data - Fix multi-worker state isolation (single worker for in-memory state) - Add Redis migration TODO for future scalability - Support custom team IDs in quick-create endpoint Frontend: - Add Schedule tab as default on home page - Week navigation with prev/next and "Current Week" jump - Horizontal group layout (2-6 columns responsive) - Completed games show score + "Final" badge (no Play button) - Incomplete games show "Play" button to create webapp game Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
192 lines
4.7 KiB
TypeScript
192 lines
4.7 KiB
TypeScript
/**
|
|
* Schedule Composable
|
|
*
|
|
* Manages SBA schedule data fetching and week navigation.
|
|
* Fetches current season/week and scheduled games from SBA API.
|
|
*/
|
|
|
|
import type { SbaCurrent, SbaScheduledGame } from '~/types'
|
|
|
|
export function useSchedule() {
|
|
const config = useRuntimeConfig()
|
|
const apiUrl = useApiUrl()
|
|
|
|
// State - use useState for SSR-safe persistence across hydration
|
|
// Regular ref() creates new state on each call, breaking after hydration
|
|
const current = useState<SbaCurrent | null>('schedule-current', () => null)
|
|
const selectedSeason = useState<number>('schedule-season', () => 0)
|
|
const selectedWeek = useState<number>('schedule-week', () => 0)
|
|
const games = useState<SbaScheduledGame[]>('schedule-games', () => [])
|
|
const loading = useState<boolean>('schedule-loading', () => false)
|
|
const error = useState<string | null>('schedule-error', () => null)
|
|
|
|
// Track if we've initialized
|
|
const initialized = useState<boolean>('schedule-initialized', () => false)
|
|
|
|
/**
|
|
* Get headers for SSR-compatible requests
|
|
*/
|
|
function getHeaders(): Record<string, string> {
|
|
const headers: Record<string, string> = {}
|
|
if (import.meta.server) {
|
|
const event = useRequestEvent()
|
|
const cookieHeader = event?.node.req.headers.cookie
|
|
if (cookieHeader) {
|
|
headers['Cookie'] = cookieHeader
|
|
}
|
|
}
|
|
return headers
|
|
}
|
|
|
|
/**
|
|
* Fetch current season and week from SBA API
|
|
*/
|
|
async function fetchCurrent(): Promise<void> {
|
|
try {
|
|
error.value = null
|
|
const response = await $fetch<SbaCurrent>(`${apiUrl}/api/schedule/current`, {
|
|
credentials: 'include',
|
|
headers: getHeaders(),
|
|
})
|
|
|
|
current.value = response
|
|
// Initialize selected to current if not already set
|
|
if (!initialized.value) {
|
|
selectedSeason.value = response.season
|
|
selectedWeek.value = response.week
|
|
initialized.value = true
|
|
}
|
|
|
|
console.log('[useSchedule] Current:', response)
|
|
} catch (err: any) {
|
|
console.error('[useSchedule] Failed to fetch current:', err)
|
|
error.value = err.data?.detail || err.message || 'Failed to fetch current season/week'
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch games for the selected week
|
|
*/
|
|
async function fetchGames(): Promise<void> {
|
|
if (!selectedSeason.value || !selectedWeek.value) {
|
|
console.warn('[useSchedule] No season/week selected')
|
|
return
|
|
}
|
|
|
|
try {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
const response = await $fetch<SbaScheduledGame[]>(
|
|
`${apiUrl}/api/schedule/games`,
|
|
{
|
|
credentials: 'include',
|
|
headers: getHeaders(),
|
|
params: {
|
|
season: selectedSeason.value,
|
|
week: selectedWeek.value,
|
|
},
|
|
}
|
|
)
|
|
|
|
games.value = response
|
|
console.log(`[useSchedule] Loaded ${response.length} games for S${selectedSeason.value} W${selectedWeek.value}`)
|
|
} catch (err: any) {
|
|
console.error('[useSchedule] Failed to fetch games:', err)
|
|
error.value = err.data?.detail || err.message || 'Failed to fetch schedule games'
|
|
games.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize schedule data (fetch current + games)
|
|
*/
|
|
async function initialize(): Promise<void> {
|
|
try {
|
|
loading.value = true
|
|
await fetchCurrent()
|
|
await fetchGames()
|
|
} catch {
|
|
// Error already set in fetchCurrent
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Navigate to next week
|
|
*/
|
|
function nextWeek(): void {
|
|
selectedWeek.value++
|
|
fetchGames()
|
|
}
|
|
|
|
/**
|
|
* Navigate to previous week
|
|
*/
|
|
function prevWeek(): void {
|
|
if (selectedWeek.value > 1) {
|
|
selectedWeek.value--
|
|
fetchGames()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Jump back to current week
|
|
*/
|
|
function goToCurrentWeek(): void {
|
|
if (current.value) {
|
|
selectedSeason.value = current.value.season
|
|
selectedWeek.value = current.value.week
|
|
fetchGames()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set specific week (useful for direct navigation)
|
|
*/
|
|
function setWeek(week: number): void {
|
|
if (week >= 1) {
|
|
selectedWeek.value = week
|
|
fetchGames()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if currently viewing the current week
|
|
*/
|
|
const isCurrentWeek = computed(() => {
|
|
if (!current.value) return false
|
|
return (
|
|
selectedSeason.value === current.value.season &&
|
|
selectedWeek.value === current.value.week
|
|
)
|
|
})
|
|
|
|
return {
|
|
// State - return refs directly for proper reactivity in templates
|
|
current,
|
|
selectedSeason,
|
|
selectedWeek,
|
|
games,
|
|
loading,
|
|
error,
|
|
initialized,
|
|
|
|
// Computed
|
|
isCurrentWeek,
|
|
|
|
// Actions
|
|
initialize,
|
|
fetchCurrent,
|
|
fetchGames,
|
|
nextWeek,
|
|
prevWeek,
|
|
goToCurrentWeek,
|
|
setWeek,
|
|
}
|
|
}
|