feat(course): distinguish async from synchronous online courses

Add logic to detect and label asynchronous online sections (INT building
with TBA times) separately from synchronous online courses. Update table
rendering to show "Async" instead of "TBA" for these sections.
This commit is contained in:
2026-01-30 23:27:54 -06:00
parent 2689587dd5
commit 8bfc14e55c
3 changed files with 115 additions and 3 deletions
+7 -1
View File
@@ -14,6 +14,7 @@ import {
formatTimeRange,
getDeliveryConcern,
getPrimaryInstructor,
isAsyncOnline,
isMeetingTimeTBA,
isTimeTBA,
openSeats,
@@ -611,7 +612,12 @@ const table = createSvelteTable({
)}
passthrough
>
{#if timeIsTBA(course)}
{#if isAsyncOnline(course)}
<span
class="text-xs text-muted-foreground/60"
>Async</span
>
{:else if timeIsTBA(course)}
<span
class="text-xs text-muted-foreground/60"
>TBA</span
+91
View File
@@ -4,6 +4,7 @@ import {
formatCreditHours,
formatDate,
formatDateShort,
formatLocationDisplay,
formatMeetingDays,
formatMeetingDaysLong,
formatMeetingDaysVerbose,
@@ -13,6 +14,7 @@ import {
formatTime,
formatTimeRange,
getPrimaryInstructor,
isAsyncOnline,
isMeetingTimeTBA,
isTimeTBA,
} from "$lib/course";
@@ -411,3 +413,92 @@ describe("formatMeetingTimesTooltip", () => {
expect(result).toContain("\n\n");
});
});
describe("isAsyncOnline", () => {
it("returns true for INT building with no times", () => {
const course = {
meetingTimes: [
makeMeetingTime({
building: "INT",
building_description: "Internet Class",
begin_time: null,
end_time: null,
}),
],
} as CourseResponse;
expect(isAsyncOnline(course)).toBe(true);
});
it("returns false for INT building with meeting times", () => {
const course = {
meetingTimes: [
makeMeetingTime({
building: "INT",
building_description: "Internet Class",
tuesday: true,
thursday: true,
begin_time: "1000",
end_time: "1115",
}),
],
} as CourseResponse;
expect(isAsyncOnline(course)).toBe(false);
});
it("returns false for non-INT building", () => {
const course = {
meetingTimes: [
makeMeetingTime({
building: "MH",
building_description: "Main Hall",
begin_time: null,
end_time: null,
}),
],
} as CourseResponse;
expect(isAsyncOnline(course)).toBe(false);
});
it("returns false for empty meeting times", () => {
const course = { meetingTimes: [] } as unknown as CourseResponse;
expect(isAsyncOnline(course)).toBe(false);
});
});
describe("formatLocationDisplay", () => {
it("returns 'Online' for INT building", () => {
const course = {
meetingTimes: [
makeMeetingTime({
building: "INT",
building_description: "Internet Class",
}),
],
campus: "9",
} as CourseResponse;
expect(formatLocationDisplay(course)).toBe("Online");
});
it("returns building and room for physical location", () => {
const course = {
meetingTimes: [
makeMeetingTime({
building: "MH",
building_description: "Main Hall",
room: "2.206",
}),
],
campus: "11",
} as CourseResponse;
expect(formatLocationDisplay(course)).toBe("MH 2.206");
});
it("returns building only when no room", () => {
const course = {
meetingTimes: [
makeMeetingTime({
building: "MH",
building_description: "Main Hall",
room: null,
}),
],
campus: "11",
} as CourseResponse;
expect(formatLocationDisplay(course)).toBe("MH");
});
});
+17 -2
View File
@@ -152,6 +152,13 @@ export function isTimeTBA(mt: DbMeetingTime): boolean {
return !mt.begin_time || mt.begin_time.length !== 4;
}
/** Check if course is asynchronous online (INT building with no meeting times) */
export function isAsyncOnline(course: CourseResponse): boolean {
if (course.meetingTimes.length === 0) return false;
const mt = course.meetingTimes[0];
return mt.building === "INT" && isTimeTBA(mt);
}
/** Format a date string to "January 20, 2026". Accepts YYYY-MM-DD or MM/DD/YYYY. */
export function formatDate(dateStr: string): string {
let year: number, month: number, day: number;
@@ -170,6 +177,8 @@ export function formatDate(dateStr: string): string {
/** Short location string from first meeting time: "MH 2.206" or campus fallback */
export function formatLocation(course: CourseResponse): string | null {
for (const mt of course.meetingTimes) {
// Skip INT building - handled by formatLocationDisplay
if (mt.building === "INT") continue;
if (mt.building && mt.room) return `${mt.building} ${mt.room}`;
if (mt.building) return mt.building;
}
@@ -307,13 +316,19 @@ export function concernAccentColor(concern: DeliveryConcern): string | null {
/**
* Location display text for the table cell.
* Falls back to "Online" for online courses instead of showing a dash.
* Shows "Online" for internet class (INT building) or other online courses.
*/
export function formatLocationDisplay(course: CourseResponse): string | null {
// Check for Internet Class building first
const hasIntBuilding = course.meetingTimes.some((mt) => mt.building === "INT");
if (hasIntBuilding) return "Online";
const loc = formatLocation(course);
if (loc) return loc;
const concern = getDeliveryConcern(course);
if (concern === "online") return "Online";
if (concern === "online" || concern === "internet") return "Online";
return null;
}