feat: abhinavchadaga/course-catalog-popup (#128)
* 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 * fix: remove old CoursePopup component * fix: course catalog injected popup story should useScott's 314 class since it actually has data * fix: build error in background.ts: * chore: run eslint autofix on CourseCatalogInjectedPopup.stories.ts * chore: run prettier on CourseCatalogInjectedPopup.stories.ts
This commit is contained in:
@@ -1,111 +1,59 @@
|
|||||||
/* eslint-disable storybook/story-exports */
|
import type { Course } from '@shared/types/Course';
|
||||||
// import { UserSchedule } from '@shared/types/UserSchedule';
|
import { Status } from '@shared/types/Course';
|
||||||
// import type { Meta, StoryObj } from '@storybook/react';
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
// import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
|
|
||||||
// import { exampleCourse } from './mocked';
|
import { bevoCourse, bevoScheule, MikeScottCS314Course, MikeScottCS314Schedule } from './mocked';
|
||||||
|
|
||||||
// const exampleSchedule: UserSchedule = new UserSchedule({
|
const meta = {
|
||||||
// courses: [exampleCourse],
|
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
||||||
// name: 'Example Schedule',
|
component: CourseCatalogInjectedPopup,
|
||||||
// hours: 0,
|
args: {
|
||||||
// });
|
onClose: () => {},
|
||||||
// TODO (achadaga): import this after
|
},
|
||||||
// https://github.com/Longhorn-Developers/UT-Registration-Plus/pull/106 is merged
|
argTypes: {
|
||||||
// const bevoCourse: Course = new Course({
|
course: {
|
||||||
// uniqueId: 47280,
|
control: {
|
||||||
// number: '311C',
|
type: 'object',
|
||||||
// fullName: "BVO 311C BEVO'S SEMINAR LONGHORN CARE",
|
},
|
||||||
// courseName: "BEVO'S SEMINAR LONGHORN CARE",
|
},
|
||||||
// department: 'BVO',
|
activeSchedule: {
|
||||||
// creditHours: 3,
|
control: {
|
||||||
// status: Status.OPEN,
|
type: 'object',
|
||||||
// instructors: [new Instructor({ fullName: 'BEVO', firstName: '', lastName: 'BEVO', middleInitial: '' })],
|
},
|
||||||
// isReserved: false,
|
},
|
||||||
// description: [
|
onClose: {
|
||||||
// 'Restricted to Students in the School of Longhorn Enthusiasts',
|
control: {
|
||||||
// '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.',
|
type: 'function',
|
||||||
// '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.',
|
} satisfies Meta<typeof CourseCatalogInjectedPopup>;
|
||||||
// ],
|
|
||||||
// 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 = {
|
export default meta;
|
||||||
// title: 'Components/Injected/CourseCatalogInjectedPopup',
|
type Story = StoryObj<typeof meta>;
|
||||||
// component: CourseCatalogInjectedPopup,
|
|
||||||
// args: {
|
|
||||||
// course: exampleCourse,
|
|
||||||
// activeSchedule: exampleSchedule,
|
|
||||||
// onClose: () => {},
|
|
||||||
// },
|
|
||||||
// argTypes: {
|
|
||||||
// course: {
|
|
||||||
// control: {
|
|
||||||
// type: 'object',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// activeSchedule: {
|
|
||||||
// control: {
|
|
||||||
// type: 'object',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// onClose: {
|
|
||||||
// control: {
|
|
||||||
// type: 'function',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// } satisfies Meta<typeof CourseCatalogInjectedPopup>;
|
|
||||||
|
|
||||||
// export default meta;
|
export const OpenCourse: Story = {
|
||||||
// type Story = StoryObj<typeof meta>;
|
args: {
|
||||||
|
course: MikeScottCS314Course,
|
||||||
|
activeSchedule: MikeScottCS314Schedule,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// export const OpenCourse: Story = {
|
export const ClosedCourse: Story = {
|
||||||
// args: {
|
args: {
|
||||||
// course: exampleCourse,
|
course: {
|
||||||
// activeSchedule: exampleSchedule,
|
...MikeScottCS314Course,
|
||||||
// onClose: () => {},
|
status: Status.CLOSED,
|
||||||
// },
|
} as Course,
|
||||||
// };
|
activeSchedule: new UserSchedule({ courses: [], name: '', hours: 0 }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// export const ClosedCourse: Story = {
|
export const CourseWithNoData: Story = {
|
||||||
// args: {
|
args: {
|
||||||
// course: {
|
course: bevoCourse,
|
||||||
// ...exampleCourse,
|
activeSchedule: bevoScheule,
|
||||||
// status: Status.CLOSED,
|
},
|
||||||
// } satisfies Course,
|
};
|
||||||
// },
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export const CourseWithNoData: Story = {
|
|
||||||
// args: {
|
|
||||||
// course: bevoCourse,
|
|
||||||
// },
|
|
||||||
// };
|
|
||||||
export default {};
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { Course, Status } from '@shared/types/Course';
|
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
|
||||||
import CoursePopup from '@views/components/injected/CoursePopupOld/CoursePopup';
|
|
||||||
|
|
||||||
import { exampleCourse, exampleSchedule } from './mocked';
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'Components/Injected/CoursePopup',
|
|
||||||
component: CoursePopup,
|
|
||||||
// tags: ['autodocs'],
|
|
||||||
args: {
|
|
||||||
course: exampleCourse,
|
|
||||||
activeSchedule: exampleSchedule,
|
|
||||||
},
|
|
||||||
argTypes: {
|
|
||||||
course: {
|
|
||||||
control: {
|
|
||||||
type: 'other',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
activeSchedule: {
|
|
||||||
control: {
|
|
||||||
type: 'other',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
design: {
|
|
||||||
type: 'figma',
|
|
||||||
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=602-1879&mode=design&t=BoS5xBrpSsjgQXqv-11',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies Meta<typeof CoursePopup>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof meta>;
|
|
||||||
|
|
||||||
export const Open: Story = {
|
|
||||||
args: {
|
|
||||||
course: new Course({ ...exampleCourse, status: Status.OPEN }),
|
|
||||||
activeSchedule: new UserSchedule({
|
|
||||||
courses: [],
|
|
||||||
name: 'Example Schedule',
|
|
||||||
hours: 0,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Closed: Story = {
|
|
||||||
args: {
|
|
||||||
course: new Course({ ...exampleCourse, status: Status.CLOSED }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Cancelled: Story = {
|
|
||||||
args: {
|
|
||||||
course: new Course({ ...exampleCourse, status: Status.CANCELLED }),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Course, Status } from '@shared/types/Course';
|
import { Course, Status } from '@shared/types/Course';
|
||||||
import { CourseMeeting } from '@shared/types/CourseMeeting';
|
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
|
||||||
import Instructor from '@shared/types/Instructor';
|
import Instructor from '@shared/types/Instructor';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ export const exampleCourse: Course = new Course({
|
|||||||
season: 'Spring',
|
season: 'Spring',
|
||||||
year: 2024,
|
year: 2024,
|
||||||
},
|
},
|
||||||
status: Status.CANCELLED,
|
status: Status.OPEN,
|
||||||
uniqueId: 12345,
|
uniqueId: 12345,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
});
|
});
|
||||||
@@ -55,5 +55,106 @@ export const exampleCourse: Course = new Course({
|
|||||||
export const exampleSchedule: UserSchedule = new UserSchedule({
|
export const exampleSchedule: UserSchedule = new UserSchedule({
|
||||||
courses: [exampleCourse],
|
courses: [exampleCourse],
|
||||||
name: 'Example Schedule',
|
name: 'Example Schedule',
|
||||||
hours: 0,
|
hours: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
export 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: {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const bevoScheule: UserSchedule = new UserSchedule({
|
||||||
|
courses: [bevoCourse],
|
||||||
|
name: 'Bevo Schedule',
|
||||||
|
hours: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MikeScottCS314Course: 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: {
|
||||||
|
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/50825/',
|
||||||
|
flags: ['Writing', 'Independent Inquiry'],
|
||||||
|
instructionMode: 'In Person',
|
||||||
|
semester: {
|
||||||
|
code: '12345',
|
||||||
|
year: 2024,
|
||||||
|
season: 'Spring',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MikeScottCS314Schedule: UserSchedule = new UserSchedule({
|
||||||
|
courses: [MikeScottCS314Course],
|
||||||
|
name: 'Mike Scott CS314 Schedule',
|
||||||
|
hours: 3,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import HeadingAndActions from './HeadingAndActions';
|
|||||||
|
|
||||||
interface CourseCatalogInjectedPopupProps {
|
interface CourseCatalogInjectedPopupProps {
|
||||||
course: Course;
|
course: Course;
|
||||||
activeSchedule?: UserSchedule;
|
activeSchedule: UserSchedule;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,16 +27,12 @@ export default function CourseCatalogInjectedPopup({
|
|||||||
course,
|
course,
|
||||||
activeSchedule,
|
activeSchedule,
|
||||||
onClose,
|
onClose,
|
||||||
}: CourseCatalogInjectedPopupProps) {
|
}: CourseCatalogInjectedPopupProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
|
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
|
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
|
||||||
<Description
|
<Description course={course} />
|
||||||
course={
|
|
||||||
course
|
|
||||||
} /* lines={course.description} Looks like this was replaced. Description now set internally */
|
|
||||||
/>
|
|
||||||
<GradeDistribution course={course} />
|
<GradeDistribution course={course} />
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { Button } from '@views/components/common/Button/Button';
|
|||||||
import { Chip, flagMap } from '@views/components/common/Chip/Chip';
|
import { Chip, flagMap } from '@views/components/common/Chip/Chip';
|
||||||
import Divider from '@views/components/common/Divider/Divider';
|
import Divider from '@views/components/common/Divider/Divider';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
|
import React from 'react';
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import Add from '~icons/material-symbols/add';
|
import Add from '~icons/material-symbols/add';
|
||||||
import CalendarMonth from '~icons/material-symbols/calendar-month';
|
import CalendarMonth from '~icons/material-symbols/calendar-month';
|
||||||
@@ -25,7 +24,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
@use 'src/views/styles/colors.module.scss';
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 20px;
|
|
||||||
padding: 12px;
|
|
||||||
|
|
||||||
.description {
|
|
||||||
list-style-type: disc;
|
|
||||||
margin: 0px;
|
|
||||||
padding-left: 20px;
|
|
||||||
max-height: 200px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding: 0px 4px 4px;
|
|
||||||
.prerequisite {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.onlyOne {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.restriction {
|
|
||||||
color: colors.$speedway_brick;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
|
||||||
import Card from '@views/components/common/Card/Card';
|
|
||||||
import Spinner from '@views/components/common/Spinner/Spinner';
|
|
||||||
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 React, { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import styles from './CourseDescription.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
course: Course;
|
|
||||||
};
|
|
||||||
|
|
||||||
const LoadStatus = {
|
|
||||||
LOADING: 'LOADING',
|
|
||||||
DONE: 'DONE',
|
|
||||||
ERROR: 'ERROR',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type LoadStatusType = (typeof LoadStatus)[keyof typeof LoadStatus];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the course description component.
|
|
||||||
*
|
|
||||||
* @param {Props} props - The component props.
|
|
||||||
* @param {Course} props.course - The course object.
|
|
||||||
* @returns {JSX.Element} The rendered course description component.
|
|
||||||
*/
|
|
||||||
export default function CourseDescription({ course }: Props) {
|
|
||||||
const [description, setDescription] = useState<string[]>([]);
|
|
||||||
const [status, setStatus] = useState<LoadStatusType>(LoadStatus.LOADING);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchDescription(course)
|
|
||||||
.then(description => {
|
|
||||||
setStatus(LoadStatus.DONE);
|
|
||||||
setDescription(description);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setStatus(LoadStatus.ERROR);
|
|
||||||
});
|
|
||||||
}, [course]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={styles.container}>
|
|
||||||
{status === LoadStatus.ERROR && (
|
|
||||||
<Text color='speedway_brick' /* size='medium' weight='bold' align='center' */>
|
|
||||||
Please refresh the page and log back in using your UT EID and password
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{status === LoadStatus.LOADING && <Spinner className={styles.spinner} />}
|
|
||||||
{status === LoadStatus.DONE && (
|
|
||||||
<ul className={styles.description}>
|
|
||||||
{description.map(paragraph => (
|
|
||||||
<li key={paragraph}>
|
|
||||||
<DescriptionLine line={paragraph} />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LineProps {
|
|
||||||
line: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DescriptionLine({ line }: LineProps) {
|
|
||||||
const lowerCaseLine = line.toLowerCase();
|
|
||||||
|
|
||||||
const className = clsx({
|
|
||||||
[styles.prerequisite]: lowerCaseLine.includes('prerequisite'),
|
|
||||||
[styles.onlyOne]:
|
|
||||||
lowerCaseLine.includes('may be') || lowerCaseLine.includes('only one') || lowerCaseLine.includes('may not'),
|
|
||||||
[styles.restriction]: lowerCaseLine.includes('restrict'),
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Text className={className} /* size='medium' */>{line}</Text>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
@import 'src/views/styles/base.module.scss';
|
|
||||||
|
|
||||||
.container {
|
|
||||||
margin: 12px 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: none;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { background } from '@shared/messages';
|
|
||||||
import type { Course } from '@shared/types/Course';
|
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import { Button } from '@views/components/common/Button/Button';
|
|
||||||
import Card from '@views/components/common/Card/Card';
|
|
||||||
import Icon from '@views/components/common/Icon/Icon';
|
|
||||||
import Text from '@views/components/common/Text/Text';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import styles from './CourseButtons.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
activeSchedule?: UserSchedule;
|
|
||||||
course: Course;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { openNewTab, addCourse, removeCourse } = background;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component displays the buttons for the course info popup, that allow the user to either
|
|
||||||
* navigate to other pages that are useful for the course, or to do actions on the current course.
|
|
||||||
*/
|
|
||||||
export default function CourseButtons({ course, activeSchedule }: Props) {
|
|
||||||
const openRateMyProfessorURL = () => {
|
|
||||||
const primaryInstructor = course.instructors?.[0];
|
|
||||||
if (!primaryInstructor) return;
|
|
||||||
|
|
||||||
const name = primaryInstructor.toString({
|
|
||||||
format: 'first_last',
|
|
||||||
case: 'capitalize',
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = new URL('https://www.ratemyprofessors.com/search.jsp');
|
|
||||||
url.searchParams.append('queryBy', 'teacherName');
|
|
||||||
url.searchParams.append('schoolName', 'university of texas at austin');
|
|
||||||
url.searchParams.append('queryoption', 'HEADER');
|
|
||||||
url.searchParams.append('query', name);
|
|
||||||
url.searchParams.append('facetSearch', 'true');
|
|
||||||
|
|
||||||
openNewTab({ url: url.toString() });
|
|
||||||
};
|
|
||||||
|
|
||||||
const openSyllabiURL = () => {
|
|
||||||
const { department, number } = course;
|
|
||||||
|
|
||||||
const { firstName, lastName } = course.instructors?.[0] ?? {};
|
|
||||||
|
|
||||||
const url = new URL('https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/');
|
|
||||||
url.searchParams.append('department', department);
|
|
||||||
url.searchParams.append('course_number', number);
|
|
||||||
url.searchParams.append('instructor_first', firstName ?? '');
|
|
||||||
url.searchParams.append('instructor_last', lastName ?? '');
|
|
||||||
url.searchParams.append('course_type', 'In Residence');
|
|
||||||
url.searchParams.append('search', 'Search');
|
|
||||||
|
|
||||||
openNewTab({ url: url.toString() });
|
|
||||||
};
|
|
||||||
|
|
||||||
const openTextbookURL = () => {
|
|
||||||
const { department, number, semester, uniqueId } = course;
|
|
||||||
const url = new URL('https://www.universitycoop.com/adoption-search-results');
|
|
||||||
url.searchParams.append('sn', `${semester.code}__${department}__${number}__${uniqueId}`);
|
|
||||||
|
|
||||||
openNewTab({ url: url.toString() });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveCourse = async () => {
|
|
||||||
if (!activeSchedule) return;
|
|
||||||
addCourse({ course, scheduleName: activeSchedule.name });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveCourse = async () => {
|
|
||||||
if (!activeSchedule) return;
|
|
||||||
removeCourse({ course, scheduleName: activeSchedule.name });
|
|
||||||
};
|
|
||||||
|
|
||||||
const isCourseSaved = (() => {
|
|
||||||
if (!activeSchedule) return false;
|
|
||||||
return Boolean(activeSchedule.containsCourse(course));
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={styles.container}>
|
|
||||||
<Button
|
|
||||||
onClick={openRateMyProfessorURL}
|
|
||||||
disabled={!course.instructors.length}
|
|
||||||
variant='filled'
|
|
||||||
className={styles.button}
|
|
||||||
color='ut-black'
|
|
||||||
title='Search for this professor on RateMyProfessor'
|
|
||||||
>
|
|
||||||
<Text /* size='medium' weight='regular' */ color='white'>RateMyProf</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name='school' size='medium' />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={openSyllabiURL}
|
|
||||||
variant='filled'
|
|
||||||
className={styles.button}
|
|
||||||
color='ut-black'
|
|
||||||
title='Search for syllabi for this course'
|
|
||||||
>
|
|
||||||
<Text /* size='medium' weight='regular' */ color='white'>Syllabi</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name='grading' size='medium' />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={openTextbookURL}
|
|
||||||
variant='filled'
|
|
||||||
className={styles.button}
|
|
||||||
color='ut-black'
|
|
||||||
title='Search for textbooks for this course'
|
|
||||||
>
|
|
||||||
<Text /* size='medium' weight='regular' color='white' */>Textbook</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={!activeSchedule}
|
|
||||||
onClick={isCourseSaved ? handleRemoveCourse : handleSaveCourse}
|
|
||||||
title={isCourseSaved ? 'Remove this course from your schedule' : 'Add this course to your schedule'}
|
|
||||||
variant='filled'
|
|
||||||
className={styles.button}
|
|
||||||
color='ut-black'
|
|
||||||
>
|
|
||||||
<Text /* size='medium' weight='regular' color='white' */>{isCourseSaved ? 'Remove' : 'Add'}</Text>
|
|
||||||
<Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' />
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
@import 'src/views/styles/base.module.scss';
|
|
||||||
.header {
|
|
||||||
height: auto;
|
|
||||||
color: white;
|
|
||||||
padding: 12px;
|
|
||||||
margin: 20px;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.close {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
right: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-right: 40px;
|
|
||||||
|
|
||||||
.courseName {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
// underline
|
|
||||||
}
|
|
||||||
|
|
||||||
.uniqueId {
|
|
||||||
flex: 1;
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.instructors {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meeting {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import { Button } from '@views/components/common/Button/Button';
|
|
||||||
import Card from '@views/components/common/Card/Card';
|
|
||||||
import Icon from '@views/components/common/Icon/Icon';
|
|
||||||
import Link from '@views/components/common/Link/Link';
|
|
||||||
import Text from '@views/components/common/Text/Text';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import CloseIcon from '~icons/material-symbols/close';
|
|
||||||
import CopyIcon from '~icons/material-symbols/content-copy';
|
|
||||||
|
|
||||||
import CourseButtons from './CourseButtons/CourseButtons';
|
|
||||||
import styles from './CourseHeader.module.scss';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
course: Course;
|
|
||||||
activeSchedule?: UserSchedule;
|
|
||||||
onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component displays the header of the course info popup.
|
|
||||||
* It displays the course name, unique id, instructors, and schedule, all formatted nicely.
|
|
||||||
*/
|
|
||||||
export default function CourseHeader({ course, activeSchedule, onClose }: Props) {
|
|
||||||
// const getBuildingUrl = (building?: string): string | undefined => {
|
|
||||||
// if (!building) return undefined;
|
|
||||||
// return `https://utdirect.utexas.edu/apps/campus/buildings/nlogon/maps/UTM/${building}/`;
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='mx-6 my-5'>
|
|
||||||
<div className='flex items-center justify-start'>
|
|
||||||
<Text variant='h1' className='shrink truncate'>
|
|
||||||
{course.courseName}
|
|
||||||
</Text>
|
|
||||||
<Text variant='h1' className='ml-1 shrink-0'>
|
|
||||||
{`(${course.department} ${course.number})`}
|
|
||||||
</Text>
|
|
||||||
<div className='ml-auto min-w-fit flex shrink-0 gap-0'>
|
|
||||||
<Button icon={CopyIcon} variant='single' className='mr-1 px-2' color='ut-burntorange'>
|
|
||||||
{course.uniqueId}
|
|
||||||
</Button>
|
|
||||||
<button className='bg-transparent p-0 btn'>
|
|
||||||
<CloseIcon className='h-7 w-7' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Text variant='p'>
|
|
||||||
with{' '}
|
|
||||||
{course.instructors.map(instructor => (
|
|
||||||
<span className=''>{instructor.lastName}</span>
|
|
||||||
))}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
// <Card className={styles.header}>
|
|
||||||
// <Icon className={styles.close} /* size='large' */ name='close' onClick={onClose} />
|
|
||||||
// <div className={styles.title}>
|
|
||||||
// <Text className={styles.courseName} /* size='large' weight='bold' color='black' */>
|
|
||||||
// {course.courseName} ({course.department} {course.number}) blahhhhh
|
|
||||||
// </Text>
|
|
||||||
// <Link
|
|
||||||
// url={course.url}
|
|
||||||
// className={styles.uniqueId}
|
|
||||||
// /* size='medium'
|
|
||||||
// weight='semi_bold' */
|
|
||||||
// color='burnt_orange'
|
|
||||||
// title='View course details on UT Course Schedule'
|
|
||||||
// >
|
|
||||||
// #{course.uniqueId}
|
|
||||||
// </Link>
|
|
||||||
// </div>
|
|
||||||
// <Text /* size='medium' className={styles.instructors} */>
|
|
||||||
// {`with ${!course.instructors.length ? 'TBA' : ''}`}
|
|
||||||
// {course.instructors.map((instructor, index) => {
|
|
||||||
// const name = instructor.toString({
|
|
||||||
// format: 'first_last',
|
|
||||||
// case: 'capitalize',
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const url = instructor.getDirectoryUrl();
|
|
||||||
// const numInstructors = course.instructors.length;
|
|
||||||
// const isLast = course.instructors.length > 1 && index === course.instructors.length - 1;
|
|
||||||
// return (
|
|
||||||
// <span key={name}>
|
|
||||||
// {numInstructors > 1 && index === course.instructors.length - 1 ? '& ' : ''}
|
|
||||||
// <Link
|
|
||||||
// key={name}
|
|
||||||
// /* size='medium'
|
|
||||||
// weight='normal' */
|
|
||||||
// url={url}
|
|
||||||
// title="View instructor's directory page"
|
|
||||||
// >
|
|
||||||
// {name}
|
|
||||||
// </Link>
|
|
||||||
// {numInstructors > 2 && !isLast ? ', ' : ''}
|
|
||||||
// </span>
|
|
||||||
// );
|
|
||||||
// })}
|
|
||||||
// </Text>
|
|
||||||
// {course.schedule.meetings.map(meeting => (
|
|
||||||
// <Text /* size='medium' */ className={styles.meeting} key={meeting.startTime}>
|
|
||||||
// <Text as='span' /* size='medium' weight='bold' */ color='black'>
|
|
||||||
// {meeting.getDaysString({
|
|
||||||
// format: 'long',
|
|
||||||
// separator: 'short',
|
|
||||||
// })}
|
|
||||||
// </Text>
|
|
||||||
// {' at '}
|
|
||||||
// <Text as='span' /* size='medium' */>
|
|
||||||
// {meeting.getTimeString({
|
|
||||||
// separator: 'to',
|
|
||||||
// capitalize: true,
|
|
||||||
// })}
|
|
||||||
// </Text>
|
|
||||||
// {' in '}
|
|
||||||
// <Link
|
|
||||||
// /* size='medium'
|
|
||||||
// weight='normal' */
|
|
||||||
// title='View building on UT Map'
|
|
||||||
// url={getBuildingUrl(meeting.location?.building)}
|
|
||||||
// disabled={!meeting.location?.building}
|
|
||||||
// >
|
|
||||||
// {meeting.location?.building ?? 'TBA'}
|
|
||||||
// </Link>
|
|
||||||
// </Text>
|
|
||||||
// ))}
|
|
||||||
|
|
||||||
// <CourseButtons course={course} activeSchedule={activeSchedule} />
|
|
||||||
// </Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
.popup {
|
|
||||||
border-radius: 12px;
|
|
||||||
position: relative;
|
|
||||||
width: 55%;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 90%;
|
|
||||||
|
|
||||||
// fade in animation
|
|
||||||
animation: fadeIn 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
// fade in animation
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import Popup from '@views/components/common/Popup/Popup';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import CourseDescription from './CourseDescription/CourseDescription';
|
|
||||||
import CourseHeader from './CourseHeader/CourseHeader';
|
|
||||||
import styles from './CoursePopup.module.scss';
|
|
||||||
import GradeDistribution from './GradeDistribution/GradeDistribution';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
course: Course;
|
|
||||||
activeSchedule?: UserSchedule;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The popup that appears when the user clicks on a course for more details.
|
|
||||||
*/
|
|
||||||
export default function CoursePopup({ course, activeSchedule, onClose }: Props) {
|
|
||||||
return (
|
|
||||||
<Popup className={styles.popup} overlay onClose={onClose}>
|
|
||||||
<CourseHeader course={course} activeSchedule={activeSchedule} onClose={onClose} />
|
|
||||||
<CourseDescription course={course} />
|
|
||||||
<GradeDistribution course={course} />
|
|
||||||
</Popup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
@use 'src/views/styles/colors.module.scss';
|
|
||||||
@use 'src/views/styles/elevation.module.scss';
|
|
||||||
|
|
||||||
.chartContainer {
|
|
||||||
height: 250px;
|
|
||||||
margin: 20px;
|
|
||||||
padding: 12px;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.selectContainer {
|
|
||||||
display: flex;
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: -8px;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
select {
|
|
||||||
z-index: elevation.$MAX_Z_INDEX;
|
|
||||||
padding: 4px;
|
|
||||||
font-family: 'Inter';
|
|
||||||
border-radius: 8px;
|
|
||||||
border-color: colors.$charcoal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.highcharts-background) {
|
|
||||||
fill: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.textContainer {
|
|
||||||
margin: 20px;
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
padding: 12px;
|
|
||||||
box-shadow: none;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
// add some vertical padding to each element
|
|
||||||
> * {
|
|
||||||
margin: 0.2em 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import type { Course, Semester } from '@shared/types/Course';
|
|
||||||
import type { Distribution, LetterGrade } from '@shared/types/Distribution';
|
|
||||||
import Card from '@views/components/common/Card/Card';
|
|
||||||
import Icon from '@views/components/common/Icon/Icon';
|
|
||||||
import Spinner from '@views/components/common/Spinner/Spinner';
|
|
||||||
import Text from '@views/components/common/Text/Text';
|
|
||||||
import {
|
|
||||||
NoDataError,
|
|
||||||
queryAggregateDistribution,
|
|
||||||
querySemesterDistribution,
|
|
||||||
} from '@views/lib/database/queryDistribution';
|
|
||||||
import colors from '@views/styles/colors.module.scss';
|
|
||||||
import Highcharts from 'highcharts';
|
|
||||||
import HighchartsReact from 'highcharts-react-official';
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import styles from './GradeDistribution.module.scss';
|
|
||||||
|
|
||||||
const DataStatus = {
|
|
||||||
LOADING: 'LOADING',
|
|
||||||
FOUND: 'FOUND',
|
|
||||||
NOT_FOUND: 'NOT_FOUND',
|
|
||||||
ERROR: 'ERROR',
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
type DataStatusType = (typeof DataStatus)[keyof typeof DataStatus];
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
course: Course;
|
|
||||||
}
|
|
||||||
|
|
||||||
const GRADE_COLORS = {
|
|
||||||
A: colors.turtle_pond,
|
|
||||||
'A-': colors.turtle_pond,
|
|
||||||
'B+': colors.cactus,
|
|
||||||
B: colors.cactus,
|
|
||||||
'B-': colors.cactus,
|
|
||||||
'C+': colors.sunshine,
|
|
||||||
C: colors.sunshine,
|
|
||||||
'C-': colors.sunshine,
|
|
||||||
'D+': colors.tangerine,
|
|
||||||
D: colors.tangerine,
|
|
||||||
'D-': colors.tangerine,
|
|
||||||
F: colors.speedway_brick,
|
|
||||||
} as const satisfies Record<LetterGrade, string>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A chart to fetch and display the grade distribution for a course
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export default function GradeDistribution({ course }: Props) {
|
|
||||||
const ref = useRef<HighchartsReact.RefObject>(null);
|
|
||||||
const [semesters, setSemesters] = useState<Semester[]>([]);
|
|
||||||
const [selectedSemester, setSelectedSemester] = useState<Semester | null>(null);
|
|
||||||
const [distribution, setDistribution] = useState<Distribution | null>(null);
|
|
||||||
const [status, setStatus] = useState<DataStatusType>(DataStatus.LOADING);
|
|
||||||
|
|
||||||
const [chartOptions, setChartOptions] = useState<Highcharts.Options>({
|
|
||||||
title: {
|
|
||||||
text: undefined,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
text: undefined,
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
xAxis: {
|
|
||||||
title: {
|
|
||||||
text: 'Grades',
|
|
||||||
},
|
|
||||||
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'],
|
|
||||||
crosshair: true,
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
min: 0,
|
|
||||||
title: {
|
|
||||||
text: 'Students',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
chart: {
|
|
||||||
style: {
|
|
||||||
fontFamily: 'Inter',
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
spacingBottom: 25,
|
|
||||||
spacingTop: 25,
|
|
||||||
height: 250,
|
|
||||||
},
|
|
||||||
credits: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
accessibility: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
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: Array.from({ length: 12 }, () => 0),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateChart = (distribution: Distribution) => {
|
|
||||||
setChartOptions(options => ({
|
|
||||||
...options,
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
type: 'column',
|
|
||||||
name: 'Grades',
|
|
||||||
data: Object.entries(distribution).map(([grade, count]) => ({
|
|
||||||
y: count,
|
|
||||||
color: GRADE_COLORS[grade as LetterGrade],
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
window.dispatchEvent(new Event('resize'));
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
queryAggregateDistribution(course)
|
|
||||||
.then(([distribution, semesters]) => {
|
|
||||||
setSemesters(semesters);
|
|
||||||
updateChart(distribution);
|
|
||||||
setStatus(DataStatus.FOUND);
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
if (err instanceof NoDataError) {
|
|
||||||
return setStatus(DataStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
return setStatus(DataStatus.ERROR);
|
|
||||||
});
|
|
||||||
}, [course]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
(async () => {
|
|
||||||
let distribution: Distribution;
|
|
||||||
if (selectedSemester) {
|
|
||||||
distribution = await querySemesterDistribution(course, selectedSemester);
|
|
||||||
} else {
|
|
||||||
[distribution] = await queryAggregateDistribution(course);
|
|
||||||
}
|
|
||||||
updateChart(distribution);
|
|
||||||
setStatus(DataStatus.FOUND);
|
|
||||||
})().catch(err => {
|
|
||||||
if (err instanceof NoDataError) {
|
|
||||||
return setStatus(DataStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
return setStatus(DataStatus.ERROR);
|
|
||||||
});
|
|
||||||
}, [selectedSemester, course]);
|
|
||||||
|
|
||||||
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
const index = parseInt(event.target.value, 10);
|
|
||||||
if (index === 0) {
|
|
||||||
setSelectedSemester(null);
|
|
||||||
} else {
|
|
||||||
setSelectedSemester(semesters[index - 1]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === DataStatus.FOUND) {
|
|
||||||
return (
|
|
||||||
<Card className={styles.chartContainer}>
|
|
||||||
{semesters.length > 0 && (
|
|
||||||
<div className={styles.selectContainer}>
|
|
||||||
<select onChange={handleSelectSemester}>
|
|
||||||
<option value={0}>Aggregate</option>
|
|
||||||
{semesters.map((semester, index) => (
|
|
||||||
<option key={semester.season + semester.year} value={index + 1}>
|
|
||||||
{semester.season} {semester.year}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className={styles.textContainer}>
|
|
||||||
{status === DataStatus.LOADING && <Spinner />}
|
|
||||||
{status === DataStatus.ERROR && (
|
|
||||||
<Card className={styles.text}>
|
|
||||||
<Text color='speedway_brick' /* size='medium' weight='semi_bold' */>
|
|
||||||
There was an error fetching the grade distribution data
|
|
||||||
</Text>
|
|
||||||
<Icon color='speedway_brick' /* size='large' */ name='sentiment_dissatisfied' />
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
{status === DataStatus.NOT_FOUND && (
|
|
||||||
<Card className={styles.text}>
|
|
||||||
<Text color='charcoal' /* size='medium' weight='semi_bold' */>
|
|
||||||
No grade distribution data was found for this course
|
|
||||||
</Text>
|
|
||||||
<Icon color='charcoal' /* size='x_large' */ name='search_off' />
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user