diff --git a/src/manifest.ts b/src/manifest.ts index f4da2c401..4832495df 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -18,11 +18,13 @@ const nameSuffix = isBeta ? ' (beta)' : mode === 'development' ? ' (dev)' : ''; const HOST_PERMISSIONS: string[] = [ '*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*', + '*://*.utdirect.utexas.edu/registration/classlist/*', '*://*.utexas.collegescheduler.com/*', '*://*.catalog.utexas.edu/ribbit/', '*://*.registrar.utexas.edu/schedules/*', '*://*.login.utexas.edu/login/*', 'https://utexas.bluera.com/*', + '*://my.utexas.edu/student/student/*', ]; const manifest = defineManifest(async () => ({ diff --git a/src/pages/background/handler/userScheduleHandler.ts b/src/pages/background/handler/userScheduleHandler.ts index 5c53c4281..cce4be870 100644 --- a/src/pages/background/handler/userScheduleHandler.ts +++ b/src/pages/background/handler/userScheduleHandler.ts @@ -31,6 +31,15 @@ const userScheduleHandler: MessageHandler = { renameSchedule({ data, sendResponse }) { renameSchedule(data.scheduleId, data.newName).then(sendResponse); }, + // proxy so we can add courses + addCourseByURL({ data: { url, method, body, response }, sendResponse }) { + fetch(url, { + method, + body, + }) + .then(res => (response === 'json' ? res.json() : res.text())) + .then(sendResponse); + }, }; export default userScheduleHandler; diff --git a/src/pages/background/lib/addCourse.ts b/src/pages/background/lib/addCourse.ts index 2594b288f..8676e212c 100644 --- a/src/pages/background/lib/addCourse.ts +++ b/src/pages/background/lib/addCourse.ts @@ -19,6 +19,6 @@ export default async function addCourse(scheduleId: string, course: Course): Pro course.colors = getUnusedColor(activeSchedule, course); activeSchedule.courses.push(course); activeSchedule.updatedAt = Date.now(); - await UserScheduleStore.set('schedules', schedules); + console.log(`Course added: ${course.courseName} (ID: ${course.uniqueId})`); } diff --git a/src/pages/background/lib/addCourseByURL.ts b/src/pages/background/lib/addCourseByURL.ts new file mode 100644 index 000000000..c5444ff2e --- /dev/null +++ b/src/pages/background/lib/addCourseByURL.ts @@ -0,0 +1,64 @@ +import addCourse from '@pages/background/lib/addCourse'; +import { background } from '@shared/messages'; +import type { UserSchedule } from '@shared/types/UserSchedule'; +import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper'; +import getCourseTableRows from '@views/lib/getCourseTableRows'; +import { SiteSupport } from '@views/lib/getSiteSupport'; + +/** + * Adds a course to the active schedule by fetching course details from a provided URL. + * If no URL is provided, prompts the user to enter one. + * Sriram and Elie made this + * + * @param activeSchedule - The user's active schedule to which the course will be added. + * @param link - The URL from which to fetch the course details. If not provided, a prompt will ask for it. + * + * @returns A promise that resolves when the course has been added or the operation is cancelled. + * + * @throws an error if there is an issue with scraping the course details. + */ +export async function addCourseByURL(activeSchedule: UserSchedule, link?: string): Promise { + // todo: Use a proper modal instead of a prompt + // eslint-disable-next-line no-param-reassign, no-alert + if (!link) link = prompt('Enter course link') || undefined; + + // Exit if the user cancels the prompt + if (!link) return; + + try { + let htmlText: string; + try { + htmlText = await background.addCourseByURL({ + url: link, + method: 'GET', + response: 'text', + }); + } catch (e) { + // eslint-disable-next-line no-alert + alert(`Failed to fetch url '${link}'`); + return; + } + + const doc = new DOMParser().parseFromString(htmlText, 'text/html'); + + const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS, doc, link); + const tableRows = getCourseTableRows(doc); + const scrapedCourses = scraper.scrape(tableRows, false); + + if (scrapedCourses.length !== 1) return; + + const description = scraper.getDescription(doc); + const row = scrapedCourses[0]!; + const course = row.course!; + course.description = description; + + if (activeSchedule.courses.every(c => c.uniqueId !== course.uniqueId)) { + console.log('adding course'); + await addCourse(activeSchedule.id, course); + } else { + console.log('course already exists'); + } + } catch (error) { + console.error('Error scraping course:', error); + } +} diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index 0e431c823..3ad26982c 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -1,18 +1,27 @@ import CourseCatalogMain from '@views/components/CourseCatalogMain'; +import InjectedButton from '@views/components/injected/AddAllButton'; import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport'; import React from 'react'; import { createRoot } from 'react-dom/client'; const support = getSiteSupport(window.location.href); -if (support === SiteSupport.COURSE_CATALOG_DETAILS || support === SiteSupport.COURSE_CATALOG_LIST) { +const renderComponent = (Component: React.ComponentType) => { const container = document.createElement('div'); container.id = 'extension-root'; document.body.appendChild(container); createRoot(container).render( - + ); +}; + +if (support === SiteSupport.COURSE_CATALOG_DETAILS || support === SiteSupport.COURSE_CATALOG_LIST) { + renderComponent(() => ); +} + +if (support === SiteSupport.MY_UT) { + renderComponent(InjectedButton); } diff --git a/src/shared/messages/UserScheduleMessages.ts b/src/shared/messages/UserScheduleMessages.ts index f6b9ab4f8..a1a837cd7 100644 --- a/src/shared/messages/UserScheduleMessages.ts +++ b/src/shared/messages/UserScheduleMessages.ts @@ -9,6 +9,12 @@ export interface UserScheduleMessages { * @param data the schedule id and course to add */ addCourse: (data: { scheduleId: string; course: Course }) => void; + /** + * Adds a course by URL + * @param data + * @returns Response of the requested course URL + */ + addCourseByURL: (data: { url: string; method: string; body?: string; response: 'json' | 'text' }) => string; /** * Remove a course from a schedule * @param data the schedule id and course to remove diff --git a/src/views/components/injected/AddAllButton.tsx b/src/views/components/injected/AddAllButton.tsx new file mode 100644 index 000000000..f1fbe7c06 --- /dev/null +++ b/src/views/components/injected/AddAllButton.tsx @@ -0,0 +1,68 @@ +import { addCourseByURL } from '@pages/background/lib/addCourseByURL'; +import { Button } from '@views/components/common/Button'; +import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; +import useSchedules from '@views/hooks/useSchedules'; +import React, { useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; + +/** + * InjectedButton component renders a button that adds courses to UTRP from official MyUT calendar + * and adds the courses to the active schedule. + * + * @returns The rendered button component or null if the container is not found. + */ +export default function InjectedButton(): JSX.Element | null { + const [container, setContainer] = useState(null); + const [activeSchedule, _] = useSchedules(); + + const extractCoursesFromCalendar = async () => { + const calendarElement = document.querySelector('#kgoui_Rcontent_I3_Rprimary_I1_Rcontent_I1_Rcontent_I0_Ritems'); + + if (!calendarElement) { + console.error('Calendar element not found'); + return []; + } + + const anchorTags = Array.from(calendarElement.querySelectorAll('a')).filter( + anchor => !anchor.href.includes('google.com') + ); + + // Make sure to remove duplicate anchorTags using set + const uniqueAnchorTags = Array.from(new Set(anchorTags.map(a => a.href))); + + for (const a of uniqueAnchorTags) { + // eslint-disable-next-line no-await-in-loop + await addCourseByURL(activeSchedule, a); + } + }; + + useEffect(() => { + const targetElement = document.getElementById('kgoui_Rcontent_I3_Rsecondary'); + + if ( + targetElement && + targetElement.classList.contains('kgoui_container_responsive_asymmetric2_column_secondary') + ) { + const buttonContainer = document.createElement('div'); + targetElement.appendChild(buttonContainer); + setContainer(buttonContainer); + + return () => { + buttonContainer.remove(); + }; + } + }, []); + + if (!container) { + return null; + } + + return ReactDOM.createPortal( + + + , + container + ); +} diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index 6fb63f66d..7e27b3abb 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -1,6 +1,8 @@ import addCourse from '@pages/background/lib/addCourse'; +import { addCourseByURL } from '@pages/background/lib/addCourseByURL'; import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule'; import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; +// import { addCourseByUrl } from '@shared/util/courseUtils'; // import { getCourseColors } from '@shared/util/colors'; // import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import { Button } from '@views/components/common/Button'; @@ -202,51 +204,13 @@ export default function Settings(): JSX.Element { }); }; - // todo: move into a util/shared place, rather than specifically in settings - const handleAddCourseByUrl = async () => { - // todo: Use a proper modal instead of a prompt - // eslint-disable-next-line no-alert - const link: string | null = prompt('Enter course link'); - - // Exit if the user cancels the prompt - if (link === null) return; - - try { - let response: Response; - try { - response = await fetch(link); - } catch (e) { - // eslint-disable-next-line no-alert - alert(`Failed to fetch url '${link}'`); - return; - } - const text = await response.text(); - const doc = new DOMParser().parseFromString(text, 'text/html'); - - const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS, doc, link); - const tableRows = getCourseTableRows(doc); - const courses = scraper.scrape(tableRows, false); - - if (courses.length === 1) { - const description = scraper.getDescription(doc); - const row = courses[0]!; - const course = row.course!; - course.description = description; - // console.log(course); - - if (activeSchedule.courses.every(c => c.uniqueId !== course.uniqueId)) { - console.log('adding course'); - addCourse(activeSchedule.id, course); - } else { - console.log('course already exists'); - } - } else { - console.log(courses); - } - } catch (error) { - console.error('Error scraping course:', error); - } - }; + // const handleAddCourseByLink = async () => { + // // todo: Use a proper modal instead of a prompt + // const link: string | null = prompt('Enter course link'); + // // Exit if the user cancels the prompt + // if (link === null) return; + // await addCourseByUrl(link, activeSchedule); + // }; const [devMode, toggleDevMode] = useDevMode(10); @@ -445,7 +409,7 @@ export default function Settings(): JSX.Element {

Developer Mode

-