diff --git a/package-lock.json b/package-lock.json index ee6f6042..65e177d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "chrome-extension-toolkit": "^0.0.23", "classnames": "^2.3.2", "clean-webpack-plugin": "^4.0.0", + "highcharts": "^10.3.3", + "highcharts-react-official": "^3.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.57.1", @@ -8213,6 +8215,20 @@ "he": "bin/he" } }, + "node_modules/highcharts": { + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-10.3.3.tgz", + "integrity": "sha512-r7wgUPQI9tr3jFDn3XT36qsNwEIZYcfgz4mkKEA6E4nn5p86y+u1EZjazIG4TRkl5/gmGRtkBUiZW81g029RIw==" + }, + "node_modules/highcharts-react-official": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.0.tgz", + "integrity": "sha512-71IJZsLmEboYFjONpwC3NRsg6JKvtKYtS5Si3e6s6MLRSOFNOY8KILTkzvO36kjpeR/A0X3/kvvewE+GMPpkjw==", + "peerDependencies": { + "highcharts": ">=6.0.0", + "react": ">=16.8.0" + } + }, "node_modules/hook-std": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz", @@ -23388,6 +23404,17 @@ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true }, + "highcharts": { + "version": "10.3.3", + "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-10.3.3.tgz", + "integrity": "sha512-r7wgUPQI9tr3jFDn3XT36qsNwEIZYcfgz4mkKEA6E4nn5p86y+u1EZjazIG4TRkl5/gmGRtkBUiZW81g029RIw==" + }, + "highcharts-react-official": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.0.tgz", + "integrity": "sha512-71IJZsLmEboYFjONpwC3NRsg6JKvtKYtS5Si3e6s6MLRSOFNOY8KILTkzvO36kjpeR/A0X3/kvvewE+GMPpkjw==", + "requires": {} + }, "hook-std": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-2.0.0.tgz", diff --git a/package.json b/package.json index f9909f11..b1c021a3 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "chrome-extension-toolkit": "^0.0.23", "classnames": "^2.3.2", "clean-webpack-plugin": "^4.0.0", + "highcharts": "^10.3.3", + "highcharts-react-official": "^3.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.57.1", diff --git a/src/background/background.ts b/src/background/background.ts index f4e09b21..1d8b1b9d 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -9,6 +9,7 @@ import { SessionStore } from './storage/SessionStore'; import browserActionHandler from './handler/browserActionHandler'; import hotReloadingHandler from './handler/hotReloadingHandler'; import tabManagementHandler from './handler/tabManagementHandler'; +import courseDataHandler from './handler/courseDataHandler'; onServiceWorkerAlive(); @@ -34,6 +35,7 @@ const messageListener = new MessageListener({ ...browserActionHandler, ...hotReloadingHandler, ...tabManagementHandler, + ...courseDataHandler, }); messageListener.listen(); diff --git a/src/background/handler/courseDataHandler.ts b/src/background/handler/courseDataHandler.ts new file mode 100644 index 00000000..0df33563 --- /dev/null +++ b/src/background/handler/courseDataHandler.ts @@ -0,0 +1,14 @@ +import { MessageHandler } from 'chrome-extension-toolkit'; +import CourseDataMessages from 'src/shared/messages/CourseDataMessages'; + +const courseDataHandler: MessageHandler = { + getDistribution({ data, sendResponse }) { + const { course } = data; + + const dummyData = Array.from({ length: 18 }, () => Math.floor(Math.random() * 100)); + + sendResponse(dummyData); + }, +}; + +export default courseDataHandler; diff --git a/src/shared/messages/CourseDataMessages.ts b/src/shared/messages/CourseDataMessages.ts new file mode 100644 index 00000000..cccfa203 --- /dev/null +++ b/src/shared/messages/CourseDataMessages.ts @@ -0,0 +1,9 @@ +import { Course } from '../types/Course'; + +export default interface CourseDataMessages { + /** + * Gets the distribution of grades for the given course + * @returns an array of the number of students in each grade range from A+ to F, with the index of the array corresponding to the grade range + */ + getDistribution: (data: { course: Course }) => number[] | undefined; +} diff --git a/src/shared/messages/TabMessages.ts b/src/shared/messages/TabMessages.ts index 117f0c0c..8df07e2f 100644 --- a/src/shared/messages/TabMessages.ts +++ b/src/shared/messages/TabMessages.ts @@ -1,6 +1,4 @@ /** * This is a type with all the message definitions that can be sent TO specific tabs */ -export default interface TAB_MESSAGES { - reAnalyzePage: (data: { url: string }) => void; -} +export default interface TAB_MESSAGES {} diff --git a/src/shared/messages/index.ts b/src/shared/messages/index.ts index 59abb4df..8d669492 100644 --- a/src/shared/messages/index.ts +++ b/src/shared/messages/index.ts @@ -3,18 +3,21 @@ import TAB_MESSAGES from './TabMessages'; import BrowserActionMessages from './BrowserActionMessages'; import HotReloadingMessages from './HotReloadingMessages'; import TabManagementMessages from './TabManagementMessages'; +import CourseDataMessages from './CourseDataMessages'; /** * This is a type with all the message definitions that can be sent TO the background script */ -export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & HotReloadingMessages; +export type BACKGROUND_MESSAGES = BrowserActionMessages & + TabManagementMessages & + HotReloadingMessages & + CourseDataMessages; /** * A utility object that can be used to send type-safe messages to the background script */ export const bMessenger = createMessenger('background'); - /** * A utility object that can be used to send type-safe messages to specific tabs */ diff --git a/src/shared/types/Course.ts b/src/shared/types/Course.ts index 3741fdf9..a1c852c1 100644 --- a/src/shared/types/Course.ts +++ b/src/shared/types/Course.ts @@ -33,7 +33,6 @@ export type Semester = { /** * The internal representation of a course for the extension */ - export class Course { /** Every course has a uniqueId within UT's registrar system corresponding to each course section */ uniqueId: number; diff --git a/src/views/components/injected/CoursePopup/CoursePopup.tsx b/src/views/components/injected/CoursePopup/CoursePopup.tsx index 73d10bf4..7ef78657 100644 --- a/src/views/components/injected/CoursePopup/CoursePopup.tsx +++ b/src/views/components/injected/CoursePopup/CoursePopup.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { Course } from 'src/shared/types/Course'; +import Card from '../../common/Card/Card'; import Popup from '../../common/Popup/Popup'; 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; @@ -19,6 +21,7 @@ export default function CoursePopup({ course, onClose }: Props) { + ); } diff --git a/src/views/components/injected/CoursePopup/GradeDistribution/GradeDistribution.module.scss b/src/views/components/injected/CoursePopup/GradeDistribution/GradeDistribution.module.scss new file mode 100644 index 00000000..0ddf0a86 --- /dev/null +++ b/src/views/components/injected/CoursePopup/GradeDistribution/GradeDistribution.module.scss @@ -0,0 +1,14 @@ +.container { + max-width: 100%; + height: 250px; + margin: 20px; + padding: 12px; + + > div { + overflow: hidden; + min-width: auto; + max-width: 100%; + height: 250px; + margin: 0 auto; + } +} diff --git a/src/views/components/injected/CoursePopup/GradeDistribution/GradeDistribution.tsx b/src/views/components/injected/CoursePopup/GradeDistribution/GradeDistribution.tsx new file mode 100644 index 00000000..f69f08f7 --- /dev/null +++ b/src/views/components/injected/CoursePopup/GradeDistribution/GradeDistribution.tsx @@ -0,0 +1,116 @@ +import React, { useEffect, useState } from 'react'; +import HighchartsReact from 'highcharts-react-official'; +import Highcharts from 'highcharts'; +import Card from 'src/views/components/common/Card/Card'; +import { bMessenger } from 'src/shared/messages'; +import { Course } from 'src/shared/types/Course'; +import styles from './GradeDistribution.module.scss'; +import colors from 'src/views/styles/colors.module.scss'; + +enum DataStatus { + LOADING = 'LOADING', + DONE = 'DONE', + ERROR = 'ERROR', +} + +interface Props { + course: Course; +} + +export default function GradeDistribution({ course }: Props) { + const [chartOptions, setChartOptions] = useState({ + 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', + }, + }, + credits: { + enabled: false, + }, + tooltip: { + headerFormat: '{point.key}', + pointFormat: + '', + footerFormat: '
{point.y:.0f} Students
', + shared: true, + useHTML: true, + }, + plotOptions: { + bar: { + pointPadding: 0.2, + borderWidth: 0, + }, + series: { + animation: { + duration: 700, + }, + }, + }, + series: [ + { + type: 'column', + name: 'Grades', + data: [], + }, + ], + }); + const [status, setStatus] = useState(DataStatus.LOADING); + + useEffect(() => { + bMessenger.getDistribution({ course }).then(distribution => { + if (!distribution) { + return setStatus(DataStatus.ERROR); + } + + setChartOptions(options => ({ + ...options, + series: [ + { + type: 'column', + name: 'Grades', + data: distribution.map((y, i) => ({ + y, + // eslint-disable-next-line no-nested-ternary + // color: i < 8 ? '#2ECC71' : i < 10 ? '#F1C40F' : '#E74C3C', + // eslint-disable-next-line no-nested-ternary + color: i < 8 ? colors.cactus : i < 10 ? colors.sunshine : colors.speedway_brick, + })), + }, + ], + })); + }); + }, [course]); + + if (!chartOptions.series?.length) { + return null; + } + + return ( + + + + ); +}