diff --git a/apps/web/src/app/dashboard/register/page.tsx b/apps/web/src/app/dashboard/register/page.tsx index e178ad16..8b7718bc 100644 --- a/apps/web/src/app/dashboard/register/page.tsx +++ b/apps/web/src/app/dashboard/register/page.tsx @@ -84,7 +84,7 @@ const RegisterPage = () => { } return ( -
+
{/* Mobile toggle buttons */}
diff --git a/apps/web/src/modules/course-selection/CourseSelector.tsx b/apps/web/src/modules/course-selection/CourseSelector.tsx index e95d1dc4..565f423b 100644 --- a/apps/web/src/modules/course-selection/CourseSelector.tsx +++ b/apps/web/src/modules/course-selection/CourseSelector.tsx @@ -59,6 +59,14 @@ const CourseSelector = ({ setFiltersParam(isFiltersExpanded ? "" : "true"); }; + const handleSectionHover = (section: CourseOffering | null) => { + if (section && (!section.startTime || !section.endTime)) { + setHoveredSection(null); + return; + } + setHoveredSection(section); + }; + const parentRef = React.useRef(null); const rowVirtualizer = useVirtualizer({ @@ -73,6 +81,20 @@ const CourseSelector = ({ onHover?.(hoveredSection); }, [hoveredSection, onHover]); + // https://tanstack.com/virtual/latest/docs/framework/react/examples/infinite-scroll + // biome-ignore lint/correctness/useExhaustiveDependencies: It's in Tanstack doc + useEffect(() => { + const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse(); + + if (!lastItem) { + return; + } + + if (lastItem.index >= filteredData.length - 1 && status === "CanLoadMore") { + loadMore(200); + } + }, [status, loadMore, filteredData.length, rowVirtualizer.getVirtualItems()]); + const handleSectionSelect = async (offering: CourseOffering) => { if (offering.status === "closed") { toast.error("This section is closed."); @@ -167,7 +189,7 @@ const CourseSelector = ({ selectedClassNumbers={selectedClassNumbers} onToggleExpand={toggleCourseExpansion} onSectionSelect={handleSectionSelect} - onSectionHover={setHoveredSection} + onSectionHover={handleSectionHover} />
); @@ -176,13 +198,6 @@ const CourseSelector = ({
)} - {status === "CanLoadMore" && ( -
- -
- )} {status === "LoadingMore" && (

Loading more courses...

diff --git a/apps/web/src/modules/course-selection/components/CourseSectionItem.tsx b/apps/web/src/modules/course-selection/components/CourseSectionItem.tsx index 5f59e60c..7e1b3acb 100644 --- a/apps/web/src/modules/course-selection/components/CourseSectionItem.tsx +++ b/apps/web/src/modules/course-selection/components/CourseSectionItem.tsx @@ -58,8 +58,16 @@ export const CourseSectionItem = ({
{offering.instructors.join(", ")}
- {offering.days.map((day) => day.slice(0, 3).toUpperCase()).join(", ")}{" "} - {offering.startTime} - {offering.endTime} + {offering.startTime && offering.endTime ? ( + <> + {offering.days + .map((day) => day.slice(0, 3).toUpperCase()) + .join(", ")}{" "} + {offering.startTime} - {offering.endTime} + + ) : ( + Time not available yet + )}
{offering.location ?? "TBD"}
diff --git a/apps/web/src/modules/schedule-calendar/schedule-calendar.tsx b/apps/web/src/modules/schedule-calendar/schedule-calendar.tsx index ae422a6e..bf197593 100644 --- a/apps/web/src/modules/schedule-calendar/schedule-calendar.tsx +++ b/apps/web/src/modules/schedule-calendar/schedule-calendar.tsx @@ -34,8 +34,8 @@ export interface Class { term: Term; instructors: string[]; location?: string; - startTime: string; - endTime: string; + startTime: string | undefined; + endTime: string | undefined; status: "open" | "closed" | "waitlist"; waitlistNum?: number; isCorequisite: boolean; @@ -112,92 +112,101 @@ export function ScheduleCalendar({ return ; } - const transformedClasses: Class[] = classes.map((c) => { - const offering = c.courseOffering; - const startTime = `${offering.startTime.split(":")[0]} ${offering.startTime.split(":")[1]}`; - const endTime = `${offering.endTime.split(":")[0]} ${offering.endTime.split(":")[1]}`; + const transformedClasses: Class[] = classes + .filter((c) => { + const offering = c.courseOffering; + return offering.startTime && offering.endTime; + }) + .map((c) => { + const offering = c.courseOffering; + // biome-ignore lint/style/noNonNullAssertion: we just filtered above + const startTime = `${offering.startTime!.split(":")[0]} ${offering.startTime!.split(":")[1]}`; + // biome-ignore lint/style/noNonNullAssertion: we just filtered above + const endTime = `${offering.endTime!.split(":")[0]} ${offering.endTime!.split(":")[1]}`; - // Format times like "Monday 9 15 11 15" - const times = offering.days.map((day) => { - const dayName = day.charAt(0).toUpperCase() + day.slice(1); - return `${dayName} ${startTime} ${endTime}`; - }); + // Format times like "Monday 9 15 11 15" + const times = offering.days.map((day) => { + const dayName = day.charAt(0).toUpperCase() + day.slice(1); + return `${dayName} ${startTime} ${endTime}`; + }); - const color = getColor(offering._id); + const color = getColor(offering._id); - const slots: { start: Date; end: Date }[] = []; + const slots: { start: Date; end: Date }[] = []; - // Map weekday names to 0-6 offset from start of week (Sunday = 0) - const weekdayMap: Record = { - Sunday: 0, - Monday: 1, - Tuesday: 2, - Wednesday: 3, - Thursday: 4, - Friday: 5, - Saturday: 6, - }; + // Map weekday names to 0-6 offset from start of week (Sunday = 0) + const weekdayMap: Record = { + Sunday: 0, + Monday: 1, + Tuesday: 2, + Wednesday: 3, + Thursday: 4, + Friday: 5, + Saturday: 6, + }; - // Get the start of the current week (Sunday) - const startOfCurrentWeek = startOfWeek(new Date(), { weekStartsOn: 0 }); // Sunday = 0 + // Get the start of the current week (Sunday) + const startOfCurrentWeek = startOfWeek(new Date(), { weekStartsOn: 0 }); // Sunday = 0 - for (const slot of times) { - const parts = slot.split(" "); - const day = parts[0]; - const startHour = Number(parts[1]); - const startMinute = Number(parts[2]); - const endHour = Number(parts[3]); - const endMinute = Number(parts[4]); + for (const slot of times) { + const parts = slot.split(" "); + const day = parts[0]; + const startHour = Number(parts[1]); + const startMinute = Number(parts[2]); + const endHour = Number(parts[3]); + const endMinute = Number(parts[4]); - const dayOffset = weekdayMap[day]; - if (dayOffset === undefined) { - throw new Error(`Invalid day: ${day}`); - } + const dayOffset = weekdayMap[day]; + if (dayOffset === undefined) { + throw new Error(`Invalid day: ${day}`); + } - const date = addDays(startOfCurrentWeek, dayOffset); + const date = addDays(startOfCurrentWeek, dayOffset); - const start = new Date(date); - start.setHours(startHour, startMinute, 0, 0); + const start = new Date(date); + start.setHours(startHour, startMinute, 0, 0); - const end = new Date(date); - end.setHours(endHour, endMinute, 0, 0); + const end = new Date(date); + end.setHours(endHour, endMinute, 0, 0); - slots.push({ start, end }); - } + slots.push({ start, end }); + } - return { - id: offering._id, - userCourseOfferingId: c._id, - classNumber: c.classNumber, - courseCode: offering.courseCode, - title: `${offering.courseCode} - ${offering.title}`, - color, - times: slots, - description: `${offering.instructors.join(", ")} • ${offering.section.toUpperCase()} • ${offering.term} ${offering.year}`, - section: offering.section, - year: offering.year, - term: offering.term, - instructors: offering.instructors, - location: offering.location, - startTime: offering.startTime, - endTime: offering.endTime, - status: offering.status, - waitlistNum: offering.waitlistNum, - isCorequisite: offering.isCorequisite, - corequisiteOf: offering.corequisiteOf, - }; - }); + return { + id: offering._id, + userCourseOfferingId: c._id, + classNumber: c.classNumber, + courseCode: offering.courseCode, + title: `${offering.courseCode} - ${offering.title}`, + color, + times: slots, + description: `${offering.instructors.join(", ")} • ${offering.section.toUpperCase()} • ${offering.term} ${offering.year}`, + section: offering.section, + year: offering.year, + term: offering.term, + instructors: offering.instructors, + location: offering.location, + startTime: offering.startTime, + endTime: offering.endTime, + status: offering.status, + waitlistNum: offering.waitlistNum, + isCorequisite: offering.isCorequisite, + corequisiteOf: offering.corequisiteOf, + }; + }); // Add hovered course preview - if (hoveredCourse) { + if (hoveredCourse?.startTime && hoveredCourse.endTime) { const isAlreadyAdded = classes.some( (c) => c.courseOffering._id === hoveredCourse._id, ); if (!isAlreadyAdded) { const offering = hoveredCourse; - const startTime = `${offering.startTime.split(":")[0]} ${offering.startTime.split(":")[1]}`; - const endTime = `${offering.endTime.split(":")[0]} ${offering.endTime.split(":")[1]}`; + // biome-ignore lint/style/noNonNullAssertion: we just filtered above + const startTime = `${offering.startTime!.split(":")[0]} ${offering.startTime!.split(":")[1]}`; + // biome-ignore lint/style/noNonNullAssertion: we just filtered above + const endTime = `${offering.endTime!.split(":")[0]} ${offering.endTime!.split(":")[1]}`; const times = offering.days.map((day) => { const dayName = day.charAt(0).toUpperCase() + day.slice(1); @@ -248,8 +257,8 @@ export function ScheduleCalendar({ term: offering.term, instructors: offering.instructors, location: offering.location, - startTime: offering.startTime, - endTime: offering.endTime, + startTime: offering.startTime as string, + endTime: offering.endTime as string, status: offering.status, waitlistNum: offering.waitlistNum, isCorequisite: offering.isCorequisite, diff --git a/packages/server/convex/http.ts b/packages/server/convex/http.ts index 902b97f8..02adb242 100644 --- a/packages/server/convex/http.ts +++ b/packages/server/convex/http.ts @@ -105,7 +105,7 @@ export const ZUpsertCourseWithPrerequisites = z.object({ export const ZUpsertProgramWithRequirements = z.object({ name: z.string(), - level: ZSchoolLevel, + level: ZSchoolLevel, // undergraduate or graduate school: ZSchoolName, programUrl: z.string(), requirements: z.array( @@ -130,7 +130,7 @@ export const ZUpsertProgramWithRequirements = z.object({ courseLevels: z.array( z.object({ program: z.string(), // CSCI-UA - level: z.coerce.number(), // 4 + level: z.coerce.number(), // 4 (represents any classes at 400 level) }), ), creditsRequired: z.number(), diff --git a/packages/server/convex/schemas/courseOfferings.ts b/packages/server/convex/schemas/courseOfferings.ts index 5fd3f96d..ea9dc725 100644 --- a/packages/server/convex/schemas/courseOfferings.ts +++ b/packages/server/convex/schemas/courseOfferings.ts @@ -29,8 +29,8 @@ const courseOfferings = { v.literal("sunday"), ), ), - startTime: v.string(), // 13:00 - endTime: v.string(), // 14:15 + startTime: v.optional(v.string()), // 13:00 + endTime: v.optional(v.string()), // 14:15 status: v.union( v.literal("open"), v.literal("closed"),