From b19c6d0eca8331983990f070e704cd9249824265 Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sat, 6 Dec 2025 19:16:13 -0600 Subject: [PATCH] feat(practice): add session HUD with tape-deck controls and PageWithNav MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major UI improvements to the practice session: - Add dark control bar at top with session info and transport controls - Replace pause/end buttons with tape-deck style buttons (⏸️/▶ and ⏹️) - Move part type, problem count, and progress info into compact HUD - Add overall progress counter (X/Y total) and health indicator - Wrap practice page with PageWithNav for consistent app navigation - Begin dark mode support with isDark prop from useTheme 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- apps/web/src/app/practice/page.tsx | 757 +++++++++--------- .../src/components/practice/ActiveSession.tsx | 330 ++++---- 2 files changed, 563 insertions(+), 524 deletions(-) diff --git a/apps/web/src/app/practice/page.tsx b/apps/web/src/app/practice/page.tsx index 0c1e52be..57072982 100644 --- a/apps/web/src/app/practice/page.tsx +++ b/apps/web/src/app/practice/page.tsx @@ -17,6 +17,8 @@ import { OfflineSessionForm, } from '@/components/practice/OfflineSessionForm' import { PlacementTest } from '@/components/practice/PlacementTest' +import { PageWithNav } from '@/components/PageWithNav' +import { useTheme } from '@/contexts/ThemeContext' import type { SlotResult } from '@/db/schema/session-plans' import { usePlayerCurriculum } from '@/hooks/usePlayerCurriculum' import { @@ -85,6 +87,9 @@ interface SessionConfig { * 6. View summary */ export default function PracticePage() { + const { resolvedTheme } = useTheme() + const isDark = resolvedTheme === 'dark' + const [viewState, setViewState] = useState('selecting') const [selectedStudent, setSelectedStudent] = useState(null) const [sessionConfig, setSessionConfig] = useState({ @@ -397,446 +402,448 @@ export default function PracticePage() { } return ( -
-
+
- {/* Header - hide during practice */} - {viewState !== 'practicing' && ( -
-

- Daily Practice -

-

- Build your soroban skills one step at a time -

-
- )} - - {/* Content based on view state */} - {viewState === 'selecting' && - (isLoadingStudents ? ( -
+ {/* Header - hide during practice */} + {viewState !== 'practicing' && ( +
- Loading students... -
- ) : ( - - ))} - - {viewState === 'dashboard' && selectedStudent && ( - - )} - - {viewState === 'configuring' && selectedStudent && ( -
-

- Configure Practice Session -

- - {/* Duration selector */} -
- -
+

- {[5, 10, 15, 20].map((mins) => ( - - ))} -

-
+ Build your soroban skills one step at a time +

+ + )} - {/* Session structure preview */} + {/* Content based on view state */} + {viewState === 'selecting' && + (isLoadingStudents ? ( +
+ Loading students... +
+ ) : ( + + ))} + + {viewState === 'dashboard' && selectedStudent && ( + + )} + + {viewState === 'configuring' && selectedStudent && (
-
- Today's Practice Structure -
-
-
- 🧮 - - Part 1: Use abacus - -
-
- 🧠 - - Part 2: Mental math (visualization) - -
-
- 💭 - - Part 3: Mental math (linear) - -
-
-
+ Configure Practice Session + - {/* Error display for plan generation */} - {error?.context === 'generate' && ( + {/* Duration selector */} +
+ +
+ {[5, 10, 15, 20].map((mins) => ( + + ))} +
+
+ + {/* Session structure preview */}
- ⚠️ -
-
- {error.message} -
-
- {error.suggestion} -
+ Today's Practice Structure +
+
+
+ 🧮 + + Part 1: Use abacus + +
+
+ 🧠 + + Part 2: Mental math (visualization) + +
+
+ 💭 + + Part 3: Mental math (linear) +
- )} - {/* Action buttons */} + {/* Error display for plan generation */} + {error?.context === 'generate' && ( +
+
+ ⚠️ +
+
+ {error.message} +
+
+ {error.suggestion} +
+
+
+
+ )} + + {/* Action buttons */} +
+ + +
+
+ )} + + {viewState === 'reviewing' && selectedStudent && currentPlan && ( +
+ {/* Error display for session start */} + {error?.context === 'start' && ( +
+
+ ⚠️ +
+
+ {error.message} +
+
+ {error.suggestion} +
+
+
+
+ )} + +
+ )} + + {viewState === 'practicing' && selectedStudent && currentPlan && ( + + )} + + {viewState === 'summary' && selectedStudent && currentPlan && ( + + )} + + {viewState === 'creating' && (
+

+ Add New Student +

+

+ Student creation form coming soon! +

-
-
- )} + )} - {viewState === 'reviewing' && selectedStudent && currentPlan && ( -
- {/* Error display for session start */} - {error?.context === 'start' && ( -
-
- ⚠️ -
-
- {error.message} -
-
- {error.suggestion} -
-
-
-
- )} - -
- )} + )} +
- {viewState === 'practicing' && selectedStudent && currentPlan && ( - - )} - - {viewState === 'summary' && selectedStudent && currentPlan && ( - - )} - - {viewState === 'creating' && ( -
-

- Add New Student -

-

- Student creation form coming soon! -

- -
- )} - - {viewState === 'placement-test' && selectedStudent && ( - setShowManualSkillModal(false)} + onSave={handleSaveManualSkills} /> )} - - {/* Manual Skill Selector Modal */} - {selectedStudent && ( - setShowManualSkillModal(false)} - onSave={handleSaveManualSkills} - /> - )} - - {/* Offline Session Form Modal */} - {selectedStudent && ( - setShowOfflineSessionModal(false)} - onSubmit={handleSubmitOfflineSession} - /> - )} -
+ {/* Offline Session Form Modal */} + {selectedStudent && ( + setShowOfflineSessionModal(false)} + onSubmit={handleSubmitOfflineSession} + /> + )} + + ) } diff --git a/apps/web/src/components/practice/ActiveSession.tsx b/apps/web/src/components/practice/ActiveSession.tsx index 4b4f8e3c..b79e6d6d 100644 --- a/apps/web/src/components/practice/ActiveSession.tsx +++ b/apps/web/src/components/practice/ActiveSession.tsx @@ -560,114 +560,226 @@ export function ActiveSession({ minHeight: '100vh', })} > - {/* Header with progress and health */} + {/* Practice Session HUD - Control bar with session info and tape-deck controls */}
-
-
+ {/* Pause/Play button */} +
-
+ + {/* Stop button */} +
+ ⏹ +
- {sessionHealth && ( + {/* Session info display */} +
+ {/* Part type with emoji */}
- {getHealthEmoji(sessionHealth.overall)} + + {getPartTypeEmoji(currentPart.type)} + - {Math.round(sessionHealth.accuracy * 100)}% + Part {currentPart.partNumber}: {getPartTypeLabel(currentPart.type)}
- )} + + {/* Progress within part */} +
+ Problem {currentSlotIndex + 1} of {currentPart.slots.length} in this part +
+
+ + {/* Overall progress and health */} +
+ {/* Problem counter */} +
+
+ {completedProblems + 1}/{totalProblems} +
+
+ Total +
+
+ + {/* Health indicator */} + {sessionHealth && ( +
+ + {getHealthEmoji(sessionHealth.overall)} + + + {Math.round(sessionHealth.accuracy * 100)}% + +
+ )} +
- {/* Part indicator */} + {/* Part instruction banner - brief contextual hint */}
-
- {getPartTypeEmoji(currentPart.type)} -
-
- Part {currentPart.partNumber}: {getPartTypeLabel(currentPart.type)} -
-
- {currentPart.type === 'abacus' && 'Use your physical abacus to solve these problems'} - {currentPart.type === 'visualization' && 'Picture the beads moving in your mind'} - {currentPart.type === 'linear' && 'Calculate the answer mentally'} -
-
- Problem {currentSlotIndex + 1} of {currentPart.slots.length} in this part -
+ {currentPart.type === 'abacus' && '🧮 Use your physical abacus'} + {currentPart.type === 'visualization' && '🧠 Picture the beads moving in your mind'} + {currentPart.type === 'linear' && '💭 Calculate the answer mentally'}
{/* Problem display */} @@ -906,86 +1018,6 @@ export function ActiveSession({ )} - {/* Teacher controls */} -
- {isPaused ? ( - - ) : ( - - )} - - -
- {/* Pause overlay */} {isPaused && (