From 6afd372945dae2a3d65002a9c5e8987e7566ef04 Mon Sep 17 00:00:00 2001 From: Sriram Hariharan Date: Wed, 15 Mar 2023 23:54:07 -0500 Subject: [PATCH] multiple schedule suppport kinda --- package-lock.json | 27 ++++++-- package.json | 3 +- src/background/background.ts | 6 +- src/background/events/onInstall.ts | 5 +- src/background/events/onUpdate.ts | 10 +-- src/background/handler/hotReloadingHandler.ts | 7 +- src/background/util/hotReloadTab.ts | 6 +- src/background/util/openDebugTab.ts | 10 ++- src/debug/index.tsx | 8 +-- src/shared/storage/DevStore.ts | 5 +- src/shared/storage/ExtensionStore.ts | 31 ++------- src/shared/storage/OptionsStore.ts | 4 +- src/shared/storage/SessionStore.ts | 4 +- src/shared/storage/UserScheduleStore.ts | 67 +++---------------- src/shared/types/Course.ts | 3 + src/shared/types/UserSchedule.ts | 35 ++++++++++ src/views/components/CourseCatalogMain.tsx | 13 +++- .../CourseButtons/CourseButtons.tsx | 18 ++++- .../CoursePopup/CourseHeader/CourseHeader.tsx | 4 +- .../injected/CoursePopup/CoursePopup.tsx | 6 +- .../GradeDistribution/GradeDistribution.tsx | 1 + .../RecruitmentBanner/RecruitmentBanner.tsx | 32 ++++----- .../injected/TableRow/TableRow.module.scss | 7 ++ .../components/injected/TableRow/TableRow.tsx | 10 ++- src/views/hooks/useUserSchedules.ts | 26 +++++++ src/views/index.tsx | 2 +- src/views/lib/CourseCatalogScraper.ts | 10 +++ src/views/lib/database/queryDistribution.ts | 12 +++- src/views/lib/getSiteSupport.ts | 6 ++ webpack/plugins/buildProcessPlugins.ts | 1 + 30 files changed, 224 insertions(+), 155 deletions(-) create mode 100644 src/shared/types/UserSchedule.ts create mode 100644 src/views/hooks/useUserSchedules.ts diff --git a/package-lock.json b/package-lock.json index ba4314c2..eb614f0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "dependencies": { "@types/sql.js": "^1.4.4", - "chrome-extension-toolkit": "^0.0.37", + "chrome-extension-toolkit": "^0.0.48", "classnames": "^2.3.2", "clean-webpack-plugin": "^4.0.0", "highcharts": "^10.3.3", @@ -29,6 +29,7 @@ "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/semver": "^7.3.13", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.47.0", "@typescript-eslint/parser": "^5.47.0", "archiver": "^5.3.1", @@ -3294,6 +3295,12 @@ "@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": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -4808,9 +4815,9 @@ } }, "node_modules/chrome-extension-toolkit": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.37.tgz", - "integrity": "sha512-j8umRVPr6uKx77a191zIUCaQlq4KE2J1+ShXxsW1TEJIPKBxGcOfwH3N3OhDGnoCF/3shAgs6nZSg2uIvZJsfg==", + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.48.tgz", + "integrity": "sha512-maShnkzOxMOWcsKaG2dFzpRwabDGBoUrkjHGwEEjYbgnPPmqTYKQAywibUAdGC4gpJL1iJyOJN+sUuha3DsloQ==", "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" @@ -20080,6 +20087,12 @@ "@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": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", @@ -21236,9 +21249,9 @@ } }, "chrome-extension-toolkit": { - "version": "0.0.37", - "resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.37.tgz", - "integrity": "sha512-j8umRVPr6uKx77a191zIUCaQlq4KE2J1+ShXxsW1TEJIPKBxGcOfwH3N3OhDGnoCF/3shAgs6nZSg2uIvZJsfg==", + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/chrome-extension-toolkit/-/chrome-extension-toolkit-0.0.48.tgz", + "integrity": "sha512-maShnkzOxMOWcsKaG2dFzpRwabDGBoUrkjHGwEEjYbgnPPmqTYKQAywibUAdGC4gpJL1iJyOJN+sUuha3DsloQ==", "requires": { "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/package.json b/package.json index a685a226..09b77943 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@types/sql.js": "^1.4.4", - "chrome-extension-toolkit": "^0.0.37", + "chrome-extension-toolkit": "^0.0.48", "classnames": "^2.3.2", "clean-webpack-plugin": "^4.0.0", "highcharts": "^10.3.3", @@ -34,6 +34,7 @@ "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@types/semver": "^7.3.13", + "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^5.47.0", "@typescript-eslint/parser": "^5.47.0", "archiver": "^5.3.1", diff --git a/src/background/background.ts b/src/background/background.ts index c0c13c6b..e60d8078 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -5,7 +5,7 @@ import onInstall from './events/onInstall'; import onNewChromeSession from './events/onNewChromeSession'; import onServiceWorkerAlive from './events/onServiceWorkerAlive'; import onUpdate from './events/onUpdate'; -import { SessionStore } from '../shared/storage/SessionStore'; +import { sessionStore } from '../shared/storage/sessionStore'; import browserActionHandler from './handler/browserActionHandler'; import hotReloadingHandler from './handler/hotReloadingHandler'; import tabManagementHandler from './handler/tabManagementHandler'; @@ -38,9 +38,9 @@ const messageListener = new MessageListener({ messageListener.listen(); -SessionStore.getChromeSessionId().then(async chromeSessionId => { +sessionStore.get('chromeSessionId').then(async chromeSessionId => { if (!chromeSessionId) { - await SessionStore.setChromeSessionId(generateRandomId(10)); + await sessionStore.set('chromeSessionId', generateRandomId(10)); onNewChromeSession(); } }); diff --git a/src/background/events/onInstall.ts b/src/background/events/onInstall.ts index cb51aa61..d6a46128 100644 --- a/src/background/events/onInstall.ts +++ b/src/background/events/onInstall.ts @@ -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 */ export default async function onInstall() { - await ExtensionStore.setVersion(chrome.runtime.getManifest().version); + await extensionStore.set('version', chrome.runtime.getManifest().version); } diff --git a/src/background/events/onUpdate.ts b/src/background/events/onUpdate.ts index 879f7430..d8821923 100644 --- a/src/background/events/onUpdate.ts +++ b/src/background/events/onUpdate.ts @@ -1,14 +1,14 @@ 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) */ export default async function onUpdate() { - await Promise.all([ - ExtensionStore.setLastUpdate(Date.now()), - ExtensionStore.setVersion(chrome.runtime.getManifest().version), - ]); + await extensionStore.set({ + version: chrome.runtime.getManifest().version, + lastUpdate: Date.now(), + }); if (process.env.NODE_ENV === 'development') { hotReloadTab(); diff --git a/src/background/handler/hotReloadingHandler.ts b/src/background/handler/hotReloadingHandler.ts index 9854b661..791c5fec 100644 --- a/src/background/handler/hotReloadingHandler.ts +++ b/src/background/handler/hotReloadingHandler.ts @@ -1,18 +1,17 @@ import HotReloadingMessages from 'src/shared/messages/HotReloadingMessages'; import { MessageHandler } from 'chrome-extension-toolkit'; -import { DevStore } from 'src/shared/storage/DevStore'; +import { devStore } from 'src/shared/storage/devStore'; const hotReloadingHandler: MessageHandler = { async reloadExtension({ sendResponse }) { - const isExtensionReloading = await DevStore.getIsExtensionReloading(); + const { isExtensionReloading, isTabReloading } = await devStore.get(['isExtensionReloading', 'isTabReloading']); if (!isExtensionReloading) return sendResponse(); - const isTabReloading = await DevStore.getIsExtensionReloading(); if (isTabReloading) { const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); const tabToReload = tabs[0]; - await DevStore.setReloadTabId(tabToReload?.id); + await devStore.set('reloadTabId', tabToReload?.id); } chrome.runtime.reload(); }, diff --git a/src/background/util/hotReloadTab.ts b/src/background/util/hotReloadTab.ts index 2a22f524..54042fdc 100644 --- a/src/background/util/hotReloadTab.ts +++ b/src/background/util/hotReloadTab.ts @@ -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) @@ -24,9 +24,7 @@ const HOT_RELOADING_WHITELIST = [ * @returns a promise that resolves when the tab is reloaded */ export async function hotReloadTab(): Promise { - const { getIsTabReloading, getReloadTabId } = DevStore; - - const [isTabReloading, reloadTabId] = await Promise.all([getIsTabReloading(), getReloadTabId()]); + const { isTabReloading, reloadTabId } = await devStore.get(['isTabReloading', 'reloadTabId']); if (!isTabReloading || !reloadTabId) return; diff --git a/src/background/util/openDebugTab.ts b/src/background/util/openDebugTab.ts index 93d5adde..77ce401d 100644 --- a/src/background/util/openDebugTab.ts +++ b/src/background/util/openDebugTab.ts @@ -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 */ export async function openDebugTab() { 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); if (isAlreadyOpen) return; - const wasVisible = await DevStore.getWasDebugTabVisible(); - const tab = await chrome.tabs.create({ url: chrome.runtime.getURL('debug.html'), - active: wasVisible, + active: wasDebugTabVisible, pinned: true, index: 0, }); - await DevStore.setDebugTabId(tab.id); + await devStore.set('debugTabId', tab.id); } } diff --git a/src/debug/index.tsx b/src/debug/index.tsx index 7649c5c8..c4c5dacc 100644 --- a/src/debug/index.tsx +++ b/src/debug/index.tsx @@ -1,7 +1,7 @@ import './hotReload'; import React, { useEffect } from 'react'; import { render } from 'react-dom'; -import { DevStore } from 'src/shared/storage/DevStore'; +import { devStore } from 'src/shared/storage/devStore'; const manifest = chrome.runtime.getManifest(); @@ -69,11 +69,7 @@ function DevDashboard() { useEffect(() => { const onVisibilityChange = () => { - if (document.visibilityState === 'visible') { - DevStore.setWasDebugTabVisible(true); - } else { - DevStore.setWasDebugTabVisible(false); - } + devStore.set('wasDebugTabVisible', document.visibilityState === 'visible'); }; document.addEventListener('visibilitychange', onVisibilityChange); return () => { diff --git a/src/shared/storage/DevStore.ts b/src/shared/storage/DevStore.ts index 90cc679d..b982d2b6 100644 --- a/src/shared/storage/DevStore.ts +++ b/src/shared/storage/DevStore.ts @@ -16,7 +16,7 @@ interface IDevStore { reloadTabId?: number; } -export const DevStore = createLocalStore({ +export const devStore = createLocalStore({ debugTabId: undefined, isTabReloading: true, wasDebugTabVisible: false, @@ -24,5 +24,4 @@ export const DevStore = createLocalStore({ reloadTabId: undefined, }); - -debugStore({ DevStore }); +debugStore({ devStore }); diff --git a/src/shared/storage/ExtensionStore.ts b/src/shared/storage/ExtensionStore.ts index ca576ba9..14b8ef8a 100644 --- a/src/shared/storage/ExtensionStore.ts +++ b/src/shared/storage/ExtensionStore.ts @@ -1,4 +1,3 @@ -import { v4 as uuidv4 } from 'uuid'; import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; /** @@ -9,31 +8,11 @@ interface IExtensionStore { version: string; /** When was the last update */ lastUpdate: number; - /** A unique identifier generated for the current user in lieu of a userId */ - deviceId: string; } -interface Actions { - getDeviceId(): Promise; -} +export const extensionStore = createLocalStore({ + version: chrome.runtime.getManifest().version, + lastUpdate: Date.now(), +}); -export const ExtensionStore = createLocalStore( - { - 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 }); +debugStore({ extensionStore }); diff --git a/src/shared/storage/OptionsStore.ts b/src/shared/storage/OptionsStore.ts index bc5cccbf..a7b1e12f 100644 --- a/src/shared/storage/OptionsStore.ts +++ b/src/shared/storage/OptionsStore.ts @@ -10,9 +10,9 @@ interface IOptionsStore { shouldScrollToLoad: boolean; } -export const OptionsStore = createSyncStore({ +export const optionsStore = createSyncStore({ shouldHighlightConflicts: true, shouldScrollToLoad: true, }); -debugStore({ OptionsStore }); +debugStore({ optionsStore }); diff --git a/src/shared/storage/SessionStore.ts b/src/shared/storage/SessionStore.ts index a8879d58..97e8a339 100644 --- a/src/shared/storage/SessionStore.ts +++ b/src/shared/storage/SessionStore.ts @@ -4,8 +4,8 @@ interface ISessionStore { chromeSessionId?: string; } -export const SessionStore = createSessionStore({ +export const sessionStore = createSessionStore({ chromeSessionId: undefined, }); -debugStore({ SessionStore }); +debugStore({ sessionStore }); diff --git a/src/shared/storage/UserScheduleStore.ts b/src/shared/storage/UserScheduleStore.ts index 8a590fab..98100ddb 100644 --- a/src/shared/storage/UserScheduleStore.ts +++ b/src/shared/storage/UserScheduleStore.ts @@ -1,62 +1,15 @@ import { createLocalStore, debugStore } from 'chrome-extension-toolkit'; -import { Course } from 'src/shared/types/Course'; -/** - * A store that is used for storing user options - */ +import { UserSchedule } from 'src/shared/types/UserSchedule'; + interface IUserScheduleStore { - current: string; - schedules: { - [id: string]: Course[]; - }; + schedules: UserSchedule[]; } -interface Actions { - createSchedule(name: string): Promise; - addCourseToSchedule(name: string, course: Course): Promise; - removeCourseFromSchedule(name: string, course: Course): Promise; - removeSchedule(name: string): Promise; - getSchedule(name: string): Promise; -} +/** + * A store that is used for storing user schedules (and the active schedule) + */ +export const userScheduleStore = createLocalStore({ + schedules: [], +}); -const UserScheduleStore = createLocalStore( - { - 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 }); +debugStore({ userScheduleStore }); diff --git a/src/shared/types/Course.ts b/src/shared/types/Course.ts index 364cabdf..3e0e46f6 100644 --- a/src/shared/types/Course.ts +++ b/src/shared/types/Course.ts @@ -45,6 +45,9 @@ export class Course { courseName: string; /** The unique identifier for which department that a course belongs to, i.e. CS, MAL, etc. */ department: string; + + /** The number of credits that a course is worth */ + creditHours: number; /** Is the course open, closed, waitlisted, or cancelled? */ status: Status; /** all the people that are teaching this course, and some metadata about their names */ diff --git a/src/shared/types/UserSchedule.ts b/src/shared/types/UserSchedule.ts new file mode 100644 index 00000000..c3f9db0a --- /dev/null +++ b/src/shared/types/UserSchedule.ts @@ -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) { + 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); + } +} diff --git a/src/views/components/CourseCatalogMain.tsx b/src/views/components/CourseCatalogMain.tsx index cd2db505..d33882f9 100644 --- a/src/views/components/CourseCatalogMain.tsx +++ b/src/views/components/CourseCatalogMain.tsx @@ -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 && } + {selectedCourse && ( + + )} ); diff --git a/src/views/components/injected/CoursePopup/CourseHeader/CourseButtons/CourseButtons.tsx b/src/views/components/injected/CoursePopup/CourseHeader/CourseButtons/CourseButtons.tsx index 3d3ad3f1..c40575a1 100644 --- a/src/views/components/injected/CoursePopup/CourseHeader/CourseButtons/CourseButtons.tsx +++ b/src/views/components/injected/CoursePopup/CourseHeader/CourseButtons/CourseButtons.tsx @@ -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 ( -