multiple schedule suppport kinda

This commit is contained in:
Sriram Hariharan
2023-03-15 23:54:07 -05:00
parent 6d4a4307cf
commit 6afd372945
30 changed files with 224 additions and 155 deletions

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Course, ScrapedRow } from 'src/shared/types/Course';
import { useKeyPress } from '../hooks/useKeyPress';
import useUserSchedules from '../hooks/useUserSchedules';
import { CourseCatalogScraper } from '../lib/CourseCatalogScraper';
import getCourseTableRows from '../lib/getCourseTableRows';
import { SiteSupport } from '../lib/getSiteSupport';
@@ -43,6 +44,9 @@ export default function CourseCatalogMain({ support }: Props) {
setRows([...rows, ...newRows]);
};
const schedules = useUserSchedules();
const [activeSchedule] = schedules;
const handleRowButtonClick = (course: Course) => () => {
setSelectedCourse(course);
};
@@ -67,11 +71,18 @@ export default function CourseCatalogMain({ support }: Props) {
key={row.course.uniqueId}
row={row}
isSelected={row.course.uniqueId === selectedCourse?.uniqueId}
isInActiveSchedule={Boolean(activeSchedule?.containsCourse(row.course))}
onClick={handleRowButtonClick(row.course)}
/>
);
})}
{selectedCourse && <CoursePopup course={selectedCourse} onClose={handleClearSelectedCourse} />}
{selectedCourse && (
<CoursePopup
course={selectedCourse}
activeSchedule={activeSchedule}
onClose={handleClearSelectedCourse}
/>
)}
<AutoLoad addRows={addRows} />
</ExtensionRoot>
);

View File

