11import type { Course } from '@/types/course' ;
2+ import { useEffect , useRef } from 'react' ;
23
34interface Props {
45 course : Course | null ;
@@ -16,15 +17,48 @@ interface Props {
1617// shell. Rendering the actual course fields and styling is in scope for the ticket.
1718
1819export default function CourseDetailPanel ( { course, onClose } : Props ) {
20+ const panelRef = useRef < HTMLElement > ( null ) ;
21+ const prevFocusRef = useRef < HTMLElement | null > ( null ) ;
22+
23+ useEffect ( ( ) => {
24+ if ( course ) {
25+ prevFocusRef . current = document . activeElement as HTMLElement ;
26+ panelRef . current ?. focus ( ) ;
27+ } else {
28+ prevFocusRef . current ?. focus ( ) ;
29+ prevFocusRef . current = null ;
30+ }
31+ } , [ course ] ) ;
32+
33+ useEffect ( ( ) => {
34+ const handleKey = ( e : KeyboardEvent ) => {
35+ if ( e . key === 'Escape' ) {
36+ onClose ( ) ;
37+ }
38+ } ;
39+
40+ document . addEventListener ( 'keydown' , handleKey ) ;
41+ return ( ) => document . removeEventListener ( 'keydown' , handleKey ) ;
42+ } , [ onClose ] ) ;
43+
1944 if ( course === null ) return null ;
2045
2146 return (
2247 < aside
23- className = "absolute inset-y-0 right-0 z-10 w-80 overflow-y-auto border-l border-gray-200 bg-white p-4 shadow-lg"
48+ className = "absolute inset-y-0 right-0 z-10 w-90 max-sm:w-5/6 overflow-y-auto border-l border-gray-200 bg-white p-4 shadow-lg"
2449 aria-label = { `Details for ${ course . code } ` }
50+ ref = { panelRef }
51+ tabIndex = { - 1 }
2552 >
26- < div className = "flex items-start justify-between gap-2" >
27- < h2 className = "text-lg font-bold text-gray-900" > { course . code } </ h2 >
53+ < div className = "flex items-start justify-between gap-2 border-b border-gray-200 pb-3" >
54+ < div >
55+ < h2 className = "text-xl font-bold text-red-600" >
56+ { course . code } [{ course . credits } credit]
57+ </ h2 >
58+ < p className = "mt-1 text-base font-semibold text-gray-900" >
59+ { course . title }
60+ </ p >
61+ </ div >
2862 < button
2963 type = "button"
3064 onClick = { onClose }
@@ -35,14 +69,28 @@ export default function CourseDetailPanel({ course, onClose }: Props) {
3569 </ button >
3670 </ div >
3771
38- { /*
39- TODO(volunteer) — flesh out the panel body. Fields available on `course`:
40- title, credits, description, prereqRaw, precludes.
41- Prereq: render `course.prereqRaw` as plain text for now (a richer view of
42- the parsed `course.prereq` AST is a later enhancement).
43- Also: visual design, Escape-to-close + focus management, mobile treatment
44- See the course-detail-panel ticket.
45- */ }
72+ < div className = "space-y-3 mt-4" >
73+ { course . description && (
74+ < div >
75+ < h3 className = "font-semibold" > Description</ h3 >
76+ < p > { course . description } </ p >
77+ </ div >
78+ ) }
79+
80+ { course . precludes && course . precludes . length > 0 && (
81+ < div >
82+ < h3 className = "font-semibold" > Precludes</ h3 >
83+ < p > { course . precludes . join ( ', ' ) } </ p >
84+ </ div >
85+ ) }
86+
87+ { course . prereqRaw && (
88+ < div >
89+ < h3 className = "font-semibold" > Prerequisite(s)</ h3 >
90+ < p > { course . prereqRaw } </ p >
91+ </ div >
92+ ) }
93+ </ div >
4694 </ aside >
4795 ) ;
4896}
0 commit comments