Skip to content
Draft
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
35 changes: 14 additions & 21 deletions client/src/components/course-schedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import { twMerge } from 'tailwind-merge';
import * as buildingCodes from '../assets/building-codes.json';
import * as buildingCoordinates from '../assets/building-coordinates.json';
import { type IcsEventOptions, sanitizeForFilename } from '../lib/calendar';
import type { Block, Schedule, TimeBlock } from '../lib/types';
import type { Block, Schedule, Season, TimeBlock } from '../lib/types';
import type { Course } from '../lib/types';
import {
formatDisplayTime,
formatTerm,
getCurrentTerm,
groupBy,
mapValues,
parseTerm,
sortBy,
sortTerms,
uniq,
Expand All @@ -36,11 +38,14 @@ const DAY_ORDER = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

const DEFAULT_MEETING_COUNT = 13;

const TERM_START_CONFIG = {
const TERM_START_CONFIG: Record<
Season,
{ startMonth: number; offsetDays: number }
> = {
Winter: { startMonth: 1, offsetDays: 6 },
Summer: { startMonth: 5, offsetDays: 6 },
Fall: { startMonth: 9, offsetDays: 6 },
} as const;
};

type ScheduleBlock = Omit<Block, 'timeblocks' | 'location' | 'display'> & {
location: string;
Expand All @@ -54,8 +59,6 @@ type RepeatingBlock = {
endTime: string;
};

type TermSeason = keyof typeof TERM_START_CONFIG;

const VSBtimeToDisplay = (time: string) => {
const totalMinutes = parseInt(time, 10);

Expand All @@ -71,18 +74,6 @@ const VSBtimeToDisplay = (time: string) => {
.padStart(2, '0')}`;
};

const parseTermSeason = (
term: string
): { season: TermSeason; year: number } | null => {
const match = term.match(/^(Winter|Summer|Fall)\s+(\d{4})$/);

if (!match) return null;

const [, season, year] = match;

return { season: season as TermSeason, year: parseInt(year, 10) };
};

const vsbDayToJsDay = (day: string): number | null => {
const parsed = parseInt(day, 10);

Expand All @@ -99,10 +90,10 @@ const getFirstOccurrenceForTermDay = (
term: string,
day: string
): Date | null => {
const termInfo = parseTermSeason(term);
const termInfo = parseTerm(term);
const jsDay = vsbDayToJsDay(day);

if (!termInfo || jsDay === null) return null;
if (jsDay === null || Number.isNaN(termInfo.year)) return null;

const { season, year } = termInfo;
const { startMonth, offsetDays } = TERM_START_CONFIG[season];
Expand Down Expand Up @@ -452,8 +443,10 @@ const ScheduleRow = ({ block, course, term }: ScheduleRowProps) => {
};

const getDefaultTerm = (offeredTerms: string[]) => {
const currentTerm = getCurrentTerm();
return offeredTerms.includes(currentTerm) ? currentTerm : offeredTerms.at(0);
const currentTermStr = formatTerm(getCurrentTerm());
return offeredTerms.includes(currentTermStr)
? currentTermStr
: offeredTerms.at(0);
};

type CourseScheduleProps = {
Expand Down
9 changes: 6 additions & 3 deletions client/src/components/course-terms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { twMerge } from 'tailwind-merge';
import type { Course } from '../lib/types';
import {
compareTerms,
formatTerm,
getCurrentTerms,
groupCurrentCourseTermInstructors,
parseTerm,
} from '../lib/utils';
import { Highlight } from './highlight';
import { Tooltip } from './tooltip';
Expand All @@ -25,7 +27,7 @@ type SeasonIconProps = {
const SeasonIcon = ({ variant, term }: SeasonIconProps) => {
const size = variantToSize(variant);

const season = term.split(' ')[0].toLowerCase();
const season = parseTerm(term).season.toLowerCase();

const icons: Record<string, JSX.Element> = {
fall: <Leaf size={size} color='brown' />,
Expand Down Expand Up @@ -74,8 +76,9 @@ export const CourseTerms = ({ course, variant, query }: CourseTermsProps) => {
setExpandedState(initialExpandedState());
}, [course]);

const currentTermStrings = getCurrentTerms().map(formatTerm);
const currentlyOfferedTerms = course.terms.filter((c) =>
getCurrentTerms().includes(c)
currentTermStrings.includes(c)
);

if (currentlyOfferedTerms.length === 0)
Expand All @@ -100,7 +103,7 @@ export const CourseTerms = ({ course, variant, query }: CourseTermsProps) => {
{Object.entries(instructorGroups)
.sort((a, b) => compareTerms(a[0], b[0]))
.map(([term, instructors], i) => {
const season = term.split(' ')[0].toLowerCase();
const season = parseTerm(term).season.toLowerCase();
return (
<div className='relative' key={term}>
<div
Expand Down
12 changes: 6 additions & 6 deletions client/src/components/final-exam-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { twMerge } from 'tailwind-merge';
import finalExamsData from '../assets/final-exams.json';
import { sanitizeForFilename } from '../lib/calendar';
import type { Course, FinalExam, FinalExamGroup } from '../lib/types';
import { getCurrentTerm } from '../lib/utils';
import { formatTerm, getCurrentTerm } from '../lib/utils';
import { AddToCalendarButton } from './add-to-calendar-button';

const finalExams = finalExamsData as FinalExamGroup;
Expand Down Expand Up @@ -117,9 +117,9 @@ type FinalExamRowProps = {
};

export const FinalExamRow = ({ course, className }: FinalExamRowProps) => {
const currentTerm = getCurrentTerm();
const currentTermStr = formatTerm(getCurrentTerm());

if (finalExams.term !== currentTerm) {
if (finalExams.term !== currentTermStr) {
return null;
}

Expand Down Expand Up @@ -148,7 +148,7 @@ export const FinalExamRow = ({ course, className }: FinalExamRowProps) => {
Final Exam
</p>
<p className='text-lg font-semibold text-gray-900 dark:text-gray-100'>
{currentTerm}
{currentTermStr}
</p>
</div>
<p className='text-sm text-gray-600 dark:text-gray-300'>
Expand Down Expand Up @@ -180,9 +180,9 @@ export const FinalExamRow = ({ course, className }: FinalExamRowProps) => {
.filter(Boolean)
.join('\n');

const filename = `${sanitizeForFilename(`${examId}-final-exam-${currentTerm}`)}.ics`;
const filename = `${sanitizeForFilename(`${examId}-final-exam-${currentTermStr}`)}.ics`;

const uid = `${sanitizeForFilename(`${course._id}-${exam.startTime}-${exam.endTime}-${currentTerm}`).slice(0, 64)}@mcgill.courses`;
const uid = `${sanitizeForFilename(`${course._id}-${exam.startTime}-${exam.endTime}-${currentTermStr}`).slice(0, 64)}@mcgill.courses`;

const calendarPayload = {
filename,
Expand Down
10 changes: 4 additions & 6 deletions client/src/components/gpa-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'recharts';

import type { CourseAverage, Grade, Instructor } from '../lib/types';
import { compareTerms } from '../lib/utils';
import { compareTerms, parseTerm } from '../lib/utils';

const gradeToGPA: Record<Grade, number> = {
A: 4.0,
Expand Down Expand Up @@ -39,11 +39,9 @@ type DataPoint = {
instructors: string[];
};

const formatShortTerm = (term: string): string => {
const [season, year] = term.split(' ');
const seasonAbbrev = season[0];
const yearAbbrev = year?.slice(-2) ?? '';
return `${seasonAbbrev}${yearAbbrev}`;
const formatShortTerm = (termStr: string): string => {
const { season, year } = parseTerm(termStr);
return `${season[0]}${String(year).slice(-2)}`;
};

const calculateTrendLine = (
Expand Down
7 changes: 7 additions & 0 deletions client/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@
Generated by typeshare 1.13.3
*/

export type Season = 'Winter' | 'Summer' | 'Fall';

export interface Term {
season: Season;
year: number;
}

export interface AddOrUpdateReviewBody {
/** The review content/text. */
content: string;
Expand Down
37 changes: 19 additions & 18 deletions client/src/lib/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
spliceCourseCode,
stripColonPrefix,
sum,
term,
timeSince,
uniq,
uniqBy,
Expand Down Expand Up @@ -146,55 +147,55 @@ describe('getCurrentTerms', () => {
vi.setSystemTime(new Date('2024-06-15'));

expect(getCurrentTerms()).toEqual([
'Summer 2024',
'Fall 2024',
'Winter 2025',
term('Summer', 2024),
term('Fall', 2024),
term('Winter', 2025),
]);
});

it('returns fall-winter-summer for August-December', () => {
vi.setSystemTime(new Date('2024-09-15'));

expect(getCurrentTerms()).toEqual([
'Fall 2024',
'Winter 2025',
'Summer 2025',
term('Fall', 2024),
term('Winter', 2025),
term('Summer', 2025),
]);
});

it('returns fall-winter-summer for January-April', () => {
vi.setSystemTime(new Date('2024-03-15'));

expect(getCurrentTerms()).toEqual([
'Fall 2023',
'Winter 2024',
'Summer 2024',
term('Fall', 2023),
term('Winter', 2024),
term('Summer', 2024),
]);
});

it('handles edge case dates', () => {
vi.setSystemTime(new Date('2024-05-01T12:00:00Z'));

expect(getCurrentTerms()).toEqual([
'Summer 2024',
'Fall 2024',
'Winter 2025',
term('Summer', 2024),
term('Fall', 2024),
term('Winter', 2025),
]);

vi.setSystemTime(new Date('2024-08-01T12:00:00Z'));

expect(getCurrentTerms()).toEqual([
'Fall 2024',
'Winter 2025',
'Summer 2025',
term('Fall', 2024),
term('Winter', 2025),
term('Summer', 2025),
]);

vi.setSystemTime(new Date('2024-01-01T12:00:00Z'));

expect(getCurrentTerms()).toEqual([
'Fall 2023',
'Winter 2024',
'Summer 2024',
term('Fall', 2023),
term('Winter', 2024),
term('Summer', 2024),
]);
});
});
Expand Down
Loading