Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: injected button - add all courses from MyUT AND passing URL to handler #291

Merged
merged 33 commits into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0b5fb0c
feat: first button attempt
DereC4 Oct 8, 2024
b1bbbc0
feat: fetching each course code
DereC4 Oct 8, 2024
406ab0c
feat: adding courses function from there but idk where to get the act…
DereC4 Oct 8, 2024
766ca8a
docs: todo
DereC4 Oct 9, 2024
459d463
feat: retrieved active schedule
DereC4 Oct 9, 2024
9325ad0
feat: button tactics
DereC4 Oct 9, 2024
3ee2035
feat: add support for my.utexas.edu
EthanL06 Oct 14, 2024
60d0002
feat: inject button into MyUT
EthanL06 Oct 14, 2024
b6cab46
feat: refactor code to render components dynamically based on site
EthanL06 Oct 14, 2024
3cd37f2
feat: scrape course ids from MyUT and remove duplicates
EthanL06 Oct 14, 2024
748cc8e
feat: site support links for classlist
DereC4 Oct 14, 2024
596195d
Merge branch 'main' of https://github.com/Longhorn-Developers/UT-Regi…
EthanL06 Oct 21, 2024
a9fc6f0
feat: add utility function to add course by URL
EthanL06 Oct 21, 2024
bde7712
feat: support additional case for course cal
DereC4 Oct 30, 2024
3dbc56c
feat: duplicates
DereC4 Oct 30, 2024
99d732f
chore: cleanup
DereC4 Oct 30, 2024
3ccb699
feat: temporary checkpoint
DereC4 Nov 11, 2024
cd0aac2
feat: reroute to use new add course by url
DereC4 Nov 14, 2024
9474260
feat: linking to new function, cleaning up, adding messaging for cour…
DereC4 Nov 14, 2024
44bcf07
chore: unused import
DereC4 Nov 14, 2024
1c41cdb
feat: relinking addCourse function to the button fingers crossed
DereC4 Nov 14, 2024
de544e1
feat: we did it!
DereC4 Nov 14, 2024
1197227
Merge branch 'main' into derek/injectedAddButton2
DereC4 Nov 14, 2024
5634d3b
chore: remove comment
DereC4 Nov 14, 2024
ad3e70d
chore: cleanup cleanup
DereC4 Nov 14, 2024
11d404e
feat: tried to handle the async stuff because of that small bug but n…
DereC4 Nov 14, 2024
81e0407
feat: i have fixed it holy kevinnn
DereC4 Nov 14, 2024
6afb8e0
chore: delete unused file and organization
DereC4 Nov 15, 2024
dd0fc44
chore: removed unused log
DereC4 Nov 15, 2024
0deef0c
feat: better log for course add
DereC4 Nov 15, 2024
12cbaeb
chore: refactor via data destructuring
doprz Nov 15, 2024
7b20732
chore: pass component as prop via React.ComponentType
doprz Nov 15, 2024
66b7a19
Merge branch 'Longhorn-Developers:main' into derek/injectedAddButton2
DereC4 Nov 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => ({
Expand Down
9 changes: 9 additions & 0 deletions src/pages/background/handler/userScheduleHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
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;
2 changes: 1 addition & 1 deletion src/pages/background/lib/addCourse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`);
}
64 changes: 64 additions & 0 deletions src/pages/background/lib/addCourseByURL.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
// 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);
}
}
13 changes: 11 additions & 2 deletions src/pages/content/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<CourseCatalogMain support={support} />
<Component />
</React.StrictMode>
);
};

if (support === SiteSupport.COURSE_CATALOG_DETAILS || support === SiteSupport.COURSE_CATALOG_LIST) {
renderComponent(() => <CourseCatalogMain support={support} />);
}

if (support === SiteSupport.MY_UT) {
renderComponent(InjectedButton);
}
6 changes: 6 additions & 0 deletions src/shared/messages/UserScheduleMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 68 additions & 0 deletions src/views/components/injected/AddAllButton.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>(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(
<ExtensionRoot>
<Button variant='filled' color='ut-burntorange' onClick={extractCoursesFromCalendar}>
Add Courses to UT Registration+
</Button>
</ExtensionRoot>,
container
);
}
56 changes: 10 additions & 46 deletions src/views/components/settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
// };
DereC4 marked this conversation as resolved.
Show resolved Hide resolved

const [devMode, toggleDevMode] = useDevMode(10);

Expand Down Expand Up @@ -445,7 +409,7 @@ export default function Settings(): JSX.Element {
<h2 className='mb-4 text-xl text-ut-black font-semibold' onClick={toggleDevMode}>
Developer Mode
</h2>
<Button variant='filled' color='ut-black' onClick={handleAddCourseByUrl}>
<Button variant='filled' color='ut-black' onClick={() => addCourseByURL(activeSchedule)}>
Add course by link
</Button>
<Button variant='filled' color='ut-burntorange' onClick={showMigrationDialog}>
Expand Down
8 changes: 8 additions & 0 deletions src/views/lib/getSiteSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const SiteSupport = {
EXTENSION_POPUP: 'EXTENSION_POPUP',
MY_CALENDAR: 'MY_CALENDAR',
REPORT_ISSUE: 'REPORT_ISSUE',
MY_UT: 'MY_UT',
CLASSLIST: 'CLASSLIST',
} as const;

/**
Expand Down Expand Up @@ -46,5 +48,11 @@ export default function getSiteSupport(url: string): SiteSupportType | null {
if (url.includes('utdirect.utexas.edu') && (url.includes('waitlist') || url.includes('classlist'))) {
return SiteSupport.WAITLIST;
}
if (url.includes('my.utexas.edu/student/student/index') || url.includes('my.utexas.edu/student/')) {
return SiteSupport.MY_UT;
}
if (url.includes('registration/classlist.WBX')) {
return SiteSupport.CLASSLIST;
}
return null;
}
Loading