fix(practice): make session plan page self-sufficient for data loading

- Update useActiveSessionPlan to accept initialData from server props
- Page now fetches its own data if cache is empty (no abstraction hole)
- Three loading scenarios handled:
  1. Cache populated (from ConfigureClient mutation): instant display
  2. Cache miss: fetches from API with loading state
  3. Direct page load: uses server props as initialData
- Add loading view while fetching session plan

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-12-09 11:52:52 -06:00
parent 8a9afa86bc
commit 7243502873
3 changed files with 59 additions and 7 deletions

View File

@ -133,11 +133,14 @@
"Bash(.claude/CLAUDE.md)",
"Bash(mcp__sqlite__describe_table:*)",
"Bash(ls:*)",
"Bash(mcp__sqlite__list_tables:*)"
"Bash(mcp__sqlite__list_tables:*)",
"Bash(mcp__sqlite__read_query:*)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["sqlite"]
"enabledMcpjsonServers": [
"sqlite"
]
}

View File

@ -27,6 +27,7 @@ import type { PracticeSession } from '@/db/schema/practice-sessions'
import type { SessionPlan, SlotResult } from '@/db/schema/session-plans'
import {
useAbandonSession,
useActiveSessionPlan,
useApproveSessionPlan,
useEndSessionEarly,
useGenerateSessionPlan,
@ -122,13 +123,23 @@ export function StudentPracticeClient({
const endEarly = useEndSessionEarly()
const abandonSession = useAbandonSession()
// Current plan from mutations or initial active session (use the latest successful result)
// Fetch active session plan from cache or API
// - If cache has data (from ConfigureClient mutation): uses cache immediately
// - If no cache but initialActiveSession exists: uses server props as initial data
// - If neither: fetches from API (shows loading state briefly)
const {
data: fetchedPlan,
isLoading: isPlanLoading,
} = useActiveSessionPlan(studentId, initialActiveSession)
// Current plan from mutations or fetched data (priority order)
// Mutations take priority (most recent user action), then fetched/cached data
const currentPlan =
recordResult.data ??
startPlan.data ??
approvePlan.data ??
generatePlan.data ??
initialActiveSession ??
fetchedPlan ??
null
// Derive error state from mutations
@ -144,13 +155,16 @@ export function StudentPracticeClient({
// Derive view from session plan state - NO useState!
// This eliminates the "bastard state" problem where viewState and currentPlan could diverge
const sessionView: SessionView = useMemo(() => {
const sessionView: SessionView | 'loading' = useMemo(() => {
// Show loading only if we're fetching AND don't have any data yet
// (mutations or initial data would give us something to show)
if (isPlanLoading && !currentPlan) return 'loading'
if (!currentPlan) return 'dashboard'
if (currentPlan.completedAt) return 'summary'
if (currentPlan.startedAt) return 'practicing'
if (currentPlan.approvedAt) return 'reviewing'
return 'continue' // Plan exists but not yet approved
}, [currentPlan])
}, [currentPlan, isPlanLoading])
// Handle continue practice - navigate to configuration page
const handleContinuePractice = useCallback(() => {
@ -437,6 +451,29 @@ export function StudentPracticeClient({
)}
{/* Content based on session view (derived from data) */}
{sessionView === 'loading' && (
<div
data-section="loading"
className={css({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '3rem',
gap: '1rem',
})}
>
<div
className={css({
fontSize: '2rem',
animation: 'pulse 1.5s ease-in-out infinite',
})}
>
Loading practice session...
</div>
</div>
)}
{sessionView === 'continue' && currentPlan && (
<ContinueSessionCard
studentName={selectedStudent.name}

View File

@ -92,12 +92,24 @@ async function updateSessionPlan({
/**
* Hook: Fetch active session plan for a player
*
* @param playerId - The player ID to fetch the session for
* @param initialData - Optional initial data from server-side props (avoids loading state on direct page load)
*/
export function useActiveSessionPlan(playerId: string | null) {
export function useActiveSessionPlan(
playerId: string | null,
initialData?: SessionPlan | null
) {
return useQuery({
queryKey: sessionPlanKeys.active(playerId ?? ''),
queryFn: () => fetchActiveSessionPlan(playerId!),
enabled: !!playerId,
// Use server-provided data as initial cache value
// This prevents a loading flash on direct page loads while still allowing refetch
initialData: initialData ?? undefined,
// Don't refetch on mount if we have initial data - trust the server
// The query will still refetch on window focus or after stale time
staleTime: initialData ? 30000 : 0, // 30s stale time if we have initial data
})
}