strat-gameplay-webapp/frontend-sba/composables/useSchedule.ts
Cal Corum fbbb1cc5da CLAUDE: Add SBA schedule integration with weekly matchup display
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>
2026-01-14 23:39:31 -06:00

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,
}
}