@@ -1,6 +1,8 @@
import React from 'react';
import { bMessenger } from 'src/shared/messages';
import { userScheduleStore } from 'src/shared/storage/userScheduleStore';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import { Button } from 'src/views/components/common/Button/Button';
import Card from 'src/views/components/common/Card/Card';
import Icon from 'src/views/components/common/Icon/Icon';
@@ -8,6 +10,7 @@ import Text from 'src/views/components/common/Text/Text';
import styles from './CourseButtons.module.scss';
type Props = {
activeSchedule?: UserSchedule;
course: Course;
};
@@ -17,7 +20,7 @@ const { openNewTab } = bMessenger;
* This component displays the buttons for the course info popup, that allow the user to either
* navigate to other pages that are useful for the course, or to do actions on the current course.
*/
export default function CourseButtons({ course }: Props) {
export default function CourseButtons({ course, activeSchedule }: Props) {
const openRateMyProfessorURL = () => {
const primaryInstructor = course.instructors?.[0];
if (!primaryInstructor) return;
@@ -61,6 +64,17 @@ export default function CourseButtons({ course }: Props) {
openNewTab({ url: url.toString() });
};
const saveCourse = async () => {
const schedules = await userScheduleStore.get('schedules');
const active = schedules.find(schedule => schedule.id === activeSchedule?.id);
if (!active) return;
active.addCourse(course);
await userScheduleStore.set('schedules', schedules);
};
return (
<Card className={styles.container}>
<Button
@@ -86,7 +100,7 @@ export default function CourseButtons({ course }: Props) {
</Text>
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
</Button>
<Button type='success' className={styles.button}>
<Button disabled={!activeSchedule} onClick={saveCourse} type='success' className={styles.button}>
<Text size='medium' weight='regular' color='white'>
Save
</Text>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import Card from 'src/views/components/common/Card/Card';
import Divider from 'src/views/components/common/Divider/Divider';
import Icon from 'src/views/components/common/Icon/Icon';
@@ -10,6 +11,7 @@ import styles from './CourseHeader.module.scss';
type Props = {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
};
@@ -17,7 +19,7 @@ type Props = {
* This component displays the header of the course info popup.
* It displays the course name, unique id, instructors, and schedule, all formatted nicely.
*/
export default function CourseHeader({ course, onClose }: Props) {
export default function CourseHeader({ course, activeSchedule, onClose }: Props) {
const getBuildingUrl = (building?: string): string | undefined => {
if (!building) return undefined;
return `https://utdirect.utexas.edu/apps/campus/buildings/nlogon/maps/UTM/${building}/`;

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import Popup from '../../common/Popup/Popup';
import CourseDescription from './CourseDescription/CourseDescription';
import CourseHeader from './CourseHeader/CourseHeader';
@@ -8,16 +9,17 @@ import GradeDistribution from './GradeDistribution/GradeDistribution';
interface Props {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
}
/**
* The popup that appears when the user clicks on a course for more details.
*/
export default function CoursePopup({ course, onClose }: Props) {
export default function CoursePopup({ course, activeSchedule, onClose }: Props) {
return (
<Popup className={styles.popup} overlay onClose={onClose}>
<CourseHeader course={course} onClose={onClose} />
<CourseHeader course={course} activeSchedule={activeSchedule} onClose={onClose} />
<CourseDescription course={course} />
<GradeDistribution course={course} />
</Popup>

View File

@@ -137,6 +137,7 @@ export default function GradeDistribution({ course }: Props) {
useEffect(() => {
queryAggregateDistribution(course)
.then(([distribution, semesters]) => {
console.log('.then -> distribution, semesters:', distribution, semesters);
setSemesters(semesters);
updateChart(distribution);
setStatus(DataStatus.FOUND);

View File

@@ -16,23 +16,8 @@ const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD'];
export default function RecruitmentBanner() {
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const shouldShowBanner = (): boolean => {
const params = ['fos_fl', 'fos_cn'];
let department = '';
params.forEach(p => {
const param = new URLSearchParams(window.location.search).get(p);
if (param) {
department = param;
}
});
if (!department) {
return false;
}
return RECRUIT_FROM_DEPARTMENTS.includes(department);
};
useEffect(() => {
if (!shouldShowBanner()) {
if (!canRecruitFrom()) {
return;
}
const container = document.createElement('div');
@@ -64,3 +49,18 @@ export default function RecruitmentBanner() {
container
);
}
export function canRecruitFrom(): boolean {
const params = ['fos_fl', 'fos_cn'];
let department = '';
params.forEach(p => {
const param = new URLSearchParams(window.location.search).get(p);
if (param) {
department = param;
}
});
if (!department) {
return false;
}
return RECRUIT_FROM_DEPARTMENTS.includes(department);
}

View File

@@ -11,3 +11,10 @@
box-shadow: none !important;
}
}
.inActiveSchedule {
* {
color: $turtle_pond !important;
font-weight: bold !important;
}
}

View File

@@ -9,13 +9,17 @@ interface Props {
isSelected: boolean;
row: ScrapedRow;
onClick: (...args: any[]) => any;
/**
* Whether the course is in the user' active schedule.
*/
isInActiveSchedule: boolean;
}
/**
* This component is injected into each row of the course catalog table.
* @returns a react portal to the new td in the column or null if the column has not been created yet.
*/
export default function TableRow({ row, isSelected, onClick }: Props): JSX.Element | null {
export default function TableRow({ row, isSelected, isInActiveSchedule, onClick }: Props): JSX.Element | null {
const [container, setContainer] = useState<HTMLTableCellElement | null>(null);
const { element, course } = row;
@@ -35,6 +39,10 @@ export default function TableRow({ row, isSelected, onClick }: Props): JSX.Eleme
element.classList[isSelected ? 'add' : 'remove'](styles.selectedRow);
}, [isSelected, element.classList]);
useEffect(() => {
element.classList[isInActiveSchedule ? 'add' : 'remove'](styles.inActiveSchedule);
}, [isInActiveSchedule, element.classList]);
if (!container) {
return null;
}

View File

@@ -0,0 +1,26 @@
import { Serialized } from 'chrome-extension-toolkit';
import { useEffect, useState } from 'react';
import { userScheduleStore } from 'src/shared/storage/userScheduleStore';
import { UserSchedule } from 'src/shared/types/UserSchedule';
export default function useUserSchedules(): UserSchedule[] {
const [schedules, setSchedules] = useState<UserSchedule[]>([]);
useEffect(() => {
function updateSchedules(schedules: Serialized<UserSchedule>[]) {
setSchedules(schedules.map(s => new UserSchedule(s)));
}
userScheduleStore.get('schedules').then(updateSchedules);
const listener = userScheduleStore.listen('schedules', ({ newValue }) => {
updateSchedules(newValue);
});
return () => {
userScheduleStore.removeListener(listener);
};
}, []);
return schedules;
}

View File

@@ -11,7 +11,7 @@ if (!support) {
throw new Error('UT Registration Plus does not support this page, even though it should...');
}
if (isExtensionPopup()) {
if (support === SiteSupport.EXTENSION_POPUP) {
render(<PopupMain />, document.getElementById('root'));
}

View File

@@ -78,6 +78,7 @@ export class CourseCatalogScraper {
number,
status,
isReserved,
creditHours: this.getCreditHours(number),
schedule: this.getSchedule(row),
registerURL: this.getRegisterURL(row),
url: this.getURL(row),
@@ -112,6 +113,15 @@ export class CourseCatalogScraper {
return [courseName, department, number];
}
/**
* Gets how many credit hours the course is worth
* @param number the course number, CS 314H
* @return the number of credit hours the course is worth
*/
getCreditHours(number: string): number {
return Number(number.split('')[0]);
}
/**
* Scrape the Unique ID from the course catalog table row
* @param row the row of the course catalog table

View File

@@ -37,9 +37,17 @@ export async function queryAggregateDistribution(course: Course): Promise<[Distr
F: row.f,
};
const semesters: Semester[] = row.semesters.split(',').map((sem: string) => {
// the db file for some reason has duplicate semesters, so we use a set to remove duplicates
const rawSemesters = new Set<string>();
row.semesters.split(',').forEach((sem: string) => {
rawSemesters.add(sem);
});
const semesters: Semester[] = [];
rawSemesters.forEach((sem: string) => {
const [season, year] = sem.split(' ');
return { year: parseInt(year, 10), season: season as Semester['season'] };
semesters.push({ year: parseInt(year, 10), season: season as Semester['season'] });
});
return [distribution, semesters];

View File

@@ -1,3 +1,5 @@
import { isExtensionPopup } from 'chrome-extension-toolkit';
/**
* An enum that represents the different types of pages that we support
* a given url/page can correspond to many of these enum values
@@ -7,6 +9,7 @@ export enum SiteSupport {
COURSE_CATALOG_DETAILS = 'COURSE_CATALOG_DETAILS',
UT_PLANNER = 'UT_PLANNER',
WAITLIST = 'WAITLIST',
EXTENSION_POPUP = 'EXTENSION_POPUP',
}
/**
@@ -15,6 +18,9 @@ export enum SiteSupport {
* @returns a list of page types that the current page is
*/
export default function getSiteSupport(url: string): SiteSupport | null {
if (isExtensionPopup()) {
return SiteSupport.EXTENSION_POPUP;
}
if (url.includes('utexas.collegescheduler.com')) {
return SiteSupport.UT_PLANNER;
}