added ExtensionRoot for consistent styling across injected components, and added a bunch of comments for all the added types and classes
This commit is contained in:
@@ -10,7 +10,6 @@ export type Instructor = {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
middleInitial?: string;
|
||||
rateMyProfessorURL?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -18,6 +17,9 @@ export type Instructor = {
|
||||
*/
|
||||
export type InstructionMode = 'Online' | 'In Person' | 'Hybrid';
|
||||
|
||||
/**
|
||||
* The status of a course (e.g. open, closed, waitlisted, cancelled)
|
||||
*/
|
||||
export enum Status {
|
||||
OPEN = 'OPEN',
|
||||
CLOSED = 'CLOSED',
|
||||
@@ -25,20 +27,37 @@ export enum Status {
|
||||
CANCELLED = 'CANCELLED',
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal representation of a course for the extension
|
||||
*/
|
||||
export class Course {
|
||||
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
|
||||
uniqueId: number;
|
||||
/** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */
|
||||
number: string;
|
||||
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
|
||||
fullName: string;
|
||||
/** Just the english name for a course, without the number and department */
|
||||
courseName: string;
|
||||
/** The unique identifier for which department that a course belongs to, i.e. CS, MAL, etc. */
|
||||
department: string;
|
||||
/** Is the course open, closed, waitlisted, or cancelled? */
|
||||
status: Status;
|
||||
/** all the people that are teaching this course, and some metadata about their names */
|
||||
instructors: Instructor[];
|
||||
/** Some courses at UT are reserved for certain groups of people or people within a certain major, which makes it difficult for people outside of that group to register for the course. */
|
||||
isReserved: boolean;
|
||||
/** The description of the course as an array of "lines". This will include important information as well as a short summary of the topics covered */
|
||||
description?: string[];
|
||||
/** The schedule for the course, which includes the days of the week that the course is taught, the time that the course is taught, and the location that the course is taught */
|
||||
schedule: CourseSchedule;
|
||||
/** the link to the course details page for this course */
|
||||
url: string;
|
||||
/** the link to the registration page for this course, for easy access when registering */
|
||||
registerURL?: string;
|
||||
/** At UT, some courses have certain "flags" which aid in graduation */
|
||||
flags: string[];
|
||||
/** How is the class being taught (online, hybrid, in person, etc) */
|
||||
instructionMode: InstructionMode;
|
||||
|
||||
constructor(course: Course | Serialized<Course>) {
|
||||
@@ -46,6 +65,9 @@ export class Course {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper type that is used to represent a row in the course schedule table, with the actual element corresponding to the course object
|
||||
*/
|
||||
export type CourseRow = {
|
||||
rowElement: HTMLTableRowElement;
|
||||
course: Course;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Serialized } from 'chrome-extension-toolkit';
|
||||
|
||||
const dayMap = {
|
||||
/**
|
||||
* a map of the days of the week that a class is taught, and the corresponding abbreviation
|
||||
*/
|
||||
const DAY_MAP = {
|
||||
M: 'Monday',
|
||||
T: 'Tuesday',
|
||||
W: 'Wednesday',
|
||||
@@ -10,28 +13,42 @@ const dayMap = {
|
||||
SU: 'Sunday',
|
||||
} as const;
|
||||
|
||||
type Day = typeof dayMap[keyof typeof dayMap];
|
||||
/** A day of the week that a class is taught */
|
||||
type Day = typeof DAY_MAP[keyof typeof DAY_MAP];
|
||||
|
||||
/** A physical room that a class is taught in */
|
||||
type Room = {
|
||||
/** The UT building code for where the class is taught */
|
||||
building: string;
|
||||
/** The room number for where the class is taught */
|
||||
number: string;
|
||||
};
|
||||
|
||||
export type CourseSection = {
|
||||
/**
|
||||
* This represents one "Meeting Time" for a course, which includes the day of the week that the course is taught, the time that the course is taught, and the location that the course is taught
|
||||
*/
|
||||
export type CourseMeeting = {
|
||||
/** The day of the week that the course is taught */
|
||||
day: Day;
|
||||
/** The start time of the course, in minutes since midnight */
|
||||
startTime: number;
|
||||
/** The end time of the course, in minutes since midnight */
|
||||
endTime: number;
|
||||
/** The location that the course is taught */
|
||||
room?: Room;
|
||||
};
|
||||
|
||||
/**
|
||||
* This represents the schedule for a course, which includes all the meeting times for the course, as well as helper functions for parsing, serializing, and deserializing the schedule
|
||||
*/
|
||||
export class CourseSchedule {
|
||||
sections: CourseSection[];
|
||||
meetings: CourseMeeting[];
|
||||
|
||||
constructor(courseSchedule: CourseSchedule | Serialized<CourseSchedule>) {
|
||||
Object.assign(this, courseSchedule);
|
||||
}
|
||||
|
||||
static parse(dayLine: string, timeLine: string, roomLine: string): CourseSection[] {
|
||||
static parse(dayLine: string, timeLine: string, roomLine: string): CourseMeeting[] {
|
||||
try {
|
||||
let days: Day[] = dayLine
|
||||
.split('')
|
||||
@@ -44,7 +61,7 @@ export class CourseSchedule {
|
||||
if (char === 'S' && nextChar === 'U') {
|
||||
day += nextChar;
|
||||
}
|
||||
return dayMap[day];
|
||||
return DAY_MAP[day];
|
||||
})
|
||||
.filter(Boolean) as Day[];
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import useInfiniteScroll from '../hooks/useInfiniteScroll';
|
||||
import { CourseScraper } from '../lib/courseCatalog/CourseScraper';
|
||||
import { populateSearchInputs } from '../lib/courseCatalog/populateSearchInputs';
|
||||
import { SiteSupport } from '../lib/getSiteSupport';
|
||||
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
||||
import CoursePopup from './injected/CoursePopup/CoursePopup';
|
||||
import TableHead from './injected/TableHead';
|
||||
import TableRow from './injected/TableRow';
|
||||
|
||||
@@ -35,22 +37,27 @@ export default function CourseCatalogMain({ support }: Props) {
|
||||
setRows(rows);
|
||||
}, []);
|
||||
|
||||
const handleRowButtonClick = (course: Course) => {
|
||||
const handleRowButtonClick = (course: Course) => () => {
|
||||
setSelectedCourse(course);
|
||||
};
|
||||
|
||||
const handleClearSelectedCourse = () => {
|
||||
setSelectedCourse(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ExtensionRoot>
|
||||
<TableHead>Plus</TableHead>
|
||||
{rows.map(row => (
|
||||
<TableRow
|
||||
element={row.rowElement}
|
||||
course={row.course}
|
||||
support={support}
|
||||
onClick={handleRowButtonClick}
|
||||
onClick={handleRowButtonClick(row.course)}
|
||||
/>
|
||||
))}
|
||||
{selectedCourse && <CoursePopup course={selectedCourse} onClose={handleClearSelectedCourse} />}
|
||||
{isScrolling && <div>Scrolling...</div>}
|
||||
</div>
|
||||
</ExtensionRoot>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
||||
|
||||
export default function PopupMain() {
|
||||
return <div>popup</div>;
|
||||
return <ExtensionRoot>Popup</ExtensionRoot>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@import 'src/views/styles/base.module.scss';
|
||||
|
||||
.extensionRoot {
|
||||
font-family: 'Inter' !important;
|
||||
color: #303030;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
font-family: Inter, sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
16
src/views/components/common/ExtensionRoot/ExtensionRoot.tsx
Normal file
16
src/views/components/common/ExtensionRoot/ExtensionRoot.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import styles from './ExtensionRoot.module.scss';
|
||||
|
||||
interface Props {
|
||||
testId?: string;
|
||||
}
|
||||
/**
|
||||
* A wrapper component for the extension elements that adds some basic styling to them
|
||||
*/
|
||||
export default function ExtensionRoot(props: React.PropsWithChildren<Props>) {
|
||||
return (
|
||||
<div className={styles.extensionRoot} data-testid={props.testId}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Course } from 'src/shared/types/Course';
|
||||
import { Button } from '../../common/Button/Button';
|
||||
import styles from './CoursePopup.module.scss';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function CoursePopup({ course }: Props) {
|
||||
export default function CoursePopup({ course, onClose }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<h1>{course.fullName}</h1>
|
||||
<p>{course.description}</p>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { Course, Instructor, Status, InstructionMode, CourseRow } from 'src/shared/types/Course';
|
||||
import { CourseSchedule, CourseSection } from 'src/shared/types/CourseSchedule';
|
||||
import { CourseSchedule, CourseMeeting } from 'src/shared/types/CourseSchedule';
|
||||
import { SiteSupport } from 'src/views/lib/getSiteSupport';
|
||||
|
||||
/**
|
||||
* The selectors that we use to scrape the course catalog list table (https://utdirect.utexas.edu/apps/registrar/course_schedule/20239/results/?fos_fl=C+S&level=U&search_type_main=FIELD)
|
||||
*/
|
||||
enum TableDataSelector {
|
||||
COURSE_HEADER = 'td.course_header',
|
||||
UNIQUE_ID = 'td[data-th="Unique"]',
|
||||
@@ -15,11 +18,17 @@ enum TableDataSelector {
|
||||
FLAGS = 'td[data-th="Flags"] ul li',
|
||||
}
|
||||
|
||||
/**
|
||||
* The selectors that we use to scrape the course details page for an individual course (https://utdirect.utexas.edu/apps/registrar/course_schedule/20239/52700/)
|
||||
*/
|
||||
enum DetailsSelector {
|
||||
COURSE_NAME = '#details h2',
|
||||
COURSE_DESCRIPTION = '#details p',
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that allows use to scrape information from UT's course catalog to create our internal representation of a course
|
||||
*/
|
||||
export class CourseScraper {
|
||||
support: SiteSupport;
|
||||
|
||||
@@ -27,6 +36,11 @@ export class CourseScraper {
|
||||
this.support = support;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass in a list of HTMLtable rows and scrape every course from them
|
||||
* @param rows the rows of the course catalog table
|
||||
* @returns an array of course row objects (which contain courses corresponding to the htmltable row)
|
||||
*/
|
||||
public scrape(rows: NodeListOf<HTMLTableRowElement>): CourseRow[] {
|
||||
const courses: CourseRow[] = [];
|
||||
|
||||
@@ -73,15 +87,26 @@ export class CourseScraper {
|
||||
return courses;
|
||||
}
|
||||
|
||||
separateCourseName(name: string): [courseName: string, department: string, number: string] {
|
||||
let courseNumberIndex = name.search(/\d/);
|
||||
let department = name.substring(0, courseNumberIndex).trim();
|
||||
let number = name.substring(courseNumberIndex, name.indexOf(' ', courseNumberIndex)).trim();
|
||||
let courseName = name.substring(name.indexOf(' ', courseNumberIndex)).trim();
|
||||
/**
|
||||
* Separate the course name into its department, number, and name
|
||||
* @example separateCourseName("CS 314H - Honors Discrete Structures") => ["Honors Discrete Structures", "CS", "314H"]
|
||||
* @param courseFullName the full name of the course (e.g. "CS 314H - Honors Discrete Structures")
|
||||
* @returns an array of the course name , department, and number
|
||||
*/
|
||||
separateCourseName(courseFullName: string): [courseName: string, department: string, number: string] {
|
||||
let courseNumberIndex = courseFullName.search(/\d/);
|
||||
let department = courseFullName.substring(0, courseNumberIndex).trim();
|
||||
let number = courseFullName.substring(courseNumberIndex, courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
||||
let courseName = courseFullName.substring(courseFullName.indexOf(' ', courseNumberIndex)).trim();
|
||||
|
||||
return [courseName, department, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape the Unique ID from the course catalog table row
|
||||
* @param row the row of the course catalog table
|
||||
* @returns the uniqueid of the course as a number
|
||||
*/
|
||||
getUniqueId(row: HTMLTableRowElement): number {
|
||||
const div = row.querySelector(TableDataSelector.UNIQUE_ID);
|
||||
if (!div) {
|
||||
@@ -90,11 +115,21 @@ export class CourseScraper {
|
||||
return Number(div.textContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrapes the individual URL for a given course that takes you to the course details page
|
||||
* @param row the row of the course catalog table
|
||||
* @returns the url of the course details page for the course in the row
|
||||
*/
|
||||
getURL(row: HTMLTableRowElement): string {
|
||||
const div = row.querySelector<HTMLAnchorElement>(`${TableDataSelector.UNIQUE_ID} a`);
|
||||
return div?.href || window.location.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape who is teaching the course from the course catalog table row with meta-data about their name
|
||||
* @param row the row of the course catalog table
|
||||
* @returns an array of instructors for the course
|
||||
*/
|
||||
getInstructors(row: HTMLTableRowElement): Instructor[] {
|
||||
const spans = row.querySelectorAll(TableDataSelector.INSTRUCTORS);
|
||||
const names = Array.from(spans)
|
||||
@@ -115,10 +150,20 @@ export class CourseScraper {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not this is a header row for a course within the course catalog list (we can't scrape courses from header rows)
|
||||
* @param row the row of the course catalog table
|
||||
* @returns true if this is a header row, false otherwise
|
||||
*/
|
||||
isHeaderRow(row: HTMLTableRowElement): boolean {
|
||||
return row.querySelector(TableDataSelector.COURSE_HEADER) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrape whether the class is being taught online, in person, or a hybrid of the two
|
||||
* @param row the row of the course catalog table
|
||||
* @returns the instruction mode of the course
|
||||
*/
|
||||
getInstructionMode(row: HTMLTableRowElement): InstructionMode {
|
||||
const text = (row.querySelector(TableDataSelector.INSTRUCTION_MODE)?.textContent || '').toLowerCase();
|
||||
|
||||
@@ -131,6 +176,11 @@ export class CourseScraper {
|
||||
return 'In Person';
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrapes the description of the course from the course details page and separates it into an array of cleaned up lines
|
||||
* @param document the document of the course details page to scrape
|
||||
* @returns an array of lines of the course description
|
||||
*/
|
||||
getDescription(document: Document): string[] {
|
||||
const lines = document.querySelectorAll(DetailsSelector.COURSE_DESCRIPTION);
|
||||
return Array.from(lines)
|
||||
@@ -139,6 +189,11 @@ export class CourseScraper {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full name of the course from the course catalog table row (e.g. "CS 314H - Honors Discrete Structures")
|
||||
* @param row the row of the course catalog table
|
||||
* @returns the full name of the course
|
||||
*/
|
||||
getFullName(row?: HTMLTableRowElement): string {
|
||||
if (!row) {
|
||||
return document.querySelector(DetailsSelector.COURSE_NAME)?.textContent || '';
|
||||
@@ -147,11 +202,21 @@ export class CourseScraper {
|
||||
return div?.textContent || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* When registration is open, the registration URL will show up in the course catalog table row as a link. This will scrape it from the row.
|
||||
* @param row the row of the course catalog table
|
||||
* @returns the registration URL for the course if it is currently displayed, undefined otherwise
|
||||
*/
|
||||
getRegisterURL(row: HTMLTableRowElement): string | undefined {
|
||||
const a = row.querySelector<HTMLAnchorElement>(TableDataSelector.REGISTER_URL);
|
||||
return a?.href;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrapes whether the course is open, closed, waitlisted, or cancelled
|
||||
* @param row the row of the course catalog table
|
||||
* @returns
|
||||
*/
|
||||
getStatus(row: HTMLTableRowElement): [status: Status, isReserved: boolean] {
|
||||
const div = row.querySelector(TableDataSelector.STATUS);
|
||||
if (!div) {
|
||||
@@ -178,11 +243,21 @@ export class CourseScraper {
|
||||
throw new Error(`Unknown status: ${text}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* At UT, some courses have certain "flags" which aid in graduation. This will scrape the flags from the course catalog table row.
|
||||
* @param row
|
||||
* @returns an array of flags for the course
|
||||
*/
|
||||
getFlags(row: HTMLTableRowElement): string[] {
|
||||
const lis = row.querySelectorAll(TableDataSelector.FLAGS);
|
||||
return Array.from(lis).map(li => li.textContent || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* This will scrape all the time information from the course catalog table row and return it as a CourseSchedule object, which represents all of the meeting timiestimes/places of the course.
|
||||
* @param row the row of the course catalog table
|
||||
* @returns a CourseSchedule object representing all of the meetings of the course
|
||||
*/
|
||||
getSchedule(row: HTMLTableRowElement): CourseSchedule {
|
||||
const dayLines = row.querySelectorAll(TableDataSelector.SCHEDULE_DAYS);
|
||||
const hourLines = row.querySelectorAll(TableDataSelector.SCHEDULE_HOURS);
|
||||
@@ -192,19 +267,17 @@ export class CourseScraper {
|
||||
throw new Error('Schedule data is malformed');
|
||||
}
|
||||
|
||||
const sections: CourseSection[] = [];
|
||||
const meetings: CourseMeeting[] = [];
|
||||
|
||||
for (let i = 0; i < dayLines.length; i += 1) {
|
||||
const lineSections = CourseSchedule.parse(
|
||||
const lineMeetings = CourseSchedule.parse(
|
||||
dayLines[i].textContent || '',
|
||||
hourLines[i].textContent || '',
|
||||
roomLines[i].textContent || ''
|
||||
);
|
||||
sections.push(...lineSections);
|
||||
meetings.push(...lineMeetings);
|
||||
}
|
||||
|
||||
return new CourseSchedule({
|
||||
sections,
|
||||
});
|
||||
return new CourseSchedule({ meetings });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user