Skip to content

Commit 8d22dbd

Browse files
Implement Course Detail Panel Component (#18)
* implement responsiveness * implement close on esc press * implement focus when panel opens * add course details to panel * remove stray classNames * add guard for course description * implement header changes * put course code and title on separate lines ---------
1 parent 347216b commit 8d22dbd

1 file changed

Lines changed: 59 additions & 11 deletions

File tree

src/components/CourseDetailPanel.tsx

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Course } from '@/types/course';
2+
import { useEffect, useRef } from 'react';
23

34
interface 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

1819
export 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

Comments
 (0)