feat(practice): add session HUD with tape-deck controls and PageWithNav
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 <noreply@anthropic.com>
This commit is contained in:
parent
871390d8e1
commit
b19c6d0eca
|
|
@ -17,6 +17,8 @@ import {
|
||||||
OfflineSessionForm,
|
OfflineSessionForm,
|
||||||
} from '@/components/practice/OfflineSessionForm'
|
} from '@/components/practice/OfflineSessionForm'
|
||||||
import { PlacementTest } from '@/components/practice/PlacementTest'
|
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 type { SlotResult } from '@/db/schema/session-plans'
|
||||||
import { usePlayerCurriculum } from '@/hooks/usePlayerCurriculum'
|
import { usePlayerCurriculum } from '@/hooks/usePlayerCurriculum'
|
||||||
import {
|
import {
|
||||||
|
|
@ -85,6 +87,9 @@ interface SessionConfig {
|
||||||
* 6. View summary
|
* 6. View summary
|
||||||
*/
|
*/
|
||||||
export default function PracticePage() {
|
export default function PracticePage() {
|
||||||
|
const { resolvedTheme } = useTheme()
|
||||||
|
const isDark = resolvedTheme === 'dark'
|
||||||
|
|
||||||
const [viewState, setViewState] = useState<ViewState>('selecting')
|
const [viewState, setViewState] = useState<ViewState>('selecting')
|
||||||
const [selectedStudent, setSelectedStudent] = useState<StudentWithProgress | null>(null)
|
const [selectedStudent, setSelectedStudent] = useState<StudentWithProgress | null>(null)
|
||||||
const [sessionConfig, setSessionConfig] = useState<SessionConfig>({
|
const [sessionConfig, setSessionConfig] = useState<SessionConfig>({
|
||||||
|
|
@ -397,446 +402,448 @@ export default function PracticePage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main
|
<PageWithNav>
|
||||||
data-component="practice-page"
|
<main
|
||||||
className={css({
|
data-component="practice-page"
|
||||||
minHeight: '100vh',
|
|
||||||
backgroundColor: 'gray.50',
|
|
||||||
padding: viewState === 'practicing' ? '0' : '2rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
className={css({
|
||||||
maxWidth: viewState === 'practicing' ? '100%' : '800px',
|
minHeight: '100vh',
|
||||||
margin: '0 auto',
|
backgroundColor: isDark ? 'gray.900' : 'gray.50',
|
||||||
|
padding: viewState === 'practicing' ? '0' : '2rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Header - hide during practice */}
|
<div
|
||||||
{viewState !== 'practicing' && (
|
className={css({
|
||||||
<header
|
maxWidth: viewState === 'practicing' ? '100%' : '800px',
|
||||||
className={css({
|
margin: '0 auto',
|
||||||
textAlign: 'center',
|
})}
|
||||||
marginBottom: '2rem',
|
>
|
||||||
})}
|
{/* Header - hide during practice */}
|
||||||
>
|
{viewState !== 'practicing' && (
|
||||||
<h1
|
<header
|
||||||
className={css({
|
|
||||||
fontSize: '2rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'gray.800',
|
|
||||||
marginBottom: '0.5rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Daily Practice
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
className={css({
|
|
||||||
fontSize: '1rem',
|
|
||||||
color: 'gray.600',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Build your soroban skills one step at a time
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Content based on view state */}
|
|
||||||
{viewState === 'selecting' &&
|
|
||||||
(isLoadingStudents ? (
|
|
||||||
<div
|
|
||||||
className={css({
|
className={css({
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
padding: '3rem',
|
marginBottom: '2rem',
|
||||||
color: 'gray.500',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Loading students...
|
<h1
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<StudentSelector
|
|
||||||
students={students}
|
|
||||||
selectedStudent={selectedStudent ?? undefined}
|
|
||||||
onSelectStudent={handleSelectStudent}
|
|
||||||
onAddStudent={handleAddStudent}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{viewState === 'dashboard' && selectedStudent && (
|
|
||||||
<ProgressDashboard
|
|
||||||
student={selectedStudent}
|
|
||||||
currentPhase={currentPhase}
|
|
||||||
recentSkills={recentSkills}
|
|
||||||
onContinuePractice={handleContinuePractice}
|
|
||||||
onViewFullProgress={handleViewFullProgress}
|
|
||||||
onGenerateWorksheet={handleGenerateWorksheet}
|
|
||||||
onChangeStudent={handleChangeStudent}
|
|
||||||
onRunPlacementTest={handleRunPlacementTest}
|
|
||||||
onSetSkillsManually={handleSetSkillsManually}
|
|
||||||
onRecordOfflinePractice={handleRecordOfflinePractice}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewState === 'configuring' && selectedStudent && (
|
|
||||||
<div
|
|
||||||
data-section="session-config"
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '1.5rem',
|
|
||||||
padding: '2rem',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
borderRadius: '16px',
|
|
||||||
boxShadow: 'md',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className={css({
|
|
||||||
fontSize: '1.5rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'gray.800',
|
|
||||||
textAlign: 'center',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Configure Practice Session
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Duration selector */}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
className={css({
|
className={css({
|
||||||
display: 'block',
|
fontSize: '2rem',
|
||||||
fontSize: '0.875rem',
|
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'gray.700',
|
color: isDark ? 'white' : 'gray.800',
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Session Duration
|
Daily Practice
|
||||||
</label>
|
</h1>
|
||||||
<div
|
<p
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
fontSize: '1rem',
|
||||||
gap: '0.5rem',
|
color: isDark ? 'gray.400' : 'gray.600',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{[5, 10, 15, 20].map((mins) => (
|
Build your soroban skills one step at a time
|
||||||
<button
|
</p>
|
||||||
key={mins}
|
</header>
|
||||||
type="button"
|
)}
|
||||||
onClick={() => setSessionConfig((c) => ({ ...c, durationMinutes: mins }))}
|
|
||||||
className={css({
|
|
||||||
flex: 1,
|
|
||||||
padding: '1rem',
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: sessionConfig.durationMinutes === mins ? 'white' : 'gray.700',
|
|
||||||
backgroundColor:
|
|
||||||
sessionConfig.durationMinutes === mins ? 'blue.500' : 'gray.100',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor:
|
|
||||||
sessionConfig.durationMinutes === mins ? 'blue.600' : 'gray.200',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{mins} min
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Session structure preview */}
|
{/* Content based on view state */}
|
||||||
|
{viewState === 'selecting' &&
|
||||||
|
(isLoadingStudents ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '3rem',
|
||||||
|
color: 'gray.500',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Loading students...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<StudentSelector
|
||||||
|
students={students}
|
||||||
|
selectedStudent={selectedStudent ?? undefined}
|
||||||
|
onSelectStudent={handleSelectStudent}
|
||||||
|
onAddStudent={handleAddStudent}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{viewState === 'dashboard' && selectedStudent && (
|
||||||
|
<ProgressDashboard
|
||||||
|
student={selectedStudent}
|
||||||
|
currentPhase={currentPhase}
|
||||||
|
recentSkills={recentSkills}
|
||||||
|
onContinuePractice={handleContinuePractice}
|
||||||
|
onViewFullProgress={handleViewFullProgress}
|
||||||
|
onGenerateWorksheet={handleGenerateWorksheet}
|
||||||
|
onChangeStudent={handleChangeStudent}
|
||||||
|
onRunPlacementTest={handleRunPlacementTest}
|
||||||
|
onSetSkillsManually={handleSetSkillsManually}
|
||||||
|
onRecordOfflinePractice={handleRecordOfflinePractice}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewState === 'configuring' && selectedStudent && (
|
||||||
<div
|
<div
|
||||||
|
data-section="session-config"
|
||||||
className={css({
|
className={css({
|
||||||
padding: '1rem',
|
display: 'flex',
|
||||||
backgroundColor: 'gray.50',
|
flexDirection: 'column',
|
||||||
borderRadius: '8px',
|
gap: '1.5rem',
|
||||||
border: '1px solid',
|
padding: '2rem',
|
||||||
borderColor: 'gray.200',
|
backgroundColor: 'white',
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: 'md',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<h2
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '0.875rem',
|
fontSize: '1.5rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: 'gray.700',
|
color: 'gray.800',
|
||||||
marginBottom: '0.75rem',
|
textAlign: 'center',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Today's Practice Structure
|
Configure Practice Session
|
||||||
</div>
|
</h2>
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
gap: '0.5rem',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
|
|
||||||
<span>🧮</span>
|
|
||||||
<span className={css({ color: 'gray.700' })}>
|
|
||||||
<strong>Part 1:</strong> Use abacus
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
|
|
||||||
<span>🧠</span>
|
|
||||||
<span className={css({ color: 'gray.700' })}>
|
|
||||||
<strong>Part 2:</strong> Mental math (visualization)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
|
|
||||||
<span>💭</span>
|
|
||||||
<span className={css({ color: 'gray.700' })}>
|
|
||||||
<strong>Part 3:</strong> Mental math (linear)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error display for plan generation */}
|
{/* Duration selector */}
|
||||||
{error?.context === 'generate' && (
|
<div>
|
||||||
|
<label
|
||||||
|
className={css({
|
||||||
|
display: 'block',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'gray.700',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Session Duration
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20].map((mins) => (
|
||||||
|
<button
|
||||||
|
key={mins}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSessionConfig((c) => ({ ...c, durationMinutes: mins }))}
|
||||||
|
className={css({
|
||||||
|
flex: 1,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '1.25rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: sessionConfig.durationMinutes === mins ? 'white' : 'gray.700',
|
||||||
|
backgroundColor:
|
||||||
|
sessionConfig.durationMinutes === mins ? 'blue.500' : 'gray.100',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor:
|
||||||
|
sessionConfig.durationMinutes === mins ? 'blue.600' : 'gray.200',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{mins} min
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Session structure preview */}
|
||||||
<div
|
<div
|
||||||
data-element="error-banner"
|
|
||||||
className={css({
|
className={css({
|
||||||
padding: '1rem',
|
padding: '1rem',
|
||||||
backgroundColor: 'red.50',
|
backgroundColor: 'gray.50',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
borderColor: 'red.200',
|
borderColor: 'gray.200',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
fontSize: '0.875rem',
|
||||||
alignItems: 'flex-start',
|
fontWeight: 'bold',
|
||||||
gap: '0.75rem',
|
color: 'gray.700',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span className={css({ fontSize: '1.25rem' })}>⚠️</span>
|
Today's Practice Structure
|
||||||
<div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
fontWeight: 'bold',
|
display: 'flex',
|
||||||
color: 'red.700',
|
flexDirection: 'column',
|
||||||
marginBottom: '0.25rem',
|
gap: '0.5rem',
|
||||||
})}
|
fontSize: '0.875rem',
|
||||||
>
|
})}
|
||||||
{error.message}
|
>
|
||||||
</div>
|
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
|
||||||
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
|
<span>🧮</span>
|
||||||
{error.suggestion}
|
<span className={css({ color: 'gray.700' })}>
|
||||||
</div>
|
<strong>Part 1:</strong> Use abacus
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
|
||||||
|
<span>🧠</span>
|
||||||
|
<span className={css({ color: 'gray.700' })}>
|
||||||
|
<strong>Part 2:</strong> Mental math (visualization)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={css({ display: 'flex', alignItems: 'center', gap: '0.5rem' })}>
|
||||||
|
<span>💭</span>
|
||||||
|
<span className={css({ color: 'gray.700' })}>
|
||||||
|
<strong>Part 3:</strong> Mental math (linear)
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Error display for plan generation */}
|
||||||
|
{error?.context === 'generate' && (
|
||||||
|
<div
|
||||||
|
data-element="error-banner"
|
||||||
|
className={css({
|
||||||
|
padding: '1rem',
|
||||||
|
backgroundColor: 'red.50',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'red.200',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '0.75rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className={css({ fontSize: '1.25rem' })}>⚠️</span>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'red.700',
|
||||||
|
marginBottom: '0.25rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
|
||||||
|
{error.suggestion}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.75rem',
|
||||||
|
marginTop: '1rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
generatePlan.reset()
|
||||||
|
setViewState('dashboard')
|
||||||
|
}}
|
||||||
|
className={css({
|
||||||
|
flex: 1,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'gray.600',
|
||||||
|
backgroundColor: 'gray.100',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
cursor: 'pointer',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'gray.200',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleGeneratePlan}
|
||||||
|
disabled={generatePlan.isPending}
|
||||||
|
className={css({
|
||||||
|
flex: 2,
|
||||||
|
padding: '1rem',
|
||||||
|
fontSize: '1.125rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.500',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: 'none',
|
||||||
|
cursor: generatePlan.isPending ? 'not-allowed' : 'pointer',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.600',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{generatePlan.isPending ? 'Generating...' : 'Generate Plan'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewState === 'reviewing' && selectedStudent && currentPlan && (
|
||||||
|
<div data-section="plan-review-wrapper">
|
||||||
|
{/* Error display for session start */}
|
||||||
|
{error?.context === 'start' && (
|
||||||
|
<div
|
||||||
|
data-element="error-banner"
|
||||||
|
className={css({
|
||||||
|
padding: '1rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
backgroundColor: 'red.50',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: 'red.200',
|
||||||
|
maxWidth: '600px',
|
||||||
|
margin: '0 auto 1rem auto',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '0.75rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className={css({ fontSize: '1.25rem' })}>⚠️</span>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'red.700',
|
||||||
|
marginBottom: '0.25rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{error.message}
|
||||||
|
</div>
|
||||||
|
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
|
||||||
|
{error.suggestion}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<PlanReview
|
||||||
|
plan={currentPlan}
|
||||||
|
studentName={selectedStudent.name}
|
||||||
|
onApprove={handleApprovePlan}
|
||||||
|
onCancel={handleCancelPlan}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewState === 'practicing' && selectedStudent && currentPlan && (
|
||||||
|
<ActiveSession
|
||||||
|
plan={currentPlan}
|
||||||
|
studentName={selectedStudent.name}
|
||||||
|
onAnswer={handleAnswer}
|
||||||
|
onEndEarly={handleEndEarly}
|
||||||
|
onComplete={handleSessionComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewState === 'summary' && selectedStudent && currentPlan && (
|
||||||
|
<SessionSummary
|
||||||
|
plan={currentPlan}
|
||||||
|
studentName={selectedStudent.name}
|
||||||
|
onPracticeAgain={handlePracticeAgain}
|
||||||
|
onBackToDashboard={handleBackToDashboard}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewState === 'creating' && (
|
||||||
<div
|
<div
|
||||||
|
data-section="create-student"
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
textAlign: 'center',
|
||||||
gap: '0.75rem',
|
padding: '3rem',
|
||||||
marginTop: '1rem',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
<h2
|
||||||
|
className={css({
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'gray.800',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Add New Student
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className={css({
|
||||||
|
color: 'gray.600',
|
||||||
|
marginBottom: '2rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Student creation form coming soon!
|
||||||
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => setViewState('selecting')}
|
||||||
generatePlan.reset()
|
|
||||||
setViewState('dashboard')
|
|
||||||
}}
|
|
||||||
className={css({
|
className={css({
|
||||||
flex: 1,
|
padding: '0.75rem 2rem',
|
||||||
padding: '1rem',
|
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
color: 'gray.600',
|
color: 'gray.700',
|
||||||
backgroundColor: 'gray.100',
|
backgroundColor: 'gray.200',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
_hover: {
|
_hover: {
|
||||||
backgroundColor: 'gray.200',
|
backgroundColor: 'gray.300',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
Cancel
|
← Back to Student Selection
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleGeneratePlan}
|
|
||||||
disabled={generatePlan.isPending}
|
|
||||||
className={css({
|
|
||||||
flex: 2,
|
|
||||||
padding: '1rem',
|
|
||||||
fontSize: '1.125rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'white',
|
|
||||||
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.500',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
cursor: generatePlan.isPending ? 'not-allowed' : 'pointer',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: generatePlan.isPending ? 'gray.400' : 'green.600',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{generatePlan.isPending ? 'Generating...' : 'Generate Plan'}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{viewState === 'reviewing' && selectedStudent && currentPlan && (
|
{viewState === 'placement-test' && selectedStudent && (
|
||||||
<div data-section="plan-review-wrapper">
|
<PlacementTest
|
||||||
{/* Error display for session start */}
|
|
||||||
{error?.context === 'start' && (
|
|
||||||
<div
|
|
||||||
data-element="error-banner"
|
|
||||||
className={css({
|
|
||||||
padding: '1rem',
|
|
||||||
marginBottom: '1rem',
|
|
||||||
backgroundColor: 'red.50',
|
|
||||||
borderRadius: '12px',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'red.200',
|
|
||||||
maxWidth: '600px',
|
|
||||||
margin: '0 auto 1rem auto',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: '0.75rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<span className={css({ fontSize: '1.25rem' })}>⚠️</span>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'red.700',
|
|
||||||
marginBottom: '0.25rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{error.message}
|
|
||||||
</div>
|
|
||||||
<div className={css({ fontSize: '0.875rem', color: 'red.600' })}>
|
|
||||||
{error.suggestion}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<PlanReview
|
|
||||||
plan={currentPlan}
|
|
||||||
studentName={selectedStudent.name}
|
studentName={selectedStudent.name}
|
||||||
onApprove={handleApprovePlan}
|
playerId={selectedStudent.id}
|
||||||
onCancel={handleCancelPlan}
|
onComplete={handlePlacementTestComplete}
|
||||||
|
onCancel={handlePlacementTestCancel}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{viewState === 'practicing' && selectedStudent && currentPlan && (
|
{/* Manual Skill Selector Modal */}
|
||||||
<ActiveSession
|
{selectedStudent && (
|
||||||
plan={currentPlan}
|
<ManualSkillSelector
|
||||||
studentName={selectedStudent.name}
|
|
||||||
onAnswer={handleAnswer}
|
|
||||||
onEndEarly={handleEndEarly}
|
|
||||||
onComplete={handleSessionComplete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewState === 'summary' && selectedStudent && currentPlan && (
|
|
||||||
<SessionSummary
|
|
||||||
plan={currentPlan}
|
|
||||||
studentName={selectedStudent.name}
|
|
||||||
onPracticeAgain={handlePracticeAgain}
|
|
||||||
onBackToDashboard={handleBackToDashboard}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewState === 'creating' && (
|
|
||||||
<div
|
|
||||||
data-section="create-student"
|
|
||||||
className={css({
|
|
||||||
textAlign: 'center',
|
|
||||||
padding: '3rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className={css({
|
|
||||||
fontSize: '1.5rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'gray.800',
|
|
||||||
marginBottom: '1rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Add New Student
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
className={css({
|
|
||||||
color: 'gray.600',
|
|
||||||
marginBottom: '2rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Student creation form coming soon!
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setViewState('selecting')}
|
|
||||||
className={css({
|
|
||||||
padding: '0.75rem 2rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
color: 'gray.700',
|
|
||||||
backgroundColor: 'gray.200',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'gray.300',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
← Back to Student Selection
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{viewState === 'placement-test' && selectedStudent && (
|
|
||||||
<PlacementTest
|
|
||||||
studentName={selectedStudent.name}
|
studentName={selectedStudent.name}
|
||||||
playerId={selectedStudent.id}
|
playerId={selectedStudent.id}
|
||||||
onComplete={handlePlacementTestComplete}
|
open={showManualSkillModal}
|
||||||
onCancel={handlePlacementTestCancel}
|
onClose={() => setShowManualSkillModal(false)}
|
||||||
|
onSave={handleSaveManualSkills}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Manual Skill Selector Modal */}
|
{/* Offline Session Form Modal */}
|
||||||
{selectedStudent && (
|
{selectedStudent && (
|
||||||
<ManualSkillSelector
|
<OfflineSessionForm
|
||||||
studentName={selectedStudent.name}
|
studentName={selectedStudent.name}
|
||||||
playerId={selectedStudent.id}
|
playerId={selectedStudent.id}
|
||||||
open={showManualSkillModal}
|
open={showOfflineSessionModal}
|
||||||
onClose={() => setShowManualSkillModal(false)}
|
onClose={() => setShowOfflineSessionModal(false)}
|
||||||
onSave={handleSaveManualSkills}
|
onSubmit={handleSubmitOfflineSession}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</main>
|
||||||
{/* Offline Session Form Modal */}
|
</PageWithNav>
|
||||||
{selectedStudent && (
|
|
||||||
<OfflineSessionForm
|
|
||||||
studentName={selectedStudent.name}
|
|
||||||
playerId={selectedStudent.id}
|
|
||||||
open={showOfflineSessionModal}
|
|
||||||
onClose={() => setShowOfflineSessionModal(false)}
|
|
||||||
onSubmit={handleSubmitOfflineSession}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -560,114 +560,226 @@ export function ActiveSession({
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Header with progress and health */}
|
{/* Practice Session HUD - Control bar with session info and tape-deck controls */}
|
||||||
<div
|
<div
|
||||||
data-section="session-header"
|
data-section="session-hud"
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: '0.75rem',
|
gap: '0.75rem',
|
||||||
backgroundColor: 'white',
|
padding: '0.75rem 1rem',
|
||||||
|
backgroundColor: 'gray.900',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
boxShadow: 'sm',
|
boxShadow: 'lg',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
{/* Tape deck controls */}
|
||||||
<div
|
<div
|
||||||
|
data-element="transport-controls"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Pause/Play button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action={isPaused ? 'resume' : 'pause'}
|
||||||
|
onClick={isPaused ? handleResume : handlePause}
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '0.875rem',
|
width: '48px',
|
||||||
color: 'gray.500',
|
height: '48px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: isPaused ? 'green.500' : 'gray.700',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: isPaused ? 'green.400' : 'gray.600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: isPaused ? 'green.400' : 'gray.600',
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
transform: 'scale(0.95)',
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
|
aria-label={isPaused ? 'Resume session' : 'Pause session'}
|
||||||
>
|
>
|
||||||
{studentName}'s Practice
|
{isPaused ? '▶' : '⏸'}
|
||||||
</div>
|
</button>
|
||||||
<div
|
|
||||||
|
{/* Stop button */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-action="end-early"
|
||||||
|
onClick={() => onEndEarly('Session ended')}
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '1.25rem',
|
width: '48px',
|
||||||
fontWeight: 'bold',
|
height: '48px',
|
||||||
color: 'gray.800',
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '1.5rem',
|
||||||
|
color: 'red.300',
|
||||||
|
backgroundColor: 'gray.700',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '2px solid',
|
||||||
|
borderColor: 'gray.600',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
_hover: {
|
||||||
|
backgroundColor: 'red.900',
|
||||||
|
borderColor: 'red.700',
|
||||||
|
color: 'red.200',
|
||||||
|
transform: 'scale(1.05)',
|
||||||
|
},
|
||||||
|
_active: {
|
||||||
|
transform: 'scale(0.95)',
|
||||||
|
},
|
||||||
})}
|
})}
|
||||||
|
aria-label="End session"
|
||||||
>
|
>
|
||||||
Problem {completedProblems + 1} of {totalProblems}
|
⏹
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sessionHealth && (
|
{/* Session info display */}
|
||||||
|
<div
|
||||||
|
data-element="session-info"
|
||||||
|
className={css({
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.125rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Part type with emoji */}
|
||||||
<div
|
<div
|
||||||
data-element="session-health"
|
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '0.5rem',
|
gap: '0.5rem',
|
||||||
padding: '0.5rem 1rem',
|
|
||||||
borderRadius: '20px',
|
|
||||||
backgroundColor: 'gray.50',
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<span>{getHealthEmoji(sessionHealth.overall)}</span>
|
<span
|
||||||
|
className={css({
|
||||||
|
fontSize: '1rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{getPartTypeEmoji(currentPart.type)}
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
className={css({
|
className={css({
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: getHealthColor(sessionHealth.overall),
|
color: 'white',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{Math.round(sessionHealth.accuracy * 100)}%
|
Part {currentPart.partNumber}: {getPartTypeLabel(currentPart.type)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
{/* Progress within part */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: 'gray.400',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Problem {currentSlotIndex + 1} of {currentPart.slots.length} in this part
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Overall progress and health */}
|
||||||
|
<div
|
||||||
|
data-element="progress-display"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.75rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Problem counter */}
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: '1rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'white',
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{completedProblems + 1}/{totalProblems}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
color: 'gray.500',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Health indicator */}
|
||||||
|
{sessionHealth && (
|
||||||
|
<div
|
||||||
|
data-element="session-health"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
backgroundColor: 'gray.800',
|
||||||
|
borderRadius: '6px',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span className={css({ fontSize: '1rem' })}>
|
||||||
|
{getHealthEmoji(sessionHealth.overall)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: getHealthColor(sessionHealth.overall),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Math.round(sessionHealth.accuracy * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Part indicator */}
|
{/* Part instruction banner - brief contextual hint */}
|
||||||
<div
|
<div
|
||||||
data-element="part-indicator"
|
data-element="part-instruction"
|
||||||
className={css({
|
className={css({
|
||||||
padding: '1rem',
|
padding: '0.5rem 1rem',
|
||||||
backgroundColor: partColors.bg,
|
backgroundColor: partColors.bg,
|
||||||
borderRadius: '12px',
|
borderRadius: '8px',
|
||||||
border: '2px solid',
|
border: '1px solid',
|
||||||
borderColor: partColors.border,
|
borderColor: partColors.border,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
color: partColors.text,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
{currentPart.type === 'abacus' && '🧮 Use your physical abacus'}
|
||||||
className={css({
|
{currentPart.type === 'visualization' && '🧠 Picture the beads moving in your mind'}
|
||||||
fontSize: '1.5rem',
|
{currentPart.type === 'linear' && '💭 Calculate the answer mentally'}
|
||||||
marginBottom: '0.25rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{getPartTypeEmoji(currentPart.type)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
fontSize: '1.25rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: partColors.text,
|
|
||||||
marginBottom: '0.25rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Part {currentPart.partNumber}: {getPartTypeLabel(currentPart.type)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
color: partColors.text,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{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'}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
fontSize: '0.75rem',
|
|
||||||
color: partColors.text,
|
|
||||||
marginTop: '0.5rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Problem {currentSlotIndex + 1} of {currentPart.slots.length} in this part
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Problem display */}
|
{/* Problem display */}
|
||||||
|
|
@ -906,86 +1018,6 @@ export function ActiveSession({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Teacher controls */}
|
|
||||||
<div
|
|
||||||
data-section="teacher-controls"
|
|
||||||
className={css({
|
|
||||||
display: 'flex',
|
|
||||||
gap: '0.75rem',
|
|
||||||
marginTop: 'auto',
|
|
||||||
paddingTop: '1rem',
|
|
||||||
borderTop: '1px solid',
|
|
||||||
borderColor: 'gray.200',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{isPaused ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-action="resume"
|
|
||||||
onClick={handleResume}
|
|
||||||
className={css({
|
|
||||||
flex: 1,
|
|
||||||
padding: '0.75rem',
|
|
||||||
fontSize: '1rem',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: 'white',
|
|
||||||
backgroundColor: 'green.500',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'green.600',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Resume Practice
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-action="pause"
|
|
||||||
onClick={handlePause}
|
|
||||||
className={css({
|
|
||||||
flex: 1,
|
|
||||||
padding: '0.75rem',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
color: 'gray.600',
|
|
||||||
backgroundColor: 'gray.100',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'gray.200',
|
|
||||||
cursor: 'pointer',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'gray.200',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Pause
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
data-action="end-early"
|
|
||||||
onClick={() => onEndEarly('Teacher ended session')}
|
|
||||||
className={css({
|
|
||||||
padding: '0.75rem 1.5rem',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
color: 'red.600',
|
|
||||||
backgroundColor: 'red.50',
|
|
||||||
borderRadius: '8px',
|
|
||||||
border: '1px solid',
|
|
||||||
borderColor: 'red.200',
|
|
||||||
cursor: 'pointer',
|
|
||||||
_hover: {
|
|
||||||
backgroundColor: 'red.100',
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
End Session
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pause overlay */}
|
{/* Pause overlay */}
|
||||||
{isPaused && (
|
{isPaused && (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue