feat(practice): add /resume route for "Welcome back" experience

- Create /practice/[studentId]/resume route for returning students
- Student selector navigates to /resume instead of main practice page
- /resume shows "Welcome back" card with session progress
- Clicking "Continue" navigates to /practice/[studentId] (goes straight to practice)
- Clicking "Start Fresh" abandons session and goes to /configure
- Main practice page no longer shows welcome card (goes straight to practicing)
- Reloading mid-session stays in practice (no welcome card)

🤖 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 12:00:01 -06:00
parent 7243502873
commit 7b476e80c1
4 changed files with 164 additions and 6 deletions

View File

@ -35,10 +35,11 @@ export function PracticeClient({ initialPlayers }: PracticeClientProps) {
createdAt: player.createdAt,
}))
// Handle student selection - navigate to student's practice page
// Handle student selection - navigate to student's resume page
// The /resume route shows "Welcome back" for in-progress sessions
const handleSelectStudent = useCallback(
(student: StudentWithProgress) => {
router.push(`/practice/${student.id}`, { scroll: false })
router.push(`/practice/${student.id}/resume`, { scroll: false })
},
[router]
)

View File

@ -163,7 +163,7 @@ export function StudentPracticeClient({
if (currentPlan.completedAt) return 'summary'
if (currentPlan.startedAt) return 'practicing'
if (currentPlan.approvedAt) return 'reviewing'
return 'continue' // Plan exists but not yet approved
return 'continue' // Plan exists but not yet approved (draft)
}, [currentPlan, isPlanLoading])
// Handle continue practice - navigate to configuration page
@ -175,9 +175,9 @@ export function StudentPracticeClient({
const handleResumeSession = useCallback(() => {
if (!currentPlan) return
// Session already started → view will show 'practicing' automatically
// Session already started → navigate to main practice page (no ?returning)
if (currentPlan.startedAt) {
// No action needed - sessionView is already 'practicing'
router.push(`/practice/${studentId}`, { scroll: false })
return
}
@ -190,7 +190,7 @@ export function StudentPracticeClient({
// Draft (not approved) → need to approve it first
// This will update sessionView to 'reviewing'
approvePlan.mutate({ playerId: studentId, planId: currentPlan.id })
}, [currentPlan, studentId, startPlan, approvePlan])
}, [currentPlan, studentId, startPlan, approvePlan, router])
// Handle starting fresh (abandon current session)
const handleStartFresh = useCallback(() => {

View File

@ -0,0 +1,106 @@
'use client'
import { useCallback } from 'react'
import { useRouter } from 'next/navigation'
import { PageWithNav } from '@/components/PageWithNav'
import { ContinueSessionCard } from '@/components/practice'
import { useTheme } from '@/contexts/ThemeContext'
import type { Player } from '@/db/schema/players'
import type { SessionPlan } from '@/db/schema/session-plans'
import { useAbandonSession } from '@/hooks/useSessionPlan'
import { css } from '../../../../../styled-system/css'
interface ResumeClientProps {
studentId: string
player: Player
session: SessionPlan
}
/**
* Client component for the Resume page
*
* Shows the "Welcome back" card for students returning to an in-progress session.
*/
export function ResumeClient({ studentId, player, session }: ResumeClientProps) {
const router = useRouter()
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme === 'dark'
const abandonSession = useAbandonSession()
// Handle continuing the session - navigate to main practice page
const handleContinue = useCallback(() => {
router.push(`/practice/${studentId}`, { scroll: false })
}, [studentId, router])
// Handle starting fresh - abandon current session and go to configure
const handleStartFresh = useCallback(() => {
abandonSession.mutate(
{ playerId: studentId, planId: session.id },
{
onSuccess: () => {
router.push(`/practice/${studentId}/configure`, { scroll: false })
},
}
)
}, [studentId, session.id, abandonSession, router])
return (
<PageWithNav>
<main
data-component="resume-practice-page"
className={css({
minHeight: '100vh',
backgroundColor: isDark ? 'gray.900' : 'gray.50',
paddingTop: 'calc(80px + 2rem)',
paddingLeft: '2rem',
paddingRight: '2rem',
paddingBottom: '2rem',
})}
>
<div
className={css({
maxWidth: '800px',
margin: '0 auto',
})}
>
{/* Header */}
<header
className={css({
textAlign: 'center',
marginBottom: '2rem',
})}
>
<h1
className={css({
fontSize: '2rem',
fontWeight: 'bold',
color: isDark ? 'white' : 'gray.800',
marginBottom: '0.5rem',
})}
>
Daily Practice
</h1>
<p
className={css({
fontSize: '1rem',
color: isDark ? 'gray.400' : 'gray.600',
})}
>
Build your soroban skills one step at a time
</p>
</header>
{/* Continue Session Card */}
<ContinueSessionCard
studentName={player.name}
studentEmoji={player.emoji}
studentColor={player.color}
session={session}
onContinue={handleContinue}
onStartFresh={handleStartFresh}
/>
</div>
</main>
</PageWithNav>
)
}

View File

@ -0,0 +1,51 @@
import { notFound, redirect } from 'next/navigation'
import { getActiveSessionPlan, getPlayer } from '@/lib/curriculum/server'
import { ResumeClient } from './ResumeClient'
// Disable caching for this page - session state must always be fresh
export const dynamic = 'force-dynamic'
interface ResumePageProps {
params: Promise<{ studentId: string }>
}
/**
* Resume Session Page - Server Component
*
* Shows "Welcome back" card for students returning to an in-progress session.
* If no active session exists, redirects to the main practice page.
*
* URL: /practice/[studentId]/resume
*/
export default async function ResumePage({ params }: ResumePageProps) {
const { studentId } = await params
// Fetch player and active session in parallel
const [player, activeSession] = await Promise.all([
getPlayer(studentId),
getActiveSessionPlan(studentId),
])
// 404 if player doesn't exist
if (!player) {
notFound()
}
// No active session → redirect to main practice page (shows dashboard)
if (!activeSession) {
redirect(`/practice/${studentId}`)
}
// Session is completed → redirect to main practice page (shows summary)
if (activeSession.completedAt) {
redirect(`/practice/${studentId}`)
}
return (
<ResumeClient
studentId={studentId}
player={player}
session={activeSession}
/>
)
}