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
parent d69707b8e8
commit 70a3f14e0a
19 changed files with 458 additions and 4 deletions

View File

@@ -0,0 +1,29 @@
@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;
}
}
}
}

View File

@@ -0,0 +1,91 @@
import { Course } from '@shared/types/Course';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
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 Card from '../../../common/Card/Card';
import styles from './CourseDescription.module.scss';
type Props = {
course: Course;
};
enum LoadStatus {
LOADING = 'LOADING',
DONE = 'DONE',
ERROR = 'ERROR',
}
/**
*
*/
export default function CourseDescription({ course }: Props) {
const [description, setDescription] = useState<string[]>([]);
const [status, setStatus] = useState<LoadStatus>(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;
}

View File

@@ -0,0 +1,18 @@
@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;
}
}

View File

@@ -0,0 +1,127 @@
import { background } from '@shared/messages';
import { Course } from '@shared/types/Course';
import { 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>
);
}

View File

@@ -0,0 +1,45 @@
@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;
}
}

View File

@@ -0,0 +1,133 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
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 { Button } from 'src/views/components/common/Button/Button';
import CourseButtons from './CourseButtons/CourseButtons';
import styles from './CourseHeader.module.scss';
import CopyIcon from '~icons/material-symbols/content-copy';
import CloseIcon from '~icons/material-symbols/close';
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='btn bg-transparent p-0'>
<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>
);
}

View File

@@ -0,0 +1,21 @@
.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;
}
}

View File

@@ -0,0 +1,27 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
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;
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>
);
}

View File

@@ -0,0 +1,48 @@
@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;
}
}
}

View File

@@ -0,0 +1,222 @@
/* eslint-disable no-nested-ternary */
import { Course, Semester } from '@shared/types/Course';
import { Distribution, LetterGrade } from '@shared/types/Distribution';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import React, { useEffect, useRef, useState } from 'react';
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 styles from './GradeDistribution.module.scss';
enum DataStatus {
LOADING = 'LOADING',
FOUND = 'FOUND',
NOT_FOUND = 'NOT_FOUND',
ERROR = 'ERROR',
}
interface Props {
course: Course;
}
const GRADE_COLORS: Record<LetterGrade, string> = {
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,
};
/**
* 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<DataStatus>(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>
);
}