From 8bfc14e55c1bdf5acc2006096476e0b1eb1b7cc6 Mon Sep 17 00:00:00 2001 From: Xevion Date: Fri, 30 Jan 2026 23:27:54 -0600 Subject: [PATCH] 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. --- web/src/lib/components/CourseTable.svelte | 8 +- web/src/lib/course.test.ts | 91 +++++++++++++++++++++++ web/src/lib/course.ts | 19 ++++- 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/CourseTable.svelte b/web/src/lib/components/CourseTable.svelte index 4f6571f..c444fcc 100644 --- a/web/src/lib/components/CourseTable.svelte +++ b/web/src/lib/components/CourseTable.svelte @@ -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)} + Async + {:else if timeIsTBA(course)} TBA { 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"); + }); +}); diff --git a/web/src/lib/course.ts b/web/src/lib/course.ts index a4397de..de966ca 100644 --- a/web/src/lib/course.ts +++ b/web/src/lib/course.ts @@ -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; }