feat: course-catalog-injected-popup (#98)
* some work * some work on course popup update the stories and create the header component * use chip component in header * complete CourseHeaderAndActions Component added course buttons, using proper subcomponents now. * Change test course to 314 * Add rmp callback * some unocss updates * add course button onclick handlers * add todo for calendar button * Rename CoursePopup Old one to "Old", remove "2" from new one * description stuff done * Modify story to use proper course info * Add Grade Distribution Stuff * Minor tweaks change style in header * Add TODO replace current grade colors with a tailwind palette * Fix syllabi url Remove unused variable and unnecessary args to url * Bunch of renaming * Kinda complete the handlers * change grade distribution colors to match updated figma * change from reducer pattern to state variables, remove chartData from state * add additional story * disabled add when course is not open * use array fill * Some changes with the instructor names * trying to get the CES stuff to work * CES button is working * remove a todo * add actual color for dminus * fix description, start no distribution state * post merge fixes * small fixes * fix: import as type * fix: some better typescript stuff i think * fix: manifest.ts * fix: pr feedback * Apply suggestions from code review --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
@@ -17,6 +17,7 @@ const HOST_PERMISSIONS: string[] = [
|
|||||||
'*://*.catalog.utexas.edu/ribbit/',
|
'*://*.catalog.utexas.edu/ribbit/',
|
||||||
'*://*.registrar.utexas.edu/schedules/*',
|
'*://*.registrar.utexas.edu/schedules/*',
|
||||||
'*://*.login.utexas.edu/login/*',
|
'*://*.login.utexas.edu/login/*',
|
||||||
|
'https://utexas.bluera.com/*',
|
||||||
];
|
];
|
||||||
|
|
||||||
const manifest = defineManifest(async () => ({
|
const manifest = defineManifest(async () => ({
|
||||||
@@ -26,7 +27,7 @@ const manifest = defineManifest(async () => ({
|
|||||||
description: packageJson.description,
|
description: packageJson.description,
|
||||||
options_page: 'src/pages/options/index.html',
|
options_page: 'src/pages/options/index.html',
|
||||||
background: { service_worker: 'src/pages/background/background.ts' },
|
background: { service_worker: 'src/pages/background/background.ts' },
|
||||||
permissions: ['storage', 'unlimitedStorage', 'background'],
|
permissions: ['storage', 'unlimitedStorage', 'background', 'scripting'],
|
||||||
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
|
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
|
||||||
action: {
|
action: {
|
||||||
default_popup: 'src/pages/popup/index.html',
|
default_popup: 'src/pages/popup/index.html',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { MessageListener } from 'chrome-extension-toolkit';
|
|||||||
import onInstall from './events/onInstall';
|
import onInstall from './events/onInstall';
|
||||||
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
|
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
|
||||||
import onUpdate from './events/onUpdate';
|
import onUpdate from './events/onUpdate';
|
||||||
|
import CESHandler from './handler/CESHandler';
|
||||||
import browserActionHandler from './handler/browserActionHandler';
|
import browserActionHandler from './handler/browserActionHandler';
|
||||||
import tabManagementHandler from './handler/tabManagementHandler';
|
import tabManagementHandler from './handler/tabManagementHandler';
|
||||||
import userScheduleHandler from './handler/userScheduleHandler';
|
import userScheduleHandler from './handler/userScheduleHandler';
|
||||||
@@ -32,6 +33,7 @@ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
|
|||||||
...browserActionHandler,
|
...browserActionHandler,
|
||||||
...tabManagementHandler,
|
...tabManagementHandler,
|
||||||
...userScheduleHandler,
|
...userScheduleHandler,
|
||||||
|
...CESHandler,
|
||||||
});
|
});
|
||||||
|
|
||||||
messageListener.listen();
|
messageListener.listen();
|
||||||
|
|||||||
39
src/pages/background/handler/CESHandler.ts
Normal file
39
src/pages/background/handler/CESHandler.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import openNewTab from '@background/util/openNewTab';
|
||||||
|
import type CESMessage from '@shared/messages/CESMessage';
|
||||||
|
import type { MessageHandler } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
|
const CESFall2023Url = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580®l=en-US';
|
||||||
|
|
||||||
|
const CESHandler: MessageHandler<CESMessage> = {
|
||||||
|
openCESPage({ data, sendResponse }) {
|
||||||
|
const { instructorFirstName, instructorLastName } = data;
|
||||||
|
openNewTab(CESFall2023Url).then(tab => {
|
||||||
|
const instructorFirstAndLastName = [instructorFirstName, instructorLastName];
|
||||||
|
chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id },
|
||||||
|
func: (...instructorFirstAndLastName: String[]) => {
|
||||||
|
const inputElement = document.getElementById(
|
||||||
|
'ctl00_ContentPlaceHolder1_ViewList_tbxValue'
|
||||||
|
) as HTMLInputElement | null;
|
||||||
|
const [instructorFirstName, instructorLastName] = instructorFirstAndLastName;
|
||||||
|
if (inputElement) {
|
||||||
|
inputElement.value = `${instructorFirstName} ${instructorLastName}`;
|
||||||
|
inputElement.focus();
|
||||||
|
const enterKeyEvent = new KeyboardEvent('keydown', {
|
||||||
|
key: 'Enter',
|
||||||
|
code: 'Enter',
|
||||||
|
keyCode: 13,
|
||||||
|
which: 13,
|
||||||
|
bubbles: true,
|
||||||
|
});
|
||||||
|
inputElement.dispatchEvent(enterKeyEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
args: instructorFirstAndLastName,
|
||||||
|
});
|
||||||
|
sendResponse(tab);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CESHandler;
|
||||||
10
src/shared/messages/CESMessage.ts
Normal file
10
src/shared/messages/CESMessage.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
interface CESMessage {
|
||||||
|
/**
|
||||||
|
* Opens the CES page for the specified instructor
|
||||||
|
*
|
||||||
|
* @param data first and last name of the instructor
|
||||||
|
*/
|
||||||
|
openCESPage: (data: { instructorFirstName: string; instructorLastName: string }) => chrome.tabs.Tab;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CESMessage;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createMessenger } from 'chrome-extension-toolkit';
|
import { createMessenger } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
import type BrowserActionMessages from './BrowserActionMessages';
|
import type BrowserActionMessages from './BrowserActionMessages';
|
||||||
|
import type CESMessage from './CESMessage';
|
||||||
import type TabManagementMessages from './TabManagementMessages';
|
import type TabManagementMessages from './TabManagementMessages';
|
||||||
import type TAB_MESSAGES from './TabMessages';
|
import type TAB_MESSAGES from './TabMessages';
|
||||||
import type { UserScheduleMessages } from './UserScheduleMessages';
|
import type { UserScheduleMessages } from './UserScheduleMessages';
|
||||||
@@ -8,7 +9,7 @@ import type { UserScheduleMessages } from './UserScheduleMessages';
|
|||||||
/**
|
/**
|
||||||
* This is a type with all the message definitions that can be sent TO the background script
|
* This is a type with all the message definitions that can be sent TO the background script
|
||||||
*/
|
*/
|
||||||
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & UserScheduleMessages;
|
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & UserScheduleMessages & CESMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility object that can be used to send type-safe messages to the background script
|
* A utility object that can be used to send type-safe messages to the background script
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const colors = {
|
|||||||
cplus: '#F59E0B',
|
cplus: '#F59E0B',
|
||||||
c: '#FB923C',
|
c: '#FB923C',
|
||||||
cminus: '#F97316',
|
cminus: '#F97316',
|
||||||
dplus: '#EA580C', // TODO (achadaga): copilot generated, get actual color from Isaiah
|
dplus: '#EF4444',
|
||||||
d: '#DC2626',
|
d: '#DC2626',
|
||||||
dminus: '#B91C1C',
|
dminus: '#B91C1C',
|
||||||
f: '#B91C1C',
|
f: '#B91C1C',
|
||||||
|
|||||||
@@ -9,6 +9,51 @@ const exampleSchedule: UserSchedule = new UserSchedule({
|
|||||||
name: 'Example Schedule',
|
name: 'Example Schedule',
|
||||||
hours: 0,
|
hours: 0,
|
||||||
});
|
});
|
||||||
|
// TODO (achadaga): import this after
|
||||||
|
// https://github.com/Longhorn-Developers/UT-Registration-Plus/pull/106 is merged
|
||||||
|
const bevoCourse: Course = new Course({
|
||||||
|
uniqueId: 47280,
|
||||||
|
number: '311C',
|
||||||
|
fullName: "BVO 311C BEVO'S SEMINAR LONGHORN CARE",
|
||||||
|
courseName: "BEVO'S SEMINAR LONGHORN CARE",
|
||||||
|
department: 'BVO',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.OPEN,
|
||||||
|
instructors: [new Instructor({ fullName: 'BEVO', firstName: '', lastName: 'BEVO', middleInitial: '' })],
|
||||||
|
isReserved: false,
|
||||||
|
description: [
|
||||||
|
'Restricted to Students in the School of Longhorn Enthusiasts',
|
||||||
|
'Immerse yourself in the daily routine of a longhorn—sunrise pasture walks and the best shady spots for a midday siesta. Understand the behavioral science behind our mascot’s stoic demeanor during games.',
|
||||||
|
'BVO 311C and 312H may not both be counted.',
|
||||||
|
'Prerequisite: Grazing 311 or 311H.',
|
||||||
|
'May be counted toward the Independent Inquiry flag requirement. May be counted toward the Writing flag requirement',
|
||||||
|
'Offered on the letter-grade basis only.',
|
||||||
|
],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Tuesday', 'Thursday'],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: { building: 'UTC', room: '123' },
|
||||||
|
}),
|
||||||
|
new CourseMeeting({
|
||||||
|
days: ['Thursday'],
|
||||||
|
startTime: 570,
|
||||||
|
endTime: 630,
|
||||||
|
location: { building: 'JES', room: '123' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
flags: ['Independent Inquiry', 'Writing'],
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
||||||
@@ -40,10 +85,25 @@ const meta = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
export const Default: Story = {
|
export const OpenCourse: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: exampleCourse,
|
course: exampleCourse,
|
||||||
activeSchedule: exampleSchedule,
|
activeSchedule: exampleSchedule,
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ClosedCourse: Story = {
|
||||||
|
args: {
|
||||||
|
course: {
|
||||||
|
...exampleCourse,
|
||||||
|
status: Status.CLOSED,
|
||||||
|
} satisfies Course,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CourseWithNoData: Story = {
|
||||||
|
args: {
|
||||||
|
course: bevoCourse,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ import type { SiteSupportType } from '@views/lib/getSiteSupport';
|
|||||||
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
|
import { populateSearchInputs } from '@views/lib/populateSearchInputs';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useKeyPress } from '../hooks/useKeyPress';
|
||||||
|
import useSchedules from '../hooks/useSchedules';
|
||||||
|
import { CourseCatalogScraper } from '../lib/CourseCatalogScraper';
|
||||||
|
import getCourseTableRows from '../lib/getCourseTableRows';
|
||||||
|
import type { SiteSupport } from '../lib/getSiteSupport';
|
||||||
|
import { populateSearchInputs } from '../lib/populateSearchInputs';
|
||||||
|
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
||||||
|
import AutoLoad from './injected/AutoLoad/AutoLoad';
|
||||||
|
import CourseCatalogInjectedPopup from './injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
|
||||||
|
import TableHead from './injected/TableHead';
|
||||||
|
import TableRow from './injected/TableRow/TableRow';
|
||||||
|
import TableSubheading from './injected/TableSubheading/TableSubheading';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
|
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,32 @@
|
|||||||
|
import type { Course } from '@shared/types/Course';
|
||||||
|
import Spinner from '@views/components/common/Spinner/Spinner';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
|
||||||
|
import { SiteSupport } from '@views/lib/getSiteSupport';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface DescriptionProps {
|
interface DescriptionProps {
|
||||||
lines: string[];
|
course: Course;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LoadStatus = {
|
||||||
|
LOADING: 'LOADING',
|
||||||
|
DONE: 'DONE',
|
||||||
|
ERROR: 'ERROR',
|
||||||
|
} as const;
|
||||||
|
type LoadStatusType = (typeof LoadStatus)[keyof typeof LoadStatus];
|
||||||
|
|
||||||
|
async function fetchDescription(course: Course): Promise<string[]> {
|
||||||
|
if (!course.description?.length) {
|
||||||
|
const response = await fetch(course.url);
|
||||||
|
const text = await response.text();
|
||||||
|
const doc = new DOMParser().parseFromString(text, 'text/html');
|
||||||
|
|
||||||
|
const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS);
|
||||||
|
course.description = scraper.getDescription(doc);
|
||||||
|
}
|
||||||
|
return course.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,27 +34,53 @@ interface DescriptionProps {
|
|||||||
*
|
*
|
||||||
* @component
|
* @component
|
||||||
* @param {DescriptionProps} props - The component props.
|
* @param {DescriptionProps} props - The component props.
|
||||||
* @param {string[]} props.lines - The lines of text to render.
|
* @param {Course} props.course - The course for which to display the description.
|
||||||
* @returns {JSX.Element} The rendered description component.
|
* @returns {JSX.Element} The rendered description component.
|
||||||
*/
|
*/
|
||||||
const Description: React.FC<DescriptionProps> = ({ lines }: DescriptionProps) => {
|
const Description: React.FC<DescriptionProps> = ({ course }: DescriptionProps) => {
|
||||||
|
const [description, setDescription] = React.useState<string[]>([]);
|
||||||
|
const [status, setStatus] = React.useState<LoadStatusType>(LoadStatus.LOADING);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
fetchDescription(course)
|
||||||
|
.then(description => {
|
||||||
|
setStatus(LoadStatus.DONE);
|
||||||
|
setDescription(description);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setStatus(LoadStatus.ERROR);
|
||||||
|
});
|
||||||
|
}, [course]);
|
||||||
|
|
||||||
const keywords = ['prerequisite', 'restricted'];
|
const keywords = ['prerequisite', 'restricted'];
|
||||||
return (
|
return (
|
||||||
<ul className='my-[5px] space-y-1.5 children:marker:text-ut-burntorange'>
|
<>
|
||||||
{lines.map(line => {
|
{status === LoadStatus.ERROR && (
|
||||||
const isKeywordPresent = keywords.some(keyword => line.toLowerCase().includes(keyword));
|
<Text color='theme-red'>Please refresh the page and log back in using your UT EID and password</Text>
|
||||||
return (
|
)}
|
||||||
<div className='flex gap-2'>
|
{/* TODO (achadaga): would be nice to have a new spinner here */}
|
||||||
<span className='text-ut-burntorange'>•</span>
|
{status === LoadStatus.LOADING && <Spinner />}
|
||||||
<li key={line}>
|
{status === LoadStatus.DONE && (
|
||||||
<Text variant='p' className={clsx({ 'font-bold text-ut-burntorange': isKeywordPresent })}>
|
<ul className='my-[5px] space-y-1.5 children:marker:text-ut-burntorange'>
|
||||||
{line}
|
{description.map(line => {
|
||||||
</Text>
|
const isKeywordPresent = keywords.some(keyword => line.toLowerCase().includes(keyword));
|
||||||
</li>
|
return (
|
||||||
</div>
|
<div key={line} className='flex gap-2'>
|
||||||
);
|
<span className='text-ut-burntorange'>•</span>
|
||||||
})}
|
<li key={line}>
|
||||||
</ul>
|
<Text
|
||||||
|
variant='p'
|
||||||
|
className={clsx({ 'font-bold text-ut-burntorange': isKeywordPresent })}
|
||||||
|
>
|
||||||
|
{line}
|
||||||
|
</Text>
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const DataStatus = {
|
|||||||
NOT_FOUND: 'NOT_FOUND',
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
ERROR: 'ERROR',
|
ERROR: 'ERROR',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type DataStatusType = (typeof DataStatus)[keyof typeof DataStatus];
|
type DataStatusType = (typeof DataStatus)[keyof typeof DataStatus];
|
||||||
|
|
||||||
const GRADE_COLORS = {
|
const GRADE_COLORS = {
|
||||||
@@ -61,7 +60,7 @@ const GradeDistribution: React.FC<GradeDistributionProps> = ({ course }) => {
|
|||||||
color: GRADE_COLORS[grade as LetterGrade],
|
color: GRADE_COLORS[grade as LetterGrade],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return [];
|
return Array(12).fill(0);
|
||||||
}, [distributions, semester, status]);
|
}, [distributions, semester, status]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -137,8 +136,21 @@ const GradeDistribution: React.FC<GradeDistributionProps> = ({ course }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='pb-[25px] pt-[12px]'>
|
<div className='pb-[25px] pt-[12px]'>
|
||||||
|
{/* TODO (achadaga): again would be nice to have an updated spinner */}
|
||||||
{status === DataStatus.LOADING && <Spinner />}
|
{status === DataStatus.LOADING && <Spinner />}
|
||||||
{status === DataStatus.NOT_FOUND && <Text variant='p'>No grade distribution data found</Text>}
|
{status === DataStatus.NOT_FOUND && (
|
||||||
|
<HighchartsReact
|
||||||
|
ref={ref}
|
||||||
|
highcharts={Highcharts}
|
||||||
|
options={{
|
||||||
|
...chartOptions,
|
||||||
|
title: {
|
||||||
|
text: `There is currently no grade distribution data for ${course.department} ${course.number}`,
|
||||||
|
},
|
||||||
|
tooltip: { enabled: false },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
|
{status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
|
||||||
{status === DataStatus.FOUND && (
|
{status === DataStatus.FOUND && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { background } from '@shared/messages'
|
||||||
|
import { Status } from '@shared/types/Course';
|
||||||
|
import type Instructor from '@shared/types/Instructor';
|
||||||
import addCourse from '@pages/background/lib/addCourse';
|
import addCourse from '@pages/background/lib/addCourse';
|
||||||
import removeCourse from '@pages/background/lib/removeCourse';
|
import removeCourse from '@pages/background/lib/removeCourse';
|
||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
@@ -18,11 +21,13 @@ import Mood from '~icons/material-symbols/mood';
|
|||||||
import Remove from '~icons/material-symbols/remove';
|
import Remove from '~icons/material-symbols/remove';
|
||||||
import Reviews from '~icons/material-symbols/reviews';
|
import Reviews from '~icons/material-symbols/reviews';
|
||||||
|
|
||||||
|
const { openNewTab, addCourse, removeCourse, openCESPage } = background;
|
||||||
|
|
||||||
interface HeadingAndActionProps {
|
interface HeadingAndActionProps {
|
||||||
/* The course to display */
|
/* The course to display */
|
||||||
course: Course;
|
course: Course;
|
||||||
/* The active schedule */
|
/* The active schedule */
|
||||||
activeSchedule: UserSchedule;
|
activeSchedule?: UserSchedule;
|
||||||
/* The function to call when the popup should be closed */
|
/* The function to call when the popup should be closed */
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
@@ -31,59 +36,83 @@ interface HeadingAndActionProps {
|
|||||||
* Opens the calendar in a new tab.
|
* Opens the calendar in a new tab.
|
||||||
* @returns {Promise<void>} A promise that resolves when the tab is opened.
|
* @returns {Promise<void>} A promise that resolves when the tab is opened.
|
||||||
*/
|
*/
|
||||||
export const handleOpenCalendar = async () => {
|
export const handleOpenCalendar = async (): Promise<void> => {
|
||||||
const url = chrome.runtime.getURL('calendar.html');
|
const url = chrome.runtime.getURL('calendar.html');
|
||||||
await openTabFromContentScript(url);
|
openNewTab({ url });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const capitalizeString = (str: string) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the heading component for the CoursePopup component.
|
* Renders the heading component for the CoursePopup component.
|
||||||
*
|
*
|
||||||
* @param {HeadingAndActionProps} props - The component props.
|
* @param {HeadingAndActionProps} props - The component props.
|
||||||
|
* @param {Course} props.course - The course object containing course details.
|
||||||
|
* @param {Schedule} props.activeSchedule - The active schedule object.
|
||||||
|
* @param {Function} props.onClose - The function to close the popup.
|
||||||
* @returns {JSX.Element} The rendered component.
|
* @returns {JSX.Element} The rendered component.
|
||||||
*/
|
*/
|
||||||
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
|
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({
|
||||||
|
course,
|
||||||
|
activeSchedule,
|
||||||
|
onClose,
|
||||||
|
}: HeadingAndActionProps): JSX.Element => {
|
||||||
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
|
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
|
||||||
const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId);
|
const [courseAdded, setCourseAdded] = useState<boolean>(
|
||||||
|
activeSchedule !== undefined ? activeSchedule.courses.some(course => course.uniqueId === uniqueId) : false
|
||||||
|
);
|
||||||
|
|
||||||
|
const getInstructorFullName = (instructor: Instructor) => {
|
||||||
|
const { firstName, lastName } = instructor;
|
||||||
|
if (firstName === '') return capitalizeString(lastName);
|
||||||
|
return `${capitalizeString(firstName)} ${capitalizeString(lastName)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const instructorString = instructors.map(getInstructorFullName).join(', ');
|
||||||
|
|
||||||
const instructorString = instructors
|
|
||||||
.map(instructor => {
|
|
||||||
const { firstName, lastName } = instructor;
|
|
||||||
if (firstName === '') return lastName;
|
|
||||||
return `${firstName} ${lastName}`;
|
|
||||||
})
|
|
||||||
.join(', ');
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
navigator.clipboard.writeText(uniqueId.toString());
|
navigator.clipboard.writeText(uniqueId.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenRateMyProf = async () => {
|
const handleOpenRateMyProf = async () => {
|
||||||
const openTabs = instructors.map(instructor => {
|
const openTabs = instructors.map(instructor => {
|
||||||
const { fullName } = instructor;
|
const instructorSearchTerm = getInstructorFullName(instructor);
|
||||||
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${fullName}`;
|
instructorSearchTerm.replace(' ', '+');
|
||||||
return openTabFromContentScript(url);
|
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${instructorSearchTerm}`;
|
||||||
|
return openNewTab({ url });
|
||||||
});
|
});
|
||||||
await Promise.all(openTabs);
|
await Promise.all(openTabs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenCES = async () => {
|
const handleOpenCES = async () => {
|
||||||
// TODO: does not look up the professor just takes you to the page
|
const openTabs = instructors.map(instructor => {
|
||||||
const cisUrl = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580®l=en-US';
|
let { firstName, lastName } = instructor;
|
||||||
await openTabFromContentScript(cisUrl);
|
firstName = capitalizeString(firstName);
|
||||||
|
lastName = capitalizeString(lastName);
|
||||||
|
return openCESPage({ instructorFirstName: firstName, instructorLastName: lastName });
|
||||||
|
});
|
||||||
|
await Promise.all(openTabs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenPastSyllabi = async () => {
|
const handleOpenPastSyllabi = async () => {
|
||||||
// not specific to professor
|
// not specific to professor
|
||||||
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=${courseName}&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`;
|
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=${courseName}&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`;
|
||||||
await openTabFromContentScript(url);
|
openNewTab({ url });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddOrRemoveCourse = async () => {
|
const handleAddOrRemoveCourse = async () => {
|
||||||
|
if (!activeSchedule) return;
|
||||||
if (!courseAdded) {
|
if (!courseAdded) {
|
||||||
await addCourse(activeSchedule.name, course);
|
addCourse({ course, scheduleName: activeSchedule.name });
|
||||||
} else {
|
} else {
|
||||||
await removeCourse(activeSchedule.name, course);
|
removeCourse({ course, scheduleName: activeSchedule.name });
|
||||||
}
|
}
|
||||||
|
setCourseAdded(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full pb-3 pt-6'>
|
<div className='w-full pb-3 pt-6'>
|
||||||
<div className='flex flex-col gap-1'>
|
<div className='flex flex-col'>
|
||||||
<div className='flex items-center gap-1'>
|
<div className='flex items-center gap-1'>
|
||||||
<Text variant='h1' className='truncate'>
|
<Text variant='h1' className='truncate'>
|
||||||
{courseName}
|
{courseName}
|
||||||
@@ -99,36 +128,35 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, a
|
|||||||
<CloseIcon className='h-7 w-7' />
|
<CloseIcon className='h-7 w-7' />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex gap-2.5 flex-content-center'>
|
<div className='flex gap-2 flex-content-center'>
|
||||||
<Text variant='h4' className='inline-flex items-center justify-center'>
|
{instructorString.length > 0 && (
|
||||||
with {instructorString}
|
<Text variant='h4' className='inline-flex items-center justify-center'>
|
||||||
</Text>
|
with {instructorString}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
<div className='flex-content-centr flex gap-1'>
|
<div className='flex-content-centr flex gap-1'>
|
||||||
{flags.map(flag => (
|
{flags.map(flag => (
|
||||||
<Chip label={flagMap[flag]} />
|
<Chip key={flagMap[flag]} label={flagMap[flag]} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
{schedule.meetings.map(meeting => (
|
{schedule.meetings.map(meeting => {
|
||||||
<Text variant='h4'>
|
const daysString = meeting.getDaysString({ format: 'long', separator: 'long' });
|
||||||
{meeting.getDaysString({ format: 'long', separator: 'long' })}{' '}
|
const timeString = meeting.getTimeString({ separator: ' to ', capitalize: false });
|
||||||
{meeting.getTimeString({ separator: ' to ', capitalize: false })}
|
const locationString = meeting.location ? ` in ${meeting.location.building}` : '';
|
||||||
{meeting.location && (
|
return (
|
||||||
<>
|
<Text key={daysString + timeString + locationString} variant='h4'>
|
||||||
{` in `}
|
{daysString} {timeString}
|
||||||
<Text variant='h4' className='text-ut-burntorange underline'>
|
{locationString}
|
||||||
{meeting.location.building}
|
</Text>
|
||||||
</Text>
|
);
|
||||||
</>
|
})}
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
|
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
|
||||||
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
|
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
|
||||||
<Divider orientation='vertical' size='28px' />
|
<Divider size='1.75rem' orientation='vertical' />
|
||||||
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
|
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
|
||||||
RateMyProf
|
RateMyProf
|
||||||
</Button>
|
</Button>
|
||||||
@@ -140,6 +168,7 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, a
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant='filled'
|
variant='filled'
|
||||||
|
disabled={course.status !== Status.OPEN}
|
||||||
color={!courseAdded ? 'ut-green' : 'ut-red'}
|
color={!courseAdded ? 'ut-green' : 'ut-red'}
|
||||||
icon={!courseAdded ? Add : Remove}
|
icon={!courseAdded ? Add : Remove}
|
||||||
onClick={handleAddOrRemoveCourse}
|
onClick={handleAddOrRemoveCourse}
|
||||||
|
|||||||
Reference in New Issue
Block a user