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

27
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@types/sql.js": "^1.4.4", "@types/sql.js": "^1.4.4",
"chrome-extension-toolkit": "^0.0.37", "chrome-extension-toolkit": "^0.0.48",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"highcharts": "^10.3.3", "highcharts": "^10.3.3",
@@ -29,6 +29,7 @@
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@types/semver": "^7.3.13", "@types/semver": "^7.3.13",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.47.0", "@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0", "@typescript-eslint/parser": "^5.47.0",
"archiver": "^5.3.1", "archiver": "^5.3.1",
@@ -3294,6 +3295,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -4808,9 +4815,9 @@
} }
}, },
"node_modules/chrome-extension-toolkit": { "node_modules/chrome-extension-toolkit": {
"version": "0.0.37", "version": "0.0.48",
"resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.37.tgz", "resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.48.tgz",
"integrity": "sha512-j8umRVPr6uKx77a191zIUCaQlq4KE2J1+ShXxsW1TEJIPKBxGcOfwH3N3OhDGnoCF/3shAgs6nZSg2uIvZJsfg==", "integrity": "sha512-maShnkzOxMOWcsKaG2dFzpRwabDGBoUrkjHGwEEjYbgnPPmqTYKQAywibUAdGC4gpJL1iJyOJN+sUuha3DsloQ==",
"dependencies": { "dependencies": {
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
@@ -20080,6 +20087,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==",
"dev": true
},
"@types/ws": { "@types/ws": {
"version": "8.5.3", "version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -21236,9 +21249,9 @@
} }
}, },
"chrome-extension-toolkit": { "chrome-extension-toolkit": {
"version": "0.0.37", "version": "0.0.48",
"resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.37.tgz", "resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.48.tgz",
"integrity": "sha512-j8umRVPr6uKx77a191zIUCaQlq4KE2J1+ShXxsW1TEJIPKBxGcOfwH3N3OhDGnoCF/3shAgs6nZSg2uIvZJsfg==", "integrity": "sha512-maShnkzOxMOWcsKaG2dFzpRwabDGBoUrkjHGwEEjYbgnPPmqTYKQAywibUAdGC4gpJL1iJyOJN+sUuha3DsloQ==",
"requires": { "requires": {
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"

View File

@@ -14,7 +14,7 @@
}, },
"dependencies": { "dependencies": {
"@types/sql.js": "^1.4.4", "@types/sql.js": "^1.4.4",
"chrome-extension-toolkit": "^0.0.37", "chrome-extension-toolkit": "^0.0.48",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"clean-webpack-plugin": "^4.0.0", "clean-webpack-plugin": "^4.0.0",
"highcharts": "^10.3.3", "highcharts": "^10.3.3",
@@ -34,6 +34,7 @@
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@types/semver": "^7.3.13", "@types/semver": "^7.3.13",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.47.0", "@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0", "@typescript-eslint/parser": "^5.47.0",
"archiver": "^5.3.1", "archiver": "^5.3.1",

View File

@@ -5,7 +5,7 @@ import onInstall from './events/onInstall';
import onNewChromeSession from './events/onNewChromeSession'; import onNewChromeSession from './events/onNewChromeSession';
import onServiceWorkerAlive from './events/onServiceWorkerAlive'; import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate'; import onUpdate from './events/onUpdate';
import { SessionStore } from '../shared/storage/SessionStore'; import { sessionStore } from '../shared/storage/sessionStore';
import browserActionHandler from './handler/browserActionHandler'; import browserActionHandler from './handler/browserActionHandler';
import hotReloadingHandler from './handler/hotReloadingHandler'; import hotReloadingHandler from './handler/hotReloadingHandler';
import tabManagementHandler from './handler/tabManagementHandler'; import tabManagementHandler from './handler/tabManagementHandler';
@@ -38,9 +38,9 @@ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
messageListener.listen(); messageListener.listen();
SessionStore.getChromeSessionId().then(async chromeSessionId => { sessionStore.get('chromeSessionId').then(async chromeSessionId => {
if (!chromeSessionId) { if (!chromeSessionId) {
await SessionStore.setChromeSessionId(generateRandomId(10)); await sessionStore.set('chromeSessionId', generateRandomId(10));
onNewChromeSession(); onNewChromeSession();
} }
}); });

View File

@@ -1,9 +1,8 @@
import { SECOND } from 'src/shared/util/time'; import { extensionStore } from '../../shared/storage/extensionStore';
import { ExtensionStore } from '../../shared/storage/ExtensionStore';
/** /**
* Called when the extension is first installed or synced onto a new machine * Called when the extension is first installed or synced onto a new machine
*/ */
export default async function onInstall() { export default async function onInstall() {
await ExtensionStore.setVersion(chrome.runtime.getManifest().version); await extensionStore.set('version', chrome.runtime.getManifest().version);
} }

View File

@@ -1,14 +1,14 @@
import { hotReloadTab } from 'src/background/util/hotReloadTab'; import { hotReloadTab } from 'src/background/util/hotReloadTab';
import { ExtensionStore } from '../../shared/storage/ExtensionStore'; import { extensionStore } from '../../shared/storage/extensionStore';
/** /**
* Called when the extension is updated (or when the extension is reloaded in development mode) * Called when the extension is updated (or when the extension is reloaded in development mode)
*/ */
export default async function onUpdate() { export default async function onUpdate() {
await Promise.all([ await extensionStore.set({
ExtensionStore.setLastUpdate(Date.now()), version: chrome.runtime.getManifest().version,
ExtensionStore.setVersion(chrome.runtime.getManifest().version), lastUpdate: Date.now(),
]); });
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
hotReloadTab(); hotReloadTab();

View File

@@ -1,18 +1,17 @@
import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages'; import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages';
import { MessageHandler } from 'chrome-extension-toolkit'; import { MessageHandler } from 'chrome-extension-toolkit';
import { DevStore } from 'src/shared/storage/DevStore'; import { devStore } from 'src/shared/storage/devStore';
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = { const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
async reloadExtension({ sendResponse }) { async reloadExtension({ sendResponse }) {
const isExtensionReloading = await DevStore.getIsExtensionReloading(); const { isExtensionReloading, isTabReloading } = await devStore.get(['isExtensionReloading', 'isTabReloading']);
if (!isExtensionReloading) return sendResponse(); if (!isExtensionReloading) return sendResponse();
const isTabReloading = await DevStore.getIsExtensionReloading();
if (isTabReloading) { if (isTabReloading) {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tabToReload = tabs[0]; const tabToReload = tabs[0];
await DevStore.setReloadTabId(tabToReload?.id); await devStore.set('reloadTabId', tabToReload?.id);
} }
chrome.runtime.reload(); chrome.runtime.reload();
}, },

View File

@@ -1,4 +1,4 @@
import { DevStore } from 'src/shared/storage/DevStore'; import { devStore } from 'src/shared/storage/devStore';
/** /**
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao) * A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
@@ -24,9 +24,7 @@ const HOT_RELOADING_WHITELIST = [
* @returns a promise that resolves when the tab is reloaded * @returns a promise that resolves when the tab is reloaded
*/ */
export async function hotReloadTab(): Promise<void> { export async function hotReloadTab(): Promise<void> {
const { getIsTabReloading, getReloadTabId } = DevStore; const { isTabReloading, reloadTabId } = await devStore.get(['isTabReloading', 'reloadTabId']);
const [isTabReloading, reloadTabId] = await Promise.all([getIsTabReloading(), getReloadTabId()]);
if (!isTabReloading || !reloadTabId) return; if (!isTabReloading || !reloadTabId) return;

View File

@@ -1,24 +1,22 @@
import { DevStore } from 'src/shared/storage/DevStore'; import { devStore } from 'src/shared/storage/devStore';
/** /**
* Open the debug tab as the first tab * Open the debug tab as the first tab
*/ */
export async function openDebugTab() { export async function openDebugTab() {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
const debugTabId = await DevStore.getDebugTabId(); const { debugTabId, wasDebugTabVisible } = await devStore.get(['debugTabId', 'wasDebugTabVisible']);
const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId); const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
if (isAlreadyOpen) return; if (isAlreadyOpen) return;
const wasVisible = await DevStore.getWasDebugTabVisible();
const tab = await chrome.tabs.create({ const tab = await chrome.tabs.create({
url: chrome.runtime.getURL('debug.html'), url: chrome.runtime.getURL('debug.html'),
active: wasVisible, active: wasDebugTabVisible,
pinned: true, pinned: true,
index: 0, index: 0,
}); });
await DevStore.setDebugTabId(tab.id); await devStore.set('debugTabId', tab.id);
} }
} }

View File

@@ -1,7 +1,7 @@
import './hotReload'; import './hotReload';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { DevStore } from 'src/shared/storage/DevStore'; import { devStore } from 'src/shared/storage/devStore';
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
@@ -69,11 +69,7 @@ function DevDashboard() {
useEffect(() => { useEffect(() => {
const onVisibilityChange = () => { const onVisibilityChange = () => {
if (document.visibilityState === 'visible') { devStore.set('wasDebugTabVisible', document.visibilityState === 'visible');
DevStore.setWasDebugTabVisible(true);
} else {
DevStore.setWasDebugTabVisible(false);
}
}; };
document.addEventListener('visibilitychange', onVisibilityChange); document.addEventListener('visibilitychange', onVisibilityChange);
return () => { return () => {

View File

@@ -16,7 +16,7 @@ interface IDevStore {
reloadTabId?: number; reloadTabId?: number;
} }
export const DevStore = createLocalStore<IDevStore>({ export const devStore = createLocalStore<IDevStore>({
debugTabId: undefined, debugTabId: undefined,
isTabReloading: true, isTabReloading: true,
wasDebugTabVisible: false, wasDebugTabVisible: false,
@@ -24,5 +24,4 @@ export const DevStore = createLocalStore<IDevStore>({
reloadTabId: undefined, reloadTabId: undefined,
}); });
debugStore({ devStore });
debugStore({ DevStore });

View File

@@ -1,4 +1,3 @@
import { v4 as uuidv4 } from 'uuid';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
/** /**
@@ -9,31 +8,11 @@ interface IExtensionStore {
version: string; version: string;
/** When was the last update */ /** When was the last update */
lastUpdate: number; lastUpdate: number;
/** A unique identifier generated for the current user in lieu of a userId */
deviceId: string;
} }
interface Actions { export const extensionStore = createLocalStore<IExtensionStore>({
getDeviceId(): Promise<string>; version: chrome.runtime.getManifest().version,
} lastUpdate: Date.now(),
});
export const ExtensionStore = createLocalStore<IExtensionStore, Actions>( debugStore({ extensionStore });
{
version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(),
deviceId: '',
},
store => ({
getDeviceId: async () => {
const deviceId = await store.getDeviceId();
if (deviceId) {
return deviceId;
}
const newDeviceId = uuidv4();
await store.setDeviceId(newDeviceId);
return newDeviceId;
},
})
);
debugStore({ ExtensionStore });

View File

@@ -10,9 +10,9 @@ interface IOptionsStore {
shouldScrollToLoad: boolean; shouldScrollToLoad: boolean;
} }
export const OptionsStore = createSyncStore<IOptionsStore>({ export const optionsStore = createSyncStore<IOptionsStore>({
shouldHighlightConflicts: true, shouldHighlightConflicts: true,
shouldScrollToLoad: true, shouldScrollToLoad: true,
}); });
debugStore({ OptionsStore }); debugStore({ optionsStore });

View File

@@ -4,8 +4,8 @@ interface ISessionStore {
chromeSessionId?: string; chromeSessionId?: string;
} }
export const SessionStore = createSessionStore<ISessionStore>({ export const sessionStore = createSessionStore<ISessionStore>({
chromeSessionId: undefined, chromeSessionId: undefined,
}); });
debugStore({ SessionStore }); debugStore({ sessionStore });

View File

@@ -1,62 +1,15 @@
import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
import { Course } from 'src/shared/types/Course'; import { UserSchedule } from 'src/shared/types/UserSchedule';
/**
* A store that is used for storing user options
*/
interface IUserScheduleStore { interface IUserScheduleStore {
current: string; schedules: UserSchedule[];
schedules: {
[id: string]: Course[];
};
} }
interface Actions { /**
createSchedule(name: string): Promise<void>; * A store that is used for storing user schedules (and the active schedule)
addCourseToSchedule(name: string, course: Course): Promise<void>; */
removeCourseFromSchedule(name: string, course: Course): Promise<void>; export const userScheduleStore = createLocalStore<IUserScheduleStore>({
removeSchedule(name: string): Promise<void>; schedules: [],
getSchedule(name: string): Promise<Course[] | undefined>; });
}
const UserScheduleStore = createLocalStore<IUserScheduleStore, Actions>( debugStore({ userScheduleStore });
{
current: 'Schedule 1',
schedules: {},
},
store => ({
async createSchedule(name: string) {
const schedules = await store.getSchedules();
if (!schedules[name]) {
schedules[name] = [];
await store.setSchedules(schedules as any);
}
},
async removeSchedule(name: string) {
const schedules = await store.getSchedules();
delete schedules[name];
await store.setSchedules(schedules);
},
async getSchedule(name) {
const schedules = await store.getSchedules();
return schedules[name]?.map(course => new Course(course));
},
async addCourseToSchedule(name, course) {
const schedules = await store.getSchedules();
const scheduleToEdit = schedules[name];
if (scheduleToEdit) {
scheduleToEdit.push(course);
await store.setSchedules(schedules);
}
},
async removeCourseFromSchedule(name, course) {
const schedules = await store.getSchedules();
const scheduleToEdit = schedules[name];
if (scheduleToEdit) {
schedules[name] = scheduleToEdit.filter(c => c.uniqueId !== course.uniqueId);
await store.setSchedules(schedules);
}
},
})
);
debugStore({ UserScheduleStore });

View File

@@ -45,6 +45,9 @@ export class Course {
courseName: string; courseName: string;
/** The unique identifier for which department that a course belongs to, i.e. CS, MAL, etc. */ /** The unique identifier for which department that a course belongs to, i.e. CS, MAL, etc. */
department: string; department: string;
/** The number of credits that a course is worth */
creditHours: number;
/** Is the course open, closed, waitlisted, or cancelled? */ /** Is the course open, closed, waitlisted, or cancelled? */
status: Status; status: Status;
/** all the people that are teaching this course, and some metadata about their names */ /** all the people that are teaching this course, and some metadata about their names */

View File

@@ -0,0 +1,35 @@
import { Serialized } from 'chrome-extension-toolkit';
import { Course } from './Course';
/**
* Represents a user's schedule that is stored in the extension
*/
export class UserSchedule {
courses: Course[];
id: string;
name: string;
constructor(schedule: Serialized<UserSchedule>) {
this.courses = schedule.courses.map(c => new Course(c));
this.id = schedule.id;
this.name = schedule.name;
}
containsCourse(course: Course): boolean {
return this.courses.some(c => c.uniqueId === course.uniqueId);
}
getCreditHours(): number {
return this.courses.reduce((acc, course) => acc + course.creditHours, 0);
}
addCourse(course: Course): void {
if (!this.containsCourse(course)) {
this.courses.push(course);
}
}
removeCourse(course: Course): void {
this.courses = this.courses.filter(c => c.uniqueId !== course.uniqueId);
}
}

View File

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

View File

@@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { bMessenger } from 'src/shared/messages'; import { bMessenger } from 'src/shared/messages';
import { userScheduleStore } from 'src/shared/storage/userScheduleStore';
import { Course } from 'src/shared/types/Course'; import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import { Button } from 'src/views/components/common/Button/Button'; import { Button } from 'src/views/components/common/Button/Button';
import Card from 'src/views/components/common/Card/Card'; import Card from 'src/views/components/common/Card/Card';
import Icon from 'src/views/components/common/Icon/Icon'; 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'; import styles from './CourseButtons.module.scss';
type Props = { type Props = {
activeSchedule?: UserSchedule;
course: Course; 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 * 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. * 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 openRateMyProfessorURL = () => {
const primaryInstructor = course.instructors?.[0]; const primaryInstructor = course.instructors?.[0];
if (!primaryInstructor) return; if (!primaryInstructor) return;
@@ -61,6 +64,17 @@ export default function CourseButtons({ course }: Props) {
openNewTab({ url: url.toString() }); 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 ( return (
<Card className={styles.container}> <Card className={styles.container}>
<Button <Button
@@ -86,7 +100,7 @@ export default function CourseButtons({ course }: Props) {
</Text> </Text>
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' /> <Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
</Button> </Button>
<Button type='success' className={styles.button}> <Button disabled={!activeSchedule} onClick={saveCourse} type='success' className={styles.button}>
<Text size='medium' weight='regular' color='white'> <Text size='medium' weight='regular' color='white'>
Save Save
</Text> </Text>

View File

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

View File

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

View File

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

View File

@@ -16,23 +16,8 @@ const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD'];
export default function RecruitmentBanner() { export default function RecruitmentBanner() {
const [container, setContainer] = useState<HTMLDivElement | null>(null); 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(() => { useEffect(() => {
if (!shouldShowBanner()) { if (!canRecruitFrom()) {
return; return;
} }
const container = document.createElement('div'); const container = document.createElement('div');
@@ -64,3 +49,18 @@ export default function RecruitmentBanner() {
container 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; box-shadow: none !important;
} }
} }
.inActiveSchedule {
* {
color: $turtle_pond !important;
font-weight: bold !important;
}
}

View File

@@ -9,13 +9,17 @@ interface Props {
isSelected: boolean; isSelected: boolean;
row: ScrapedRow; row: ScrapedRow;
onClick: (...args: any[]) => any; 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. * 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. * @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 [container, setContainer] = useState<HTMLTableCellElement | null>(null);
const { element, course } = row; 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); element.classList[isSelected ? 'add' : 'remove'](styles.selectedRow);
}, [isSelected, element.classList]); }, [isSelected, element.classList]);
useEffect(() => {
element.classList[isInActiveSchedule ? 'add' : 'remove'](styles.inActiveSchedule);
}, [isInActiveSchedule, element.classList]);
if (!container) { if (!container) {
return null; 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...'); 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')); render(<PopupMain />, document.getElementById('root'));
} }

View File

@@ -78,6 +78,7 @@ export class CourseCatalogScraper {
number, number,
status, status,
isReserved, isReserved,
creditHours: this.getCreditHours(number),
schedule: this.getSchedule(row), schedule: this.getSchedule(row),
registerURL: this.getRegisterURL(row), registerURL: this.getRegisterURL(row),
url: this.getURL(row), url: this.getURL(row),
@@ -112,6 +113,15 @@ export class CourseCatalogScraper {
return [courseName, department, number]; 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 * Scrape the Unique ID from the course catalog table row
* @param row the row of the course catalog table * @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, 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(' '); 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]; 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 * An enum that represents the different types of pages that we support
* a given url/page can correspond to many of these enum values * 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', COURSE_CATALOG_DETAILS = 'COURSE_CATALOG_DETAILS',
UT_PLANNER = 'UT_PLANNER', UT_PLANNER = 'UT_PLANNER',
WAITLIST = 'WAITLIST', WAITLIST = 'WAITLIST',
EXTENSION_POPUP = 'EXTENSION_POPUP',
} }
/** /**
@@ -15,6 +18,9 @@ export enum SiteSupport {
* @returns a list of page types that the current page is * @returns a list of page types that the current page is
*/ */
export default function getSiteSupport(url: string): SiteSupport | null { export default function getSiteSupport(url: string): SiteSupport | null {
if (isExtensionPopup()) {
return SiteSupport.EXTENSION_POPUP;
}
if (url.includes('utexas.collegescheduler.com')) { if (url.includes('utexas.collegescheduler.com')) {
return SiteSupport.UT_PLANNER; return SiteSupport.UT_PLANNER;
} }

View File

@@ -87,6 +87,7 @@ export function getBuildPlugins(mode: Environment, htmlEntries: EntryId[], manif
title: `${manifest.short_name} v${manifest.version} ${mode}`, title: `${manifest.short_name} v${manifest.version} ${mode}`,
logo: path.resolve('public', 'icons', 'icon_production_128.png'), logo: path.resolve('public', 'icons', 'icon_production_128.png'),
failureSound: 'Ping', failureSound: 'Ping',
successSound: false,
showDuration: true, showDuration: true,
suppressWarning: true, suppressWarning: true,
}) })