Squashed commit of the following:

commit c46e4a51c9
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 21:37:46 2024 -0600

    change from reducer pattern to state variables, remove chartData from state

commit 36bcdd2522
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 21:15:41 2024 -0600

    change grade distribution colors to match updated figma

commit 11a50df88d
Merge: c16b301 b4c96a9
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 17:57:13 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit c16b301ff0
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 17:47:21 2024 -0600

    Kinda complete the handlers

commit 1ac1d9095a
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:36:59 2024 -0600

    Bunch of renaming

commit 925829ad41
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:24:53 2024 -0600

    Fix syllabi url

    Remove unused variable and unnecessary args to url

commit f2e5d51eb3
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:24:22 2024 -0600

    Add TODO

    replace current grade colors with a tailwind palette

commit 747ee44440
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:51 2024 -0600

    Minor tweaks

    change style in header

commit ddfe952a32
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:38 2024 -0600

    Add Grade Distribution Stuff

commit c27bf3c390
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:13 2024 -0600

    Modify story to use proper course info

commit 7afdbac1b8
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 16:37:01 2024 -0600

    description stuff done

commit 1a89432276
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:26:32 2024 -0600

    Rename CoursePopup

    Old one to "Old", remove "2" from new one

commit 4c2b31e61a
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:23:01 2024 -0600

    add todo for calendar button

commit 11b7a51ded
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:22:18 2024 -0600

    add course button onclick handlers

commit f2dfcec838
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 14:52:38 2024 -0600

    some unocss updates

commit f9f375514b
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 13:00:46 2024 -0600

    Add rmp callback

commit 122fc6dbdd
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 13:00:16 2024 -0600

    Change test course to 314

commit 19b124b3bd
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 12:19:21 2024 -0600

    complete CourseHeaderAndActions Component

    added course buttons, using proper subcomponents now.

commit 2eea01fc74
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 11:22:12 2024 -0600

    use chip component in header

commit 9cb13c8fd1
Merge: a62b718 9392085
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 11:21:12 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit a62b718c43
Merge: 43d2675 7b7b858
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 10:57:24 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit 43d2675be5
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 10:54:49 2024 -0600

    some work on course popup

    update the stories and create the header component

commit 31bcef3099
Merge: 874f8d5 fa1d737
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Wed Feb 14 14:33:16 2024 -0600

    Merge branch 'main' into abhinavchadaga/course-catalog-popup

    pulling from main

commit 874f8d56cb
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Wed Feb 14 14:30:24 2024 -0600

    some work
This commit is contained in:
knownotunknown
2024-02-19 22:39:26 -06:00
committed by doprz
parent e0212d5109
commit c4a738f281
19 changed files with 458 additions and 4 deletions

View File

@@ -8,7 +8,7 @@ import { SiteSupport } from '../lib/getSiteSupport';
import { populateSearchInputs } from '../lib/populateSearchInputs';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
import AutoLoad from './injected/AutoLoad/AutoLoad';
import CoursePopup from './injected/CoursePopup/CoursePopup';
import CoursePopup from './injected/CoursePopupOld/CoursePopup';
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
import TableHead from './injected/TableHead';
import TableRow from './injected/TableRow/TableRow';

View File

