Squashed commit of the following:
commitc46e4a51c9Author: 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 commit36bcdd2522Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Mon Feb 19 21:15:41 2024 -0600 change grade distribution colors to match updated figma commit11a50df88dMerge:c16b301b4c96a9Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Mon Feb 19 17:57:13 2024 -0600 Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup commitc16b301ff0Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Mon Feb 19 17:47:21 2024 -0600 Kinda complete the handlers commit1ac1d9095aAuthor: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sun Feb 18 17:36:59 2024 -0600 Bunch of renaming commit925829ad41Author: 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 commitf2e5d51eb3Author: 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 commit747ee44440Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sun Feb 18 01:26:51 2024 -0600 Minor tweaks change style in header commitddfe952a32Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sun Feb 18 01:26:38 2024 -0600 Add Grade Distribution Stuff commitc27bf3c390Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sun Feb 18 01:26:13 2024 -0600 Modify story to use proper course info commit7afdbac1b8Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 16:37:01 2024 -0600 description stuff done commit1a89432276Author: 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 commit4c2b31e61aAuthor: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 15:23:01 2024 -0600 add todo for calendar button commit11b7a51dedAuthor: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 15:22:18 2024 -0600 add course button onclick handlers commitf2dfcec838Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 14:52:38 2024 -0600 some unocss updates commitf9f375514bAuthor: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 13:00:46 2024 -0600 Add rmp callback commit122fc6dbddAuthor: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 13:00:16 2024 -0600 Change test course to 314 commit19b124b3bdAuthor: 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. commit2eea01fc74Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 11:22:12 2024 -0600 use chip component in header commit9cb13c8fd1Merge:a62b7189392085Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 11:21:12 2024 -0600 Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup commita62b718c43Merge:43d26757b7b858Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Sat Feb 17 10:57:24 2024 -0600 Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup commit43d2675be5Author: 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 commit31bcef3099Merge:874f8d5fa1d737Author: 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 commit874f8d56cbAuthor: Abhinav Chadaga <abhinav.chadaga@utexas.edu> Date: Wed Feb 14 14:30:24 2024 -0600 some work
This commit is contained in:
@@ -16,6 +16,20 @@ export const colors = {
|
|||||||
red: '#af2e2d',
|
red: '#af2e2d',
|
||||||
black: '#1a2024',
|
black: '#1a2024',
|
||||||
},
|
},
|
||||||
|
gradeDistribution: {
|
||||||
|
a: '#22c55e',
|
||||||
|
aminus: '#a3e635',
|
||||||
|
bplus: '#84CC16',
|
||||||
|
b: '#FDE047',
|
||||||
|
bminus: '#FACC15',
|
||||||
|
cplus: '#F59E0B',
|
||||||
|
c: '#FB923C',
|
||||||
|
cminus: '#F97316',
|
||||||
|
dplus: '#EA580C', // TODO (achadaga): copilot generated, get actual color from Isaiah
|
||||||
|
d: '#DC2626',
|
||||||
|
dminus: '#B91C1C',
|
||||||
|
f: '#B91C1C',
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type NestedKeys<T> = {
|
type NestedKeys<T> = {
|
||||||
|
|||||||
69
src/stories/injected/CourseCatalogInjectedPopup.stories.ts
Normal file
69
src/stories/injected/CourseCatalogInjectedPopup.stories.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
|
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
|
||||||
|
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
|
||||||
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
|
||||||
|
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
|
||||||
|
const exampleCourse: Course = new Course({
|
||||||
|
uniqueId: 50805,
|
||||||
|
number: '314',
|
||||||
|
fullName: 'C S 314 DATA STRUCTURES',
|
||||||
|
courseName: 'DATA STRUCTURES',
|
||||||
|
department: 'C S',
|
||||||
|
creditHours: 3,
|
||||||
|
status: Status.OPEN,
|
||||||
|
instructors: [
|
||||||
|
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
|
||||||
|
],
|
||||||
|
isReserved: true,
|
||||||
|
description: [
|
||||||
|
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
|
||||||
|
'Computer Science 314 and 314H may not both be counted.',
|
||||||
|
'BVO 311C and 312H may not both be counted.',
|
||||||
|
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
|
||||||
|
'May be counted toward the Quantitative Reasoning flag requirement.',
|
||||||
|
],
|
||||||
|
schedule: new CourseSchedule({
|
||||||
|
meetings: [
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.T, DAY_MAP.TH],
|
||||||
|
startTime: 480,
|
||||||
|
endTime: 570,
|
||||||
|
location: { building: 'UTC', room: '123' },
|
||||||
|
}),
|
||||||
|
new CourseMeeting({
|
||||||
|
days: [DAY_MAP.TH],
|
||||||
|
startTime: 570,
|
||||||
|
endTime: 630,
|
||||||
|
location: { building: 'JES', room: '123' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
flags: ['Writing', 'Independent Inquiry'],
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const meta: Meta<typeof CourseCatalogInjectedPopup> = {
|
||||||
|
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
||||||
|
component: CourseCatalogInjectedPopup,
|
||||||
|
argTypes: {
|
||||||
|
onClose: { action: 'onClose' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof CourseCatalogInjectedPopup>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
course: exampleCourse,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Course, Status } from 'src/shared/types/Course';
|
import { Course, Status } from 'src/shared/types/Course';
|
||||||
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
|
||||||
import { UserSchedule } from 'src/shared/types/UserSchedule';
|
import { UserSchedule } from 'src/shared/types/UserSchedule';
|
||||||
import CoursePopup from 'src/views/components/injected/CoursePopup/CoursePopup';
|
import CoursePopup from 'src/views/components/injected/CoursePopupOld/CoursePopup';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import Instructor from 'src/shared/types/Instructor';
|
import Instructor from 'src/shared/types/Instructor';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { SiteSupport } from '../lib/getSiteSupport';
|
|||||||
import { populateSearchInputs } from '../lib/populateSearchInputs';
|
import { populateSearchInputs } from '../lib/populateSearchInputs';
|
||||||
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
|
||||||
import AutoLoad from './injected/AutoLoad/AutoLoad';
|
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 RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
|
||||||
import TableHead from './injected/TableHead';
|
import TableHead from './injected/TableHead';
|
||||||
import TableRow from './injected/TableRow/TableRow';
|
import TableRow from './injected/TableRow/TableRow';
|
||||||
|
|||||||
@@ -1,10 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Text from '../Text/Text';
|
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 {
|
interface Props {
|
||||||
label: string;
|
label: Flag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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®l=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;
|
||||||
Reference in New Issue
Block a user