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(.claude/CLAUDE.md)",
"Bash(mcp__sqlite__describe_table:*)", "Bash(mcp__sqlite__describe_table:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(mcp__sqlite__list_tables:*)" "Bash(mcp__sqlite__list_tables:*)",
"Bash(mcp__sqlite__read_query:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []
}, },
"enableAllProjectMcpServers": true, "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 type { SessionPlan, SlotResult } from '@/db/schema/session-plans'
import { import {
useAbandonSession, useAbandonSession,
useActiveSessionPlan,
useApproveSessionPlan, useApproveSessionPlan,
useEndSessionEarly, useEndSessionEarly,
useGenerateSessionPlan, useGenerateSessionPlan,
@@ -122,13 +123,23 @@ export function StudentPracticeClient({
const endEarly = useEndSessionEarly() const endEarly = useEndSessionEarly()
const abandonSession = useAbandonSession() 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 = const currentPlan =
recordResult.data ?? recordResult.data ??
startPlan.data ?? startPlan.data ??
approvePlan.data ?? approvePlan.data ??
generatePlan.data ?? generatePlan.data ??
initialActiveSession ?? fetchedPlan ??
null null
// Derive error state from mutations // Derive error state from mutations
@@ -144,13 +155,16 @@ export function StudentPracticeClient({
// Derive view from session plan state - NO useState! // Derive view from session plan state - NO useState!
// This eliminates the "bastard state" problem where viewState and currentPlan could diverge // 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) return 'dashboard'
if (currentPlan.completedAt) return 'summary' if (currentPlan.completedAt) return 'summary'
if (currentPlan.startedAt) return 'practicing' if (currentPlan.startedAt) return 'practicing'
if (currentPlan.approvedAt) return 'reviewing' if (currentPlan.approvedAt) return 'reviewing'
return 'continue' // Plan exists but not yet approved return 'continue' // Plan exists but not yet approved
}, [currentPlan]) }, [currentPlan, isPlanLoading])
// Handle continue practice - navigate to configuration page // Handle continue practice - navigate to configuration page
const handleContinuePractice = useCallback(() => { const handleContinuePractice = useCallback(() => {
@@ -437,6 +451,29 @@ export function StudentPracticeClient({
)} )}
{/* Content based on session view (derived from data) */} {/* 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 && ( {sessionView === 'continue' && currentPlan && (
<ContinueSessionCard <ContinueSessionCard
studentName={selectedStudent.name} studentName={selectedStudent.name}

View File

@@ -92,12 +92,24 @@ async function updateSessionPlan({
/** /**
* Hook: Fetch active session plan for a player * 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({ return useQuery({
queryKey: sessionPlanKeys.active(playerId ?? ''), queryKey: sessionPlanKeys.active(playerId ?? ''),
queryFn: () => fetchActiveSessionPlan(playerId!), queryFn: () => fetchActiveSessionPlan(playerId!),
enabled: !!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
}) })
} }