@@ -1,10 +1,21 @@
import React from 'react';
import Text from '../Text/Text';
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];
/**
* A type that represents the flags that a course can have.
*/
export type Flag = 'WR' | 'QR' | 'GC' | 'CD' | 'E' | 'II';
export const flagMap: Record<string, Flag> = {
Writing: 'WR',
'Quantitative Reasoning': 'QR',
'Global Cultures': 'GC',
'Cultural Diversity in the United States': 'CD',
Ethics: 'E',
'Independent Inquiry': 'II',
};
interface Props {
label: string;
label: Flag;
}
/**

View File

@@ -0,0 +1,24 @@
import Popup from '@views/components/common/Popup/Popup';
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import Description from './Description';
import GradeDistribution from './GradeDistribution';
import HeadingAndActions from './HeadingAndActions';
interface CourseCatalogInjectedPopupProps {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
}
const CourseCatalogInjectedPopup: React.FC<CourseCatalogInjectedPopupProps> = ({ course, activeSchedule, onClose }) => (
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
<div className='flex flex-col'>
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
<Description lines={course.description} />
<GradeDistribution course={course} />
</div>
</Popup>
);
export default CourseCatalogInjectedPopup;

View File

@@ -0,0 +1,30 @@
import clsx from 'clsx';
import React from 'react';
import Text from '../../common/Text/Text';
interface DescriptionProps {
lines: string[];
}
const Description: React.FC<DescriptionProps> = ({ lines }: DescriptionProps) => {
const keywords = ['prerequisite', 'restricted'];
return (
<ul className='my-[5px] space-y-1.5 children:marker:text-ut-burntorange'>
{lines.map(line => {
const isKeywordPresent = keywords.some(keyword => line.toLowerCase().includes(keyword));
return (
<div className='flex gap-2'>
<span className='text-ut-burntorange'></span>
<li key={line}>
<Text variant='p' className={clsx({ 'font-bold text-ut-burntorange': isKeywordPresent })}>
{line}
</Text>
</li>
</div>
);
})}
</ul>
);
};
export default Description;

View File

@@ -0,0 +1,170 @@
import Spinner from '@views/components/common/Spinner/Spinner';
import Text from '@views/components/common/Text/Text';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { Distribution, LetterGrade } from 'src/shared/types/Distribution';
import { colors } from 'src/shared/util/themeColors';
import {
NoDataError,
queryAggregateDistribution,
querySemesterDistribution,
} from 'src/views/lib/database/queryDistribution';
interface GradeDistributionProps {
course: Course;
}
enum DataStatus {
LOADING = 'LOADING',
FOUND = 'FOUND',
NOT_FOUND = 'NOT_FOUND',
ERROR = 'ERROR',
}
const GRADE_COLORS: Record<LetterGrade, string> = {
A: colors.gradeDistribution.a,
'A-': colors.gradeDistribution.aminus,
'B+': colors.gradeDistribution.bplus,
B: colors.gradeDistribution.b,
'B-': colors.gradeDistribution.bminus,
'C+': colors.gradeDistribution.cplus,
C: colors.gradeDistribution.c,
'C-': colors.gradeDistribution.cminus,
'D+': colors.gradeDistribution.dplus,
D: colors.gradeDistribution.d,
'D-': colors.gradeDistribution.dminus,
F: colors.gradeDistribution.f,
};
const GradeDistribution: React.FC<GradeDistributionProps> = ({ course }) => {
const [semester, setSemester] = React.useState('Aggregate');
const [distributions, setDistributions] = React.useState<Record<string, Distribution>>({});
const [status, setStatus] = React.useState(DataStatus.LOADING);
const ref = React.useRef<HighchartsReact.RefObject>(null);
const chartData = React.useMemo(() => {
if (status === DataStatus.FOUND && distributions[semester]) {
return Object.entries(distributions[semester]).map(([grade, count]) => ({
y: count,
color: GRADE_COLORS[grade as LetterGrade],
}));
}
return [];
}, [distributions, semester, status]);
React.useEffect(() => {
const fetchInitialData = async () => {
try {
const [aggregateDist, semesters] = await queryAggregateDistribution(course);
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
const semesterDistributions = await Promise.all(semesterPromises);
semesters.forEach((semester, i) => {
initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
});
setDistributions(initialDistributions);
setStatus(DataStatus.FOUND);
} catch (e) {
console.error(e);
if (e instanceof NoDataError) {
setStatus(DataStatus.NOT_FOUND);
} else {
setStatus(DataStatus.ERROR);
}
}
};
fetchInitialData();
}, [course]);
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSemester(event.target.value);
};
const chartOptions: Highcharts.Options = {
title: { text: undefined },
subtitle: { text: undefined },
legend: { enabled: false },
xAxis: {
title: { text: 'Grade' },
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'],
crosshair: true,
},
yAxis: {
min: 0,
title: { text: 'Number of Students' },
},
chart: {
style: { fontFamily: 'Roboto Flex', fontWeight: '600' },
spacingBottom: 25,
spacingTop: 25,
height: 250,
},
credits: { enabled: false },
accessibility: { enabled: true },
tooltip: {
headerFormat: '<span style="font-size:small; font-weight:bold">{point.key}</span><table>',
pointFormat:
'<td style="color:{black};padding:0;font-size:small; font-weight:bold;"><b>{point.y:.0f} Students</b></td>',
footerFormat: '</table>',
shared: true,
useHTML: true,
},
plotOptions: {
bar: { pointPadding: 0.2, borderWidth: 0 },
series: { animation: { duration: 700 } },
},
series: [
{
type: 'column',
name: 'Grades',
data: chartData,
},
],
};
return (
<div className='pb-[25px] pt-[12px]'>
{status === DataStatus.LOADING && <Spinner />}
{status === DataStatus.NOT_FOUND && <Text variant='p'>No grade distribution data found</Text>}
{status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
{status === DataStatus.FOUND && (
<>
<div className='w-full flex items-center justify-center gap-[12px]'>
<Text variant='p'>Grade distribution for {`${course.department} ${course.number}`}</Text>
<select
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
onChange={handleSelectSemester}
>
{Object.keys(distributions)
.sort((k1, k2) => {
if (k1 === 'Aggregate') {
return -1;
}
if (k2 === 'Aggregate') {
return 1;
}
const [season1, year1] = k1.split(' ');
const [, year2] = k2.split(' ');
if (year1 !== year2) {
return parseInt(year2, 10) - parseInt(year1, 10);
}
return season1 === 'Fall' ? 1 : -1;
})
.map(semester => (
<option key={semester} value={semester}>
{semester}
</option>
))}
</select>
</div>
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
</>
)}
</div>
);
};
export default GradeDistribution;

View File

@@ -0,0 +1,136 @@
import { Button } from '@views/components/common/Button/Button';
import { Chip, flagMap } from '@views/components/common/Chip/Chip';
import Divider from '@views/components/common/Divider/Divider';
import Text from '@views/components/common/Text/Text';
import React from 'react';
import addCourse from 'src/pages/background/lib/addCourse';
import openNewTab from 'src/pages/background/util/openNewTab';
import { Course } from 'src/shared/types/Course';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import Add from '~icons/material-symbols/add';
import CalendarMonth from '~icons/material-symbols/calendar-month';
import CloseIcon from '~icons/material-symbols/close';
import Copy from '~icons/material-symbols/content-copy';
import Description from '~icons/material-symbols/description';
import Mood from '~icons/material-symbols/mood';
import Reviews from '~icons/material-symbols/reviews';
interface HeadingAndActionProps {
/* The course to display */
course: Course;
/* The active schedule */
activeSchedule: UserSchedule;
/* The function to call when the popup should be closed */
onClose: () => void;
}
/**
* Renders the heading component for the CoursePopup component.
*
* @param {HeadingAndActionProps} props - The component props.
* @returns {JSX.Element} The rendered component.
*/
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
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 handleOpenCalendar = async () => {
const url = chrome.runtime.getURL('calendar.html');
await openNewTab(url);
};
const handleOpenRateMyProf = async () => {
const openTabs = instructors.map(instructor => {
const { fullName } = instructor;
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${fullName}`;
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 openNewTab(cisUrl);
};
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 openNewTab(url);
};
const handleAddCourse = async () => {
await addCourse(activeSchedule.name, course);
};
return (
<div className='w-full pb-3 pt-6'>
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1'>
<Text variant='h1' className='truncate'>
{courseName}
</Text>
<Text variant='h1' className='flex-1 whitespace-nowrap'>
{' '}
({department} {courseNumber})
</Text>
<Button color='ut-burntorange' variant='single' icon={Copy} onClick={handleCopy}>
{uniqueId}
</Button>
<button className='btn bg-transparent p-0' onClick={onClose}>
<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-content-centr flex gap-1'>
{flags.map(flag => (
<Chip 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>
))}
</div>
</div>
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
<Divider type='solid' color='ut-offwhite' className='h-7' />
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
RateMyProf
</Button>
<Button variant='outline' color='ut-teal' icon={Mood} onClick={handleOpenCES}>
CES
</Button>
<Button variant='outline' color='ut-orange' icon={Description} onClick={handleOpenPastSyllabi}>
Past Syllabi
</Button>
<Button variant='filled' color='ut-green' icon={Add} onClick={handleAddCourse}>
Add Course
</Button>
</div>
<Divider />
</div>
);
};
export default HeadingAndActions;