From 7b476e80c1165ac6636c5dd80c48baae82e638eb Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Tue, 9 Dec 2025 12:00:01 -0600 Subject: [PATCH] feat(practice): add /resume route for "Welcome back" experience MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- apps/web/src/app/practice/PracticeClient.tsx | 5 +- .../[studentId]/StudentPracticeClient.tsx | 8 +- .../[studentId]/resume/ResumeClient.tsx | 106 ++++++++++++++++++ .../app/practice/[studentId]/resume/page.tsx | 51 +++++++++ 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/app/practice/[studentId]/resume/ResumeClient.tsx create mode 100644 apps/web/src/app/practice/[studentId]/resume/page.tsx diff --git a/apps/web/src/app/practice/PracticeClient.tsx b/apps/web/src/app/practice/PracticeClient.tsx index 178e066d..71d554f4 100644 --- a/apps/web/src/app/practice/PracticeClient.tsx +++ b/apps/web/src/app/practice/PracticeClient.tsx @@ -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] ) diff --git a/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx b/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx index a0928d27..0a9cb607 100644 --- a/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx +++ b/apps/web/src/app/practice/[studentId]/StudentPracticeClient.tsx @@ -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(() => { diff --git a/apps/web/src/app/practice/[studentId]/resume/ResumeClient.tsx b/apps/web/src/app/practice/[studentId]/resume/ResumeClient.tsx new file mode 100644 index 00000000..e911018c --- /dev/null +++ b/apps/web/src/app/practice/[studentId]/resume/ResumeClient.tsx @@ -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 ( + +
+
+ {/* Header */} +
+

+ Daily Practice +

+

+ Build your soroban skills one step at a time +

+
+ + {/* Continue Session Card */} + +
+
+
+ ) +} diff --git a/apps/web/src/app/practice/[studentId]/resume/page.tsx b/apps/web/src/app/practice/[studentId]/resume/page.tsx new file mode 100644 index 00000000..2f91fa15 --- /dev/null +++ b/apps/web/src/app/practice/[studentId]/resume/page.tsx @@ -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 ( + + ) +}