refactor(practice): improve relationship display and dashboard components
- DashboardClient: refactor session observation and error boundary handling - PracticeSubNav: streamline relationship indicator integration - RelationshipCard: improve layout and styling - RelationshipIndicator: simplify component structure - useStudentRelationship: cleanup hook implementation - Export relationship components from practice index 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6def610877
commit
c8f2984d7b
|
|
@ -2573,12 +2573,15 @@ export function DashboardClient({
|
|||
const [isObserving, setIsObserving] = useState(false)
|
||||
|
||||
// Handle session observation from PracticeSubNav action menu
|
||||
const handleObserveSession = useCallback((sessionId: string) => {
|
||||
// We're already on this student's page, just open the observer modal
|
||||
if (activeSession?.id === sessionId) {
|
||||
setIsObserving(true)
|
||||
}
|
||||
}, [activeSession?.id])
|
||||
const handleObserveSession = useCallback(
|
||||
(sessionId: string) => {
|
||||
// We're already on this student's page, just open the observer modal
|
||||
if (activeSession?.id === sessionId) {
|
||||
setIsObserving(true)
|
||||
}
|
||||
},
|
||||
[activeSession?.id]
|
||||
)
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(tab: TabId) => {
|
||||
|
|
@ -2754,114 +2757,115 @@ export function DashboardClient({
|
|||
<PracticeErrorBoundary studentName={player.name}>
|
||||
<main
|
||||
data-component="practice-dashboard-page"
|
||||
style={{
|
||||
opacity: contentOpacity,
|
||||
transition: contentTransition,
|
||||
}}
|
||||
className={css({
|
||||
minHeight: '100vh',
|
||||
backgroundColor: isDark ? 'gray.900' : 'gray.50',
|
||||
padding: { base: '0.75rem', sm: '1rem', md: '1.5rem' },
|
||||
})}
|
||||
>
|
||||
<div className={css({ maxWidth: '900px', margin: '0 auto' })}>
|
||||
{/* Session mode banner - renders in-flow, projects to nav on scroll */}
|
||||
<ContentBannerSlot
|
||||
stickyOffset={STICKY_HEADER_OFFSET}
|
||||
className={css({ marginBottom: '1rem' })}
|
||||
style={{
|
||||
opacity: contentOpacity,
|
||||
transition: contentTransition,
|
||||
}}
|
||||
className={css({
|
||||
minHeight: '100vh',
|
||||
backgroundColor: isDark ? 'gray.900' : 'gray.50',
|
||||
padding: { base: '0.75rem', sm: '1rem', md: '1.5rem' },
|
||||
})}
|
||||
>
|
||||
<div className={css({ maxWidth: '900px', margin: '0 auto' })}>
|
||||
{/* Session mode banner - renders in-flow, projects to nav on scroll */}
|
||||
<ContentBannerSlot
|
||||
stickyOffset={STICKY_HEADER_OFFSET}
|
||||
className={css({ marginBottom: '1rem' })}
|
||||
/>
|
||||
|
||||
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} isDark={isDark} />
|
||||
|
||||
{activeTab === 'overview' && (
|
||||
<OverviewTab
|
||||
student={selectedStudent}
|
||||
currentPhase={currentPhase}
|
||||
skillHealth={skillHealth}
|
||||
onStartPractice={handleStartPractice}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'skills' && (
|
||||
<SkillsTab
|
||||
skills={liveSkills}
|
||||
problemHistory={problemHistory}
|
||||
recentSessions={recentSessions}
|
||||
isDark={isDark}
|
||||
onManageSkills={() => setShowManualSkillModal(true)}
|
||||
studentId={studentId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && <HistoryTab isDark={isDark} studentId={studentId} />}
|
||||
|
||||
{activeTab === 'notes' && (
|
||||
<NotesTab
|
||||
isDark={isDark}
|
||||
notes={currentNotes}
|
||||
studentName={player.name}
|
||||
playerId={player.id}
|
||||
onNotesSaved={setCurrentNotes}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<ManualSkillSelector
|
||||
studentName={player.name}
|
||||
playerId={player.id}
|
||||
open={showManualSkillModal}
|
||||
onClose={() => setShowManualSkillModal(false)}
|
||||
onSave={handleSaveManualSkills}
|
||||
currentMasteredSkills={liveSkills.filter((s) => s.isPracticing).map((s) => s.skillId)}
|
||||
skillMasteryData={liveSkills}
|
||||
bktResultsMap={bktResultsMap}
|
||||
/>
|
||||
</main>
|
||||
|
||||
<TabNavigation activeTab={activeTab} onTabChange={handleTabChange} isDark={isDark} />
|
||||
{showStartPracticeModal && sessionMode && (
|
||||
<StartPracticeModal
|
||||
studentId={studentId}
|
||||
studentName={player.name}
|
||||
focusDescription={sessionMode.focusDescription}
|
||||
sessionMode={sessionMode}
|
||||
avgSecondsPerProblem={avgSecondsPerProblem}
|
||||
existingPlan={activeSession}
|
||||
problemHistory={problemHistory}
|
||||
onClose={() => setShowStartPracticeModal(false)}
|
||||
onStarted={() => setShowStartPracticeModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'overview' && (
|
||||
<OverviewTab
|
||||
student={selectedStudent}
|
||||
currentPhase={currentPhase}
|
||||
skillHealth={skillHealth}
|
||||
onStartPractice={handleStartPractice}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'skills' && (
|
||||
<SkillsTab
|
||||
skills={liveSkills}
|
||||
problemHistory={problemHistory}
|
||||
recentSessions={recentSessions}
|
||||
isDark={isDark}
|
||||
onManageSkills={() => setShowManualSkillModal(true)}
|
||||
studentId={studentId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'history' && <HistoryTab isDark={isDark} studentId={studentId} />}
|
||||
|
||||
{activeTab === 'notes' && (
|
||||
<NotesTab
|
||||
isDark={isDark}
|
||||
notes={currentNotes}
|
||||
studentName={player.name}
|
||||
playerId={player.id}
|
||||
onNotesSaved={setCurrentNotes}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
<ManualSkillSelector
|
||||
studentName={player.name}
|
||||
playerId={player.id}
|
||||
open={showManualSkillModal}
|
||||
onClose={() => setShowManualSkillModal(false)}
|
||||
onSave={handleSaveManualSkills}
|
||||
currentMasteredSkills={liveSkills.filter((s) => s.isPracticing).map((s) => s.skillId)}
|
||||
skillMasteryData={liveSkills}
|
||||
bktResultsMap={bktResultsMap}
|
||||
/>
|
||||
</main>
|
||||
|
||||
{showStartPracticeModal && sessionMode && (
|
||||
<StartPracticeModal
|
||||
studentId={studentId}
|
||||
studentName={player.name}
|
||||
focusDescription={sessionMode.focusDescription}
|
||||
sessionMode={sessionMode}
|
||||
avgSecondsPerProblem={avgSecondsPerProblem}
|
||||
existingPlan={activeSession}
|
||||
problemHistory={problemHistory}
|
||||
onClose={() => setShowStartPracticeModal(false)}
|
||||
onStarted={() => setShowStartPracticeModal(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Session Observer Modal */}
|
||||
{isObserving && activeSession && (
|
||||
<SessionObserverModal
|
||||
isOpen={isObserving}
|
||||
onClose={() => setIsObserving(false)}
|
||||
session={{
|
||||
sessionId: activeSession.id,
|
||||
playerId: studentId,
|
||||
startedAt:
|
||||
typeof activeSession.createdAt === 'string'
|
||||
? activeSession.createdAt
|
||||
: activeSession.createdAt instanceof Date
|
||||
? activeSession.createdAt.toISOString()
|
||||
: new Date().toISOString(),
|
||||
currentPartIndex: activeSession.currentPartIndex ?? 0,
|
||||
currentSlotIndex: activeSession.currentSlotIndex ?? 0,
|
||||
totalParts: activeSession.parts?.length ?? 1,
|
||||
totalProblems: activeSession.parts?.reduce((sum, p) => sum + p.slots.length, 0) ?? 0,
|
||||
completedProblems:
|
||||
activeSession.results?.filter((r) => r.isCorrect !== null).length ?? 0,
|
||||
}}
|
||||
student={{
|
||||
name: player.name,
|
||||
emoji: player.emoji,
|
||||
color: player.color,
|
||||
}}
|
||||
observerId={userId}
|
||||
/>
|
||||
)}
|
||||
{/* Session Observer Modal */}
|
||||
{isObserving && activeSession && (
|
||||
<SessionObserverModal
|
||||
isOpen={isObserving}
|
||||
onClose={() => setIsObserving(false)}
|
||||
session={{
|
||||
sessionId: activeSession.id,
|
||||
playerId: studentId,
|
||||
startedAt:
|
||||
typeof activeSession.createdAt === 'string'
|
||||
? activeSession.createdAt
|
||||
: activeSession.createdAt instanceof Date
|
||||
? activeSession.createdAt.toISOString()
|
||||
: new Date().toISOString(),
|
||||
currentPartIndex: activeSession.currentPartIndex ?? 0,
|
||||
currentSlotIndex: activeSession.currentSlotIndex ?? 0,
|
||||
totalParts: activeSession.parts?.length ?? 1,
|
||||
totalProblems:
|
||||
activeSession.parts?.reduce((sum, p) => sum + p.slots.length, 0) ?? 0,
|
||||
completedProblems:
|
||||
activeSession.results?.filter((r) => r.isCorrect !== null).length ?? 0,
|
||||
}}
|
||||
student={{
|
||||
name: player.name,
|
||||
emoji: player.emoji,
|
||||
color: player.color,
|
||||
}}
|
||||
observerId={userId}
|
||||
/>
|
||||
)}
|
||||
</PracticeErrorBoundary>
|
||||
</PageWithNav>
|
||||
</SessionModeBannerProvider>
|
||||
|
|
|
|||
|
|
@ -179,7 +179,14 @@ export function PracticeSubNav({
|
|||
}
|
||||
: undefined,
|
||||
}
|
||||
}, [student.id, student.name, student.isArchived, viewerRelationship, hasActiveSession, activeSession?.id])
|
||||
}, [
|
||||
student.id,
|
||||
student.name,
|
||||
student.isArchived,
|
||||
viewerRelationship,
|
||||
hasActiveSession,
|
||||
activeSession?.id,
|
||||
])
|
||||
|
||||
// Use student actions hook for menu logic
|
||||
const { actions, handlers, modals, classrooms } = useStudentActions(studentActionData, {
|
||||
|
|
@ -515,7 +522,7 @@ export function PracticeSubNav({
|
|||
sideOffset={8}
|
||||
align="end"
|
||||
>
|
||||
{/* Go to Dashboard - when not on dashboard */}
|
||||
{/* Go to Dashboard - when not on dashboard */}
|
||||
{!isOnDashboard && (
|
||||
<DropdownMenu.Item
|
||||
className={menuItemStyles(isDark)}
|
||||
|
|
@ -635,16 +642,18 @@ export function PracticeSubNav({
|
|||
)}
|
||||
|
||||
{/* Show enroll option even if no enrollments yet */}
|
||||
{classrooms.enrolled.length === 0 && !classrooms.current && actions.enrollInClassroom && (
|
||||
<DropdownMenu.Item
|
||||
className={menuItemStyles(isDark)}
|
||||
onSelect={handlers.openEnrollModal}
|
||||
data-action="enroll-in-classroom"
|
||||
>
|
||||
<span>{ACTION_DEFINITIONS.enrollInClassroom.icon}</span>
|
||||
<span>{ACTION_DEFINITIONS.enrollInClassroom.label}</span>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{classrooms.enrolled.length === 0 &&
|
||||
!classrooms.current &&
|
||||
actions.enrollInClassroom && (
|
||||
<DropdownMenu.Item
|
||||
className={menuItemStyles(isDark)}
|
||||
onSelect={handlers.openEnrollModal}
|
||||
data-action="enroll-in-classroom"
|
||||
>
|
||||
<span>{ACTION_DEFINITIONS.enrollInClassroom.icon}</span>
|
||||
<span>{ACTION_DEFINITIONS.enrollInClassroom.label}</span>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
|
||||
<DropdownMenu.Separator className={separatorStyles(isDark)} />
|
||||
|
||||
|
|
|
|||
|
|
@ -112,12 +112,7 @@ export function RelationshipCard({ playerId, className, compact = false }: Relat
|
|||
>
|
||||
{/* Parents Section */}
|
||||
{stakeholders.parents.length > 0 && (
|
||||
<StakeholderSection
|
||||
title="Parents"
|
||||
icon="👪"
|
||||
isDark={isDark}
|
||||
compact={compact}
|
||||
>
|
||||
<StakeholderSection title="Parents" icon="👪" isDark={isDark} compact={compact}>
|
||||
<div className={css({ display: 'flex', flexWrap: 'wrap', gap: '6px' })}>
|
||||
{stakeholders.parents.map((parent) => (
|
||||
<ParentBadge key={parent.id} parent={parent} isDark={isDark} compact={compact} />
|
||||
|
|
@ -128,12 +123,7 @@ export function RelationshipCard({ playerId, className, compact = false }: Relat
|
|||
|
||||
{/* Classrooms Section */}
|
||||
{stakeholders.enrolledClassrooms.length > 0 && (
|
||||
<StakeholderSection
|
||||
title="Classrooms"
|
||||
icon="🏫"
|
||||
isDark={isDark}
|
||||
compact={compact}
|
||||
>
|
||||
<StakeholderSection title="Classrooms" icon="🏫" isDark={isDark} compact={compact}>
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '6px' })}>
|
||||
{stakeholders.enrolledClassrooms.map((classroom) => (
|
||||
<ClassroomRow
|
||||
|
|
@ -150,12 +140,7 @@ export function RelationshipCard({ playerId, className, compact = false }: Relat
|
|||
|
||||
{/* Pending Enrollments */}
|
||||
{stakeholders.pendingEnrollments.length > 0 && (
|
||||
<StakeholderSection
|
||||
title="Pending"
|
||||
icon="⌛"
|
||||
isDark={isDark}
|
||||
compact={compact}
|
||||
>
|
||||
<StakeholderSection title="Pending" icon="⌛" isDark={isDark} compact={compact}>
|
||||
<div className={css({ display: 'flex', flexDirection: 'column', gap: '6px' })}>
|
||||
{stakeholders.pendingEnrollments.map((pending) => (
|
||||
<PendingRow key={pending.id} pending={pending} isDark={isDark} compact={compact} />
|
||||
|
|
@ -210,7 +195,12 @@ function ViewerRelationshipSection({
|
|||
? `Enrolled in ${relationship.classroomName}`
|
||||
: 'Enrolled in your classroom',
|
||||
color: {
|
||||
light: { bg: 'purple.50', border: 'purple.200', text: 'purple.700', accent: 'purple.600' },
|
||||
light: {
|
||||
bg: 'purple.50',
|
||||
border: 'purple.200',
|
||||
text: 'purple.700',
|
||||
accent: 'purple.600',
|
||||
},
|
||||
dark: {
|
||||
bg: 'purple.900/40',
|
||||
border: 'purple.700',
|
||||
|
|
@ -225,7 +215,12 @@ function ViewerRelationshipSection({
|
|||
title: 'Visiting Student',
|
||||
subtitle: presence ? `Present in ${presence.classroomName}` : 'In your classroom',
|
||||
color: {
|
||||
light: { bg: 'emerald.50', border: 'emerald.200', text: 'emerald.700', accent: 'emerald.600' },
|
||||
light: {
|
||||
bg: 'emerald.50',
|
||||
border: 'emerald.200',
|
||||
text: 'emerald.700',
|
||||
accent: 'emerald.600',
|
||||
},
|
||||
dark: {
|
||||
bg: 'emerald.900/40',
|
||||
border: 'emerald.700',
|
||||
|
|
|
|||
|
|
@ -154,12 +154,7 @@ export function RelationshipIndicator({
|
|||
)}
|
||||
>
|
||||
{badges.map((badge) => (
|
||||
<CompactBadge
|
||||
key={badge.key}
|
||||
badge={badge}
|
||||
isDark={isDark}
|
||||
showTooltip={showTooltip}
|
||||
/>
|
||||
<CompactBadge key={badge.key} badge={badge} isDark={isDark} showTooltip={showTooltip} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
|
@ -254,13 +249,7 @@ function CompactBadge({
|
|||
/**
|
||||
* Full badge: icon + label
|
||||
*/
|
||||
function FullBadge({
|
||||
badge,
|
||||
isDark,
|
||||
}: {
|
||||
badge: RelationshipBadge
|
||||
isDark: boolean
|
||||
}) {
|
||||
function FullBadge({ badge, isDark }: { badge: RelationshipBadge; isDark: boolean }) {
|
||||
const colors = isDark ? badge.color.dark : badge.color.light
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ export { NumericKeypad } from './NumericKeypad'
|
|||
export { PracticeErrorBoundary } from './PracticeErrorBoundary'
|
||||
export { PracticeFeedback } from './PracticeFeedback'
|
||||
export type { RelationshipBadgeProps, RelationshipConfig } from './RelationshipBadge'
|
||||
export { RelationshipBadge, RelationshipSummary, RELATIONSHIP_CONFIGS, getRelationType } from './RelationshipBadge'
|
||||
export {
|
||||
RelationshipBadge,
|
||||
RelationshipSummary,
|
||||
RELATIONSHIP_CONFIGS,
|
||||
getRelationType,
|
||||
} from './RelationshipBadge'
|
||||
export type { RelationshipCardProps } from './RelationshipCard'
|
||||
export { RelationshipCard } from './RelationshipCard'
|
||||
export { PurposeBadge } from './PurposeBadge'
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@
|
|||
|
||||
import { useMemo } from 'react'
|
||||
import type { StudentRelationship, EnrollmentStatus } from '@/types/student'
|
||||
import {
|
||||
useMyClassroom,
|
||||
useEnrolledStudents,
|
||||
useClassroomPresence,
|
||||
} from '@/hooks/useClassroom'
|
||||
import { useMyClassroom, useEnrolledStudents, useClassroomPresence } from '@/hooks/useClassroom'
|
||||
import { usePlayersWithSkillData } from '@/hooks/useUserPlayers'
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue