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:
@@ -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®l=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}
|
||||
|
||||
Reference in New Issue
Block a user