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:
Abhinav Chadaga
2024-03-02 14:04:01 -06:00
committed by doprz
parent 0c5bec8002
commit 89d03f4244
11 changed files with 284 additions and 67 deletions

View File

@@ -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 removeCourse from '@pages/background/lib/removeCourse';
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 Reviews from '~icons/material-symbols/reviews';
const { openNewTab, addCourse, removeCourse, openCESPage } = background;
interface HeadingAndActionProps {
/* The course to display */
course: Course;
/* The active schedule */
activeSchedule: UserSchedule;
activeSchedule?: UserSchedule;
/* The function to call when the popup should be closed */
onClose: () => void;
}
@@ -31,59 +36,83 @@ interface HeadingAndActionProps {
* Opens the calendar in a new tab.
* @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');
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.
*
* @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.
*/
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 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 = () => {
navigator.clipboard.writeText(uniqueId.toString());
};
const handleOpenRateMyProf = async () => {
const openTabs = instructors.map(instructor => {
const { fullName } = instructor;
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${fullName}`;
return openTabFromContentScript(url);
const instructorSearchTerm = getInstructorFullName(instructor);
instructorSearchTerm.replace(' ', '+');
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${instructorSearchTerm}`;
return openNewTab({ url });
});
await Promise.all(openTabs);
};
const handleOpenCES = async () => {
// TODO: does not look up the professor just takes you to the page
const cisUrl = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580&regl=en-US';
await openTabFromContentScript(cisUrl);
const openTabs = instructors.map(instructor => {
let { firstName, lastName } = instructor;
firstName = capitalizeString(firstName);
lastName = capitalizeString(lastName);
return openCESPage({ instructorFirstName: firstName, instructorLastName: lastName });
});
await Promise.all(openTabs);
};
const handleOpenPastSyllabi = async () => {
// 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`;
await openTabFromContentScript(url);
openNewTab({ url });
};
const handleAddOrRemoveCourse = async () => {
if (!activeSchedule) return;
if (!courseAdded) {
await addCourse(activeSchedule.name, course);
addCourse({ course, scheduleName: activeSchedule.name });
} else {
await removeCourse(activeSchedule.name, course);
removeCourse({ course, scheduleName: activeSchedule.name });
}
setCourseAdded(prev => !prev);
};
return (
<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'>
<Text variant='h1' className='truncate'>
{courseName}
@@ -99,36 +128,35 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, a
<CloseIcon className='h-7 w-7' />
</button>
</div>
<div className='flex gap-2.5 flex-content-center'>
<Text variant='h4' className='inline-flex items-center justify-center'>
with {instructorString}
</Text>
<div className='flex gap-2 flex-content-center'>
{instructorString.length > 0 && (
<Text variant='h4' className='inline-flex items-center justify-center'>
with {instructorString}
</Text>
)}
<div className='flex-content-centr flex gap-1'>
{flags.map(flag => (
<Chip label={flagMap[flag]} />
<Chip key={flagMap[flag]} label={flagMap[flag]} />
))}
</div>
</div>
<div className='flex flex-col'>
{schedule.meetings.map(meeting => (
<Text variant='h4'>
{meeting.getDaysString({ format: 'long', separator: 'long' })}{' '}
{meeting.getTimeString({ separator: ' to ', capitalize: false })}
{meeting.location && (
<>
{` in `}
<Text variant='h4' className='text-ut-burntorange underline'>
{meeting.location.building}
</Text>
</>
)}
</Text>
))}
{schedule.meetings.map(meeting => {
const daysString = meeting.getDaysString({ format: 'long', separator: 'long' });
const timeString = meeting.getTimeString({ separator: ' to ', capitalize: false });
const locationString = meeting.location ? ` in ${meeting.location.building}` : '';
return (
<Text key={daysString + timeString + locationString} variant='h4'>
{daysString} {timeString}
{locationString}
</Text>
);
})}
</div>
</div>
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
<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}>
RateMyProf
</Button>
@@ -140,6 +168,7 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, a
</Button>
<Button
variant='filled'
disabled={course.status !== Status.OPEN}
color={!courseAdded ? 'ut-green' : 'ut-red'}
icon={!courseAdded ? Add : Remove}
onClick={handleAddOrRemoveCourse}