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:
parent
8a9afa86bc
commit
7243502873
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue