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:
parent
7243502873
commit
7b476e80c1
|
|
@ -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]
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue