Skip to content

Commit 9de9986

Browse files
chenxin-yanxyspg
andauthored
fix: make start time and end time optional for courses (#100)
Co-authored-by: Kang Jiaming <[email protected]>
1 parent bc9ae53 commit 9de9986

File tree

6 files changed

+117
-85
lines changed

6 files changed

+117
-85
lines changed

apps/web/src/app/dashboard/register/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const RegisterPage = () => {
8484
}
8585

8686
return (
87-
<div className="flex flex-col gap-4 h-[calc(100vh-(--spacing(16))-(--spacing(12)))] w-full">
87+
<div className="flex flex-col gap-4 w-full">
8888
{/* Mobile toggle buttons */}
8989
<div className="md:hidden shrink-0 p-2">
9090
<Selector value={mobileView} onValueChange={setMobileView} />

apps/web/src/modules/course-selection/CourseSelector.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ const CourseSelector = ({
5959
setFiltersParam(isFiltersExpanded ? "" : "true");
6060
};
6161

62+
const handleSectionHover = (section: CourseOffering | null) => {
63+
if (section && (!section.startTime || !section.endTime)) {
64+
setHoveredSection(null);
65+
return;
66+
}
67+
setHoveredSection(section);
68+
};
69+
6270
const parentRef = React.useRef<HTMLDivElement>(null);
6371

6472
const rowVirtualizer = useVirtualizer({
@@ -73,6 +81,20 @@ const CourseSelector = ({
7381
onHover?.(hoveredSection);
7482
}, [hoveredSection, onHover]);
7583

84+
// https://tanstack.com/virtual/latest/docs/framework/react/examples/infinite-scroll
85+
// biome-ignore lint/correctness/useExhaustiveDependencies: It's in Tanstack doc
86+
useEffect(() => {
87+
const [lastItem] = [...rowVirtualizer.getVirtualItems()].reverse();
88+
89+
if (!lastItem) {
90+
return;
91+
}
92+
93+
if (lastItem.index >= filteredData.length - 1 && status === "CanLoadMore") {
94+
loadMore(200);
95+
}
96+
}, [status, loadMore, filteredData.length, rowVirtualizer.getVirtualItems()]);
97+
7698
const handleSectionSelect = async (offering: CourseOffering) => {
7799
if (offering.status === "closed") {
78100
toast.error("This section is closed.");
@@ -167,7 +189,7 @@ const CourseSelector = ({
167189
selectedClassNumbers={selectedClassNumbers}
168190
onToggleExpand={toggleCourseExpansion}
169191
onSectionSelect={handleSectionSelect}
170-
onSectionHover={setHoveredSection}
192+
onSectionHover={handleSectionHover}
171193
/>
172194
</div>
173195
);
@@ -176,13 +198,6 @@ const CourseSelector = ({
176198
</div>
177199
)}
178200

179-
{status === "CanLoadMore" && (
180-
<div className="flex justify-center py-4 shrink-0">
181-
<Button onClick={() => loadMore(200)} variant="outline">
182-
Load More
183-
</Button>
184-
</div>
185-
)}
186201
{status === "LoadingMore" && (
187202
<div className="flex justify-center py-4 shrink-0">
188203
<p className="text-gray-500">Loading more courses...</p>

apps/web/src/modules/course-selection/components/CourseSectionItem.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,16 @@ export const CourseSectionItem = ({
5858
<div className="text-xs text-muted-foreground space-y-1">
5959
<div>{offering.instructors.join(", ")}</div>
6060
<div>
61-
{offering.days.map((day) => day.slice(0, 3).toUpperCase()).join(", ")}{" "}
62-
{offering.startTime} - {offering.endTime}
61+
{offering.startTime && offering.endTime ? (
62+
<>
63+
{offering.days
64+
.map((day) => day.slice(0, 3).toUpperCase())
65+
.join(", ")}{" "}
66+
{offering.startTime} - {offering.endTime}
67+
</>
68+
) : (
69+
<span className="italic">Time not available yet</span>
70+
)}
6371
</div>
6472
<div>{offering.location ?? "TBD"}</div>
6573
<div className="capitalize">

apps/web/src/modules/schedule-calendar/schedule-calendar.tsx

Lines changed: 79 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export interface Class {
3434
term: Term;
3535
instructors: string[];
3636
location?: string;
37-
startTime: string;
38-
endTime: string;
37+
startTime: string | undefined;
38+
endTime: string | undefined;
3939
status: "open" | "closed" | "waitlist";
4040
waitlistNum?: number;
4141
isCorequisite: boolean;
@@ -112,92 +112,101 @@ export function ScheduleCalendar({
112112
return <Skeleton className="h-full w-full rounded-lg" />;
113113
}
114114

115-
const transformedClasses: Class[] = classes.map((c) => {
116-
const offering = c.courseOffering;
117-
const startTime = `${offering.startTime.split(":")[0]} ${offering.startTime.split(":")[1]}`;
118-
const endTime = `${offering.endTime.split(":")[0]} ${offering.endTime.split(":")[1]}`;
115+
const transformedClasses: Class[] = classes
116+
.filter((c) => {
117+
const offering = c.courseOffering;
118+
return offering.startTime && offering.endTime;
119+
})
120+
.map((c) => {
121+
const offering = c.courseOffering;
122+
// biome-ignore lint/style/noNonNullAssertion: we just filtered above
123+
const startTime = `${offering.startTime!.split(":")[0]} ${offering.startTime!.split(":")[1]}`;
124+
// biome-ignore lint/style/noNonNullAssertion: we just filtered above
125+
const endTime = `${offering.endTime!.split(":")[0]} ${offering.endTime!.split(":")[1]}`;
119126

120-
// Format times like "Monday 9 15 11 15"
121-
const times = offering.days.map((day) => {
122-
const dayName = day.charAt(0).toUpperCase() + day.slice(1);
123-
return `${dayName} ${startTime} ${endTime}`;
124-
});
127+
// Format times like "Monday 9 15 11 15"
128+
const times = offering.days.map((day) => {
129+
const dayName = day.charAt(0).toUpperCase() + day.slice(1);
130+
return `${dayName} ${startTime} ${endTime}`;
131+
});
125132

126-
const color = getColor(offering._id);
133+
const color = getColor(offering._id);
127134

128-
const slots: { start: Date; end: Date }[] = [];
135+
const slots: { start: Date; end: Date }[] = [];
129136

130-
// Map weekday names to 0-6 offset from start of week (Sunday = 0)
131-
const weekdayMap: Record<string, number> = {
132-
Sunday: 0,
133-
Monday: 1,
134-
Tuesday: 2,
135-
Wednesday: 3,
136-
Thursday: 4,
137-
Friday: 5,
138-
Saturday: 6,
139-
};
137+
// Map weekday names to 0-6 offset from start of week (Sunday = 0)
138+
const weekdayMap: Record<string, number> = {
139+
Sunday: 0,
140+
Monday: 1,
141+
Tuesday: 2,
142+
Wednesday: 3,
143+
Thursday: 4,
144+
Friday: 5,
145+
Saturday: 6,
146+
};
140147

141-
// Get the start of the current week (Sunday)
142-
const startOfCurrentWeek = startOfWeek(new Date(), { weekStartsOn: 0 }); // Sunday = 0
148+
// Get the start of the current week (Sunday)
149+
const startOfCurrentWeek = startOfWeek(new Date(), { weekStartsOn: 0 }); // Sunday = 0
143150

144-
for (const slot of times) {
145-
const parts = slot.split(" ");
146-
const day = parts[0];
147-
const startHour = Number(parts[1]);
148-
const startMinute = Number(parts[2]);
149-
const endHour = Number(parts[3]);
150-
const endMinute = Number(parts[4]);
151+
for (const slot of times) {
152+
const parts = slot.split(" ");
153+
const day = parts[0];
154+
const startHour = Number(parts[1]);
155+
const startMinute = Number(parts[2]);
156+
const endHour = Number(parts[3]);
157+
const endMinute = Number(parts[4]);
151158

152-
const dayOffset = weekdayMap[day];
153-
if (dayOffset === undefined) {
154-
throw new Error(`Invalid day: ${day}`);
155-
}
159+
const dayOffset = weekdayMap[day];
160+
if (dayOffset === undefined) {
161+
throw new Error(`Invalid day: ${day}`);
162+
}
156163

157-
const date = addDays(startOfCurrentWeek, dayOffset);
164+
const date = addDays(startOfCurrentWeek, dayOffset);
158165

159-
const start = new Date(date);
160-
start.setHours(startHour, startMinute, 0, 0);
166+
const start = new Date(date);
167+
start.setHours(startHour, startMinute, 0, 0);
161168

162-
const end = new Date(date);
163-
end.setHours(endHour, endMinute, 0, 0);
169+
const end = new Date(date);
170+
end.setHours(endHour, endMinute, 0, 0);
164171

165-
slots.push({ start, end });
166-
}
172+
slots.push({ start, end });
173+
}
167174

168-
return {
169-
id: offering._id,
170-
userCourseOfferingId: c._id,
171-
classNumber: c.classNumber,
172-
courseCode: offering.courseCode,
173-
title: `${offering.courseCode} - ${offering.title}`,
174-
color,
175-
times: slots,
176-
description: `${offering.instructors.join(", ")}${offering.section.toUpperCase()}${offering.term} ${offering.year}`,
177-
section: offering.section,
178-
year: offering.year,
179-
term: offering.term,
180-
instructors: offering.instructors,
181-
location: offering.location,
182-
startTime: offering.startTime,
183-
endTime: offering.endTime,
184-
status: offering.status,
185-
waitlistNum: offering.waitlistNum,
186-
isCorequisite: offering.isCorequisite,
187-
corequisiteOf: offering.corequisiteOf,
188-
};
189-
});
175+
return {
176+
id: offering._id,
177+
userCourseOfferingId: c._id,
178+
classNumber: c.classNumber,
179+
courseCode: offering.courseCode,
180+
title: `${offering.courseCode} - ${offering.title}`,
181+
color,
182+
times: slots,
183+
description: `${offering.instructors.join(", ")}${offering.section.toUpperCase()}${offering.term} ${offering.year}`,
184+
section: offering.section,
185+
year: offering.year,
186+
term: offering.term,
187+
instructors: offering.instructors,
188+
location: offering.location,
189+
startTime: offering.startTime,
190+
endTime: offering.endTime,
191+
status: offering.status,
192+
waitlistNum: offering.waitlistNum,
193+
isCorequisite: offering.isCorequisite,
194+
corequisiteOf: offering.corequisiteOf,
195+
};
196+
});
190197

191198
// Add hovered course preview
192-
if (hoveredCourse) {
199+
if (hoveredCourse?.startTime && hoveredCourse.endTime) {
193200
const isAlreadyAdded = classes.some(
194201
(c) => c.courseOffering._id === hoveredCourse._id,
195202
);
196203

197204
if (!isAlreadyAdded) {
198205
const offering = hoveredCourse;
199-
const startTime = `${offering.startTime.split(":")[0]} ${offering.startTime.split(":")[1]}`;
200-
const endTime = `${offering.endTime.split(":")[0]} ${offering.endTime.split(":")[1]}`;
206+
// biome-ignore lint/style/noNonNullAssertion: we just filtered above
207+
const startTime = `${offering.startTime!.split(":")[0]} ${offering.startTime!.split(":")[1]}`;
208+
// biome-ignore lint/style/noNonNullAssertion: we just filtered above
209+
const endTime = `${offering.endTime!.split(":")[0]} ${offering.endTime!.split(":")[1]}`;
201210

202211
const times = offering.days.map((day) => {
203212
const dayName = day.charAt(0).toUpperCase() + day.slice(1);
@@ -248,8 +257,8 @@ export function ScheduleCalendar({
248257
term: offering.term,
249258
instructors: offering.instructors,
250259
location: offering.location,
251-
startTime: offering.startTime,
252-
endTime: offering.endTime,
260+
startTime: offering.startTime as string,
261+
endTime: offering.endTime as string,
253262
status: offering.status,
254263
waitlistNum: offering.waitlistNum,
255264
isCorequisite: offering.isCorequisite,

packages/server/convex/http.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export const ZUpsertCourseWithPrerequisites = z.object({
105105

106106
export const ZUpsertProgramWithRequirements = z.object({
107107
name: z.string(),
108-
level: ZSchoolLevel,
108+
level: ZSchoolLevel, // undergraduate or graduate
109109
school: ZSchoolName,
110110
programUrl: z.string(),
111111
requirements: z.array(
@@ -130,7 +130,7 @@ export const ZUpsertProgramWithRequirements = z.object({
130130
courseLevels: z.array(
131131
z.object({
132132
program: z.string(), // CSCI-UA
133-
level: z.coerce.number(), // 4
133+
level: z.coerce.number(), // 4 (represents any classes at 400 level)
134134
}),
135135
),
136136
creditsRequired: z.number(),

packages/server/convex/schemas/courseOfferings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ const courseOfferings = {
2929
v.literal("sunday"),
3030
),
3131
),
32-
startTime: v.string(), // 13:00
33-
endTime: v.string(), // 14:15
32+
startTime: v.optional(v.string()), // 13:00
33+
endTime: v.optional(v.string()), // 14:15
3434
status: v.union(
3535
v.literal("open"),
3636
v.literal("closed"),

0 commit comments

Comments
 (0)