From 7ac2d171b34eeec68267798aed00d372abd6d30e Mon Sep 17 00:00:00 2001 From: condyl Date: Sun, 15 Mar 2026 18:28:01 -0400 Subject: [PATCH] feat: enhance course data handling and display in calendar and forms --- docs/API.md | 11 ++- .../generator/Calendar/CalendarComponent.jsx | 31 ++++--- .../Calendar/hooks/useCalendarEvents.js | 31 ++++--- .../Calendar/utils/calendarUtils.jsx | 52 +++++++----- .../CourseList/CourseListItemComponent.jsx | 19 ++++- .../CourseSearch/CourseSearchComponent.jsx | 85 +++++++++++++++---- src/lib/generator/courseData.js | 4 +- src/lib/generator/createCalendarEvents.js | 1 + .../utils/combinationUtils.js | 10 ++- 9 files changed, 179 insertions(+), 65 deletions(-) diff --git a/docs/API.md b/docs/API.md index d668c73..a314df7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -30,8 +30,15 @@ curl "https://api.brocktimetable.com/api/getNameList?timetableType=UG&session=FW #### Example Response -``` -[ABED 4F84 D2", "ABED 4F85 D3", "ABTE 8P85 D3", "ABTE 8P90 D3", ...] +```json +[ + { + "label": "COSC 1P02 D2", + "courseCode": "COSC1P02", + "duration": "2", + "courseName": "Introduction to Computer Science" + } +] ``` ### 2. Get Course Data diff --git a/src/components/generator/Calendar/CalendarComponent.jsx b/src/components/generator/Calendar/CalendarComponent.jsx index 415b4ee..a6c63ca 100644 --- a/src/components/generator/Calendar/CalendarComponent.jsx +++ b/src/components/generator/Calendar/CalendarComponent.jsx @@ -179,18 +179,25 @@ export default function CalendarComponent({ currentColors, ); - const courseDetails = newEvents - .filter((event) => event.description) - .map((event) => { - let titleArray = event.title.trim().split(" "); - return { - name: titleArray[0], - instructor: event.description, - section: titleArray.pop(), - startDate: event.startRecur, - endDate: event.endRecur, - }; - }); + const courseDetails = Array.from( + new Map( + newEvents + .filter((event) => !event.extendedProps.isBlocked) + .map((event) => { + let titleArray = event.title.trim().split(" "); + const detail = { + name: titleArray[0], + courseName: event.extendedProps.courseName || "", + instructor: event.description, + section: titleArray.pop(), + startDate: event.startRecur, + endDate: event.endRecur, + }; + + return [detail.name, detail]; + }), + ).values(), + ); setCourseDetails(courseDetails); setEvents(newEvents); diff --git a/src/components/generator/Calendar/hooks/useCalendarEvents.js b/src/components/generator/Calendar/hooks/useCalendarEvents.js index 05984fe..80c1ffb 100644 --- a/src/components/generator/Calendar/hooks/useCalendarEvents.js +++ b/src/components/generator/Calendar/hooks/useCalendarEvents.js @@ -71,18 +71,25 @@ export const useCalendarEvents = ({ currentColors, ); - const courseDetails = newEvents - .filter((event) => event.description) - .map((event) => { - let titleArray = event.title.trim().split(" "); - return { - name: titleArray[0], - instructor: event.description, - section: titleArray.pop(), - startDate: event.startRecur, - endDate: event.endRecur, - }; - }); + const courseDetails = Array.from( + new Map( + newEvents + .filter((event) => !event.extendedProps.isBlocked) + .map((event) => { + let titleArray = event.title.trim().split(" "); + const detail = { + name: titleArray[0], + courseName: event.extendedProps.courseName || "", + instructor: event.description, + section: titleArray.pop(), + startDate: event.startRecur, + endDate: event.endRecur, + }; + + return [detail.name, detail]; + }), + ).values(), + ); setCourseDetails(courseDetails); setEvents(newEvents); diff --git a/src/components/generator/Calendar/utils/calendarUtils.jsx b/src/components/generator/Calendar/utils/calendarUtils.jsx index 77225cd..2b1c8bb 100644 --- a/src/components/generator/Calendar/utils/calendarUtils.jsx +++ b/src/components/generator/Calendar/utils/calendarUtils.jsx @@ -56,7 +56,9 @@ export const renderEventContent = (eventInfo, isMobile = false) => { ); } else { const isPinned = isEventPinned(eventInfo.event); + const courseNameText = eventInfo.event.extendedProps.courseName?.trim(); const instructorText = eventInfo.event.extendedProps.description?.trim(); + const shouldShowCourseName = Boolean(courseNameText); const shouldShowInstructor = !!instructorText && instructorText.toLowerCase() !== "no instructor"; @@ -89,27 +91,39 @@ export const renderEventContent = (eventInfo, isMobile = false) => { )}
- {eventInfo.timeText} -
- {eventInfo.event.title} + {eventInfo.timeText} + {eventInfo.event.title} + {(isMobile || eventDuration >= minDurationToShowTime) && + shouldShowCourseName && ( + + {courseNameText} + + )} {(isMobile || eventDuration >= minDurationToShowTime) && shouldShowInstructor && ( - <> -
- - {instructorText} - - + + {instructorText} + )}
diff --git a/src/components/generator/Forms/CourseList/CourseListItemComponent.jsx b/src/components/generator/Forms/CourseList/CourseListItemComponent.jsx index 8fd1fa1..db6a271 100644 --- a/src/components/generator/Forms/CourseList/CourseListItemComponent.jsx +++ b/src/components/generator/Forms/CourseList/CourseListItemComponent.jsx @@ -78,9 +78,16 @@ export default function CourseListComponent({ - - {course} - +
+ + {course} + + {courseDetail?.courseName && ( +

+ {courseDetail.courseName} +

+ )} +
+
+ Course Name + + {courseDetail?.courseName || "N/A"} + +
Course Instructor diff --git a/src/components/generator/Forms/CourseSearch/CourseSearchComponent.jsx b/src/components/generator/Forms/CourseSearch/CourseSearchComponent.jsx index da5ca8c..858f80c 100644 --- a/src/components/generator/Forms/CourseSearch/CourseSearchComponent.jsx +++ b/src/components/generator/Forms/CourseSearch/CourseSearchComponent.jsx @@ -30,6 +30,33 @@ export default function CourseSearchComponent({ const listRef = React.useRef(null); const PAGE_SIZE = 200; + const normalizedOptions = React.useMemo(() => { + return courseOptions.map((option) => { + if (typeof option === "string") { + return { + label: option, + courseCode: option.replace(/\s+D\d+$/i, "").replace(/\s+/g, ""), + duration: option.match(/D(\d+)$/i)?.[1] || "", + courseName: "", + }; + } + + const label = + option.label || + option.value || + `${option.courseCode?.slice(0, 4) || ""} ${ + option.courseCode?.slice(4) || "" + }${option.duration ? ` D${option.duration}` : ""}`.trim(); + + return { + label, + courseCode: option.courseCode || "", + duration: option.duration || "", + courseName: option.courseName || "", + }; + }); + }, [courseOptions]); + React.useEffect(() => { setDisplayLimit(PAGE_SIZE); }, [value]); @@ -51,6 +78,10 @@ export default function CourseSearchComponent({ const selectedItem = listEl?.querySelector('[data-selected="true"]'); if (selectedItem) return; event.preventDefault(); + if (filteredOptions.length > 0) { + handleSelectOption(filteredOptions[0]); + return; + } onEnterPress(value); setOpen(false); }; @@ -58,9 +89,17 @@ export default function CourseSearchComponent({ const filteredOptions = React.useMemo(() => { const query = (value || "").trim().toUpperCase(); const results = []; - for (let i = 0; i < courseOptions.length; i += 1) { - const option = courseOptions[i]; - if (!query || option.toUpperCase().startsWith(query)) { + for (let i = 0; i < normalizedOptions.length; i += 1) { + const option = normalizedOptions[i]; + const label = option.label.toUpperCase(); + const courseName = option.courseName.toUpperCase(); + const matchesQuery = + !query || + label.startsWith(query) || + label.includes(query) || + courseName.includes(query); + + if (matchesQuery) { results.push(option); if (results.length >= displayLimit) { break; @@ -68,13 +107,20 @@ export default function CourseSearchComponent({ } } return results; - }, [courseOptions, value, displayLimit]); + }, [normalizedOptions, value, displayLimit]); + + const handleSelectOption = (option) => { + setValue(option.label); + onCourseCodeChange(null, option.label); + setOpen(false); + onEnterPress(option.label); + }; const groupedOptions = React.useMemo(() => { const groups = new Map(); for (let i = 0; i < filteredOptions.length; i += 1) { const option = filteredOptions[i]; - const key = option.slice(0, 4).toUpperCase(); + const key = option.label.slice(0, 4).toUpperCase(); if (!groups.has(key)) { groups.set(key, []); } @@ -94,6 +140,7 @@ export default function CourseSearchComponent({ variant="outline" role="combobox" aria-expanded={open} + aria-controls="course-search-list" className="w-full justify-between transition-none" > {value ? value : "Add a course"} @@ -112,28 +159,36 @@ export default function CourseSearchComponent({ }} onKeyDown={handleInputKeyDown} /> - + No course found. {groupedOptions.map((group) => ( {group.items.map((option) => ( { - setValue(currentValue); - onCourseCodeChange(null, currentValue); - setOpen(false); - onEnterPress(currentValue); + key={option.label} + value={`${option.label} ${option.courseName}`.trim()} + onSelect={() => { + handleSelectOption(option); }} > - {option} +
+
{option.label}
+ {option.courseName && ( +
+ {option.courseName} +
+ )} +
))}
diff --git a/src/lib/generator/courseData.js b/src/lib/generator/courseData.js index 3a8eb64..f2d173f 100644 --- a/src/lib/generator/courseData.js +++ b/src/lib/generator/courseData.js @@ -33,11 +33,13 @@ const initializeCourseData = (courseCode) => { }; export const storeCourseData = (course) => { - const { courseCode, sections, labs, tutorials, seminars } = course; + const { courseCode, courseName, sections, labs, tutorials, seminars } = + course; initializeCourseData(courseCode); courseData[courseCode].courseCode = courseCode; + courseData[courseCode].courseName = courseName || ""; courseData[courseCode].sections.push(...sections); replaceSectionId(courseData[courseCode].sections); courseData[courseCode].labs.push(...labs); diff --git a/src/lib/generator/createCalendarEvents.js b/src/lib/generator/createCalendarEvents.js index 8f1cdbc..11116fd 100644 --- a/src/lib/generator/createCalendarEvents.js +++ b/src/lib/generator/createCalendarEvents.js @@ -44,6 +44,7 @@ export const createCalendarEvents = ( description: component.instructor, color: customColor, extendedProps: { + courseName: course.courseName || "", isPinned: component.pinned, isMain: component.isMain ?? false, }, diff --git a/src/lib/generator/timetableGeneration/utils/combinationUtils.js b/src/lib/generator/timetableGeneration/utils/combinationUtils.js index 9cbd419..f66cf5e 100644 --- a/src/lib/generator/timetableGeneration/utils/combinationUtils.js +++ b/src/lib/generator/timetableGeneration/utils/combinationUtils.js @@ -167,10 +167,18 @@ export const generateSingleCourseCombinations = (course, timeSlots) => { ]); combinations.forEach(([lab, tutorial, seminar]) => { - singleCourseCombinations.push({ + const courseCombination = { courseCode: course.courseCode, mainComponents: mainComponentGroup, secondaryComponents: { lab, tutorial, seminar }, + }; + + if (course.courseName) { + courseCombination.courseName = course.courseName; + } + + singleCourseCombinations.push({ + ...courseCombination, }); }); });