Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/app/dashboard/register/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const RegisterPage = () => {
}

return (
<div className="flex flex-col gap-4 h-[calc(100vh-(--spacing(16))-(--spacing(12)))] w-full">
<div className="flex flex-col gap-4 w-full">
{/* Mobile toggle buttons */}
<div className="md:hidden shrink-0 p-2">
<Selector value={mobileView} onValueChange={setMobileView} />
Expand Down
31 changes: 23 additions & 8 deletions apps/web/src/modules/course-selection/CourseSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null);

const rowVirtualizer = useVirtualizer({
Expand All @@ -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.");
Expand Down Expand Up @@ -167,7 +189,7 @@ const CourseSelector = ({
selectedClassNumbers={selectedClassNumbers}
onToggleExpand={toggleCourseExpansion}
onSectionSelect={handleSectionSelect}
onSectionHover={setHoveredSection}
onSectionHover={handleSectionHover}
/>
</div>
);
Expand All @@ -176,13 +198,6 @@ const CourseSelector = ({
</div>
)}

{status === "CanLoadMore" && (
<div className="flex justify-center py-4 shrink-0">
<Button onClick={() => loadMore(200)} variant="outline">
Load More
</Button>
</div>
)}
{status === "LoadingMore" && (
<div className="flex justify-center py-4 shrink-0">
<p className="text-gray-500">Loading more courses...</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,16 @@ export const CourseSectionItem = ({
<div className="text-xs text-muted-foreground space-y-1">
<div>{offering.instructors.join(", ")}</div>
<div>
{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}
</>
) : (
<span className="italic">Time not available yet</span>
)}
</div>
<div>{offering.location ?? "TBD"}</div>
<div className="capitalize">
Expand Down
149 changes: 79 additions & 70 deletions apps/web/src/modules/schedule-calendar/schedule-calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -112,92 +112,101 @@ export function ScheduleCalendar({
return <Skeleton className="h-full w-full rounded-lg" />;
}

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<string, number> = {
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<string, number> = {
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);
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/server/convex/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(),
Expand Down
4 changes: 2 additions & 2 deletions packages/server/convex/schemas/courseOfferings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down