added onclose to popup, coursepopup now displaying time info, renamed vars, added compiler for scss to typescript and tsconfig plugins
This commit is contained in:
@@ -17,11 +17,11 @@ export const DAY_MAP = {
|
||||
export type Day = typeof DAY_MAP[keyof typeof DAY_MAP];
|
||||
|
||||
/** A physical room that a class is taught in */
|
||||
export type Room = {
|
||||
export type Location = {
|
||||
/** The UT building code for where the class is taught */
|
||||
building: string;
|
||||
/** The room number for where the class is taught */
|
||||
number: string;
|
||||
room: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export class CourseMeeting {
|
||||
/** The end time of the course, in minutes since midnight */
|
||||
endTime: number;
|
||||
/** The location that the course is taught */
|
||||
room?: Room;
|
||||
location?: Location;
|
||||
|
||||
constructor(meeting: Serialized<CourseMeeting>) {
|
||||
Object.assign(this, meeting);
|
||||
@@ -124,5 +124,16 @@ type TimeStringOptions = {
|
||||
type DaysStringOptions = {
|
||||
/** The format of the days string, short = MWF, long = Monday, Wednesday, Friday */
|
||||
format: 'short' | 'long';
|
||||
/**
|
||||
* The separator between the days
|
||||
*
|
||||
* 'none' = `MWF`
|
||||
*
|
||||
* 'conjunction' = `Monday, Wednesday, and Friday`
|
||||
*
|
||||
* 'disjunction' = `Monday, Wednesday, or Friday`
|
||||
*
|
||||
* 'narrow' = `Monday Wednesday Friday`
|
||||
*/
|
||||
separator: Intl.ListFormatStyle | 'none';
|
||||
};
|
||||
|
||||
@@ -18,10 +18,10 @@ export class CourseSchedule {
|
||||
* Given a string representation of the meeting information for a class, parse it into a CourseMeeting object
|
||||
* @param dayLine a string representation of the days of the week that the course is taught: MWF, TR, etc.
|
||||
* @param timeLine a string representation of a time-range that the course is taught: 10:00 am - 11:00 am, 1:00 pm - 2:00 pm, etc.
|
||||
* @param roomLine a string representation of the room that the course is taught in: JGB 2.302, etc.
|
||||
* @param locLine a string representation of the location that the course is taught in: JGB 2.302, etc.
|
||||
* @returns CourseMeeting object representing the meeting information
|
||||
*/
|
||||
static parse(dayLine: string, timeLine: string, roomLine: string): CourseMeeting {
|
||||
static parse(dayLine: string, timeLine: string, locLine: string): CourseMeeting {
|
||||
try {
|
||||
let days: Day[] = dayLine
|
||||
.split('')
|
||||
@@ -51,19 +51,19 @@ export class CourseSchedule {
|
||||
return Number(hour) * 60 + Number(minute);
|
||||
});
|
||||
|
||||
const [building, number] = roomLine.split(' ');
|
||||
const [building, room] = locLine.split(' ');
|
||||
|
||||
return new CourseMeeting({
|
||||
days,
|
||||
startTime,
|
||||
endTime,
|
||||
room: {
|
||||
location: {
|
||||
building,
|
||||
number,
|
||||
room,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse schedule: ${dayLine} ${timeLine} ${roomLine}`);
|
||||
throw new Error(`Failed to parse schedule: ${dayLine} ${timeLine} ${locLine}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import React, { PropsWithChildren, useCallback } from 'react';
|
||||
import styles from './Popup.module.scss';
|
||||
|
||||
interface Props {
|
||||
@@ -8,21 +8,52 @@ interface Props {
|
||||
className?: string;
|
||||
/** Should it display a subtle dark overlay over the rest of the screen */
|
||||
overlay?: boolean;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* A reusable popup component that can be used to display content on the page
|
||||
* @returns
|
||||
*/
|
||||
export default function Popup(props: PropsWithChildren<Props>) {
|
||||
export default function Popup({ onClose, children, className, style, testId, overlay }: PropsWithChildren<Props>) {
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const bodyRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!bodyRef.current) return;
|
||||
if (!bodyRef.current.contains(event.target as Node)) {
|
||||
onClose?.();
|
||||
}
|
||||
},
|
||||
[onClose, bodyRef]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const shadowRoot = document.getElementById('ut-registration-plus-container')?.shadowRoot;
|
||||
if (!shadowRoot) return;
|
||||
|
||||
shadowRoot.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
shadowRoot.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleClickOutside]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={props.style}
|
||||
style={style}
|
||||
ref={containerRef}
|
||||
className={classNames(styles.container, {
|
||||
[styles.overlay]: props.overlay,
|
||||
[styles.overlay]: overlay,
|
||||
})}
|
||||
data-testid={props.testId}
|
||||
data-testid={testId}
|
||||
>
|
||||
<div className={classNames(styles.body, props.className)}>{props.children}</div>
|
||||
<div ref={bodyRef} className={classNames(styles.body, className)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ interface Props {
|
||||
export default function CoursePopup({ course, onClose }: Props) {
|
||||
console.log(course);
|
||||
return (
|
||||
<Popup className={styles.popup} overlay>
|
||||
<Popup className={styles.popup} overlay onClose={onClose}>
|
||||
<Icon className={styles.close} size='large' name='close' onClick={onClose} />
|
||||
<Card className={styles.body}>
|
||||
<Text className={styles.title} size='large' weight='bold' color='black'>
|
||||
@@ -40,6 +40,28 @@ export default function CoursePopup({ course, onClose }: Props) {
|
||||
format: 'first_last',
|
||||
})}
|
||||
</Text>
|
||||
|
||||
{course.schedule.meetings.map(meeting => (
|
||||
<Text size='medium'>
|
||||
<Text span size='medium' weight='bold' color='black'>
|
||||
{meeting.getDaysString({
|
||||
format: 'long',
|
||||
separator: 'short',
|
||||
})}
|
||||
</Text>
|
||||
{' at '}
|
||||
<Text span size='medium'>
|
||||
{meeting.getTimeString({
|
||||
separator: 'to',
|
||||
capitalize: true,
|
||||
})}
|
||||
</Text>
|
||||
{' in '}
|
||||
<Link span size='medium' weight='bold' color='bluebonnet'>
|
||||
{meeting.location?.building}
|
||||
</Link>
|
||||
</Text>
|
||||
))}
|
||||
</Card>
|
||||
</Popup>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ enum TableDataSelector {
|
||||
STATUS = 'td[data-th="Status"]',
|
||||
SCHEDULE_DAYS = 'td[data-th="Days"]>span',
|
||||
SCHEDULE_HOURS = 'td[data-th="Hour"]>span',
|
||||
SCHEDULE_ROOM = 'td[data-th="Room"]>span',
|
||||
SCHEDULE_LOCATION = 'td[data-th="Room"]>span',
|
||||
FLAGS = 'td[data-th="Flags"] ul li',
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ export class CourseCatalogScraper {
|
||||
getSchedule(row: HTMLTableRowElement): CourseSchedule {
|
||||
const dayLines = row.querySelectorAll(TableDataSelector.SCHEDULE_DAYS);
|
||||
const hourLines = row.querySelectorAll(TableDataSelector.SCHEDULE_HOURS);
|
||||
const roomLines = row.querySelectorAll(TableDataSelector.SCHEDULE_ROOM);
|
||||
const locLines = row.querySelectorAll(TableDataSelector.SCHEDULE_LOCATION);
|
||||
|
||||
if (dayLines.length !== hourLines.length) {
|
||||
throw new Error('Schedule data is malformed');
|
||||
@@ -296,7 +296,7 @@ export class CourseCatalogScraper {
|
||||
CourseSchedule.parse(
|
||||
dayLines[i].textContent || '',
|
||||
hourLines[i].textContent || '',
|
||||
roomLines[i].textContent || ''
|
||||
locLines[i].textContent || ''
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user