feat: add core curriculum chips to injected popup (#372)

* feat: add core curriculum chips to injected popup

* fix: add VP and MAcore indicators

* feat: core now defined in constructor

* fix: make core and flags closer together

* fix: stop empty core chip from showing when no core requirements

* fix: remove optional chaining for core chips

* feat: generalize Chip for both flags and core classes

* fix: change types for storybook and add new story for CoreChip

* fix: remove labelMap prop from Chip, chore: clean up imports

* feat: change tooltip for core curriculum requirement

---------

Co-authored-by: Derek Chen <derex1987@gmail.com>
This commit is contained in:
suhas
2024-10-22 16:15:27 -05:00
committed by GitHub
parent 83d76f72da
commit 6f1afc5b25
10 changed files with 93 additions and 13 deletions

View File

@@ -79,6 +79,8 @@ export class Course {
scrapedAt!: number; scrapedAt!: number;
/** The colors of the course when displayed */ /** The colors of the course when displayed */
colors: CourseColors; colors: CourseColors;
/** The core curriculum requirements the course satisfies */
core: string[];
constructor(course: Serialized<Course>) { constructor(course: Serialized<Course>) {
Object.assign(this, course); Object.assign(this, course);
@@ -88,6 +90,7 @@ export class Course {
this.scrapedAt = Date.now(); this.scrapedAt = Date.now();
} }
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500); this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
this.core = course.core ?? [];
} }
/** /**

View File

@@ -16,8 +16,16 @@ export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
export const Default: Story = { export const FlagChip: Story = {
args: { args: {
label: 'QR', label: 'QR',
variant: 'flag',
},
};
export const CoreChip: Story = {
args: {
label: 'SB',
variant: 'core',
}, },
}; };

View File

@@ -17,6 +17,7 @@ export const ExampleCourse: Course = new Course({
'Taught as a Web-based course.', 'Taught as a Web-based course.',
], ],
flags: ['Quantitative Reasoning'], flags: ['Quantitative Reasoning'],
core: ['Natural Science and Technology, Part I'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB', fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online', instructionMode: 'Online',
instructors: [ instructors: [
@@ -60,6 +61,7 @@ export const ExampleCourse2: Course = new Course({
'May be counted toward the Independent Inquiry flag requirement.', 'May be counted toward the Independent Inquiry flag requirement.',
], ],
flags: ['Independent Inquiry'], flags: ['Independent Inquiry'],
core: ['Natural Science and Technology, Part II'],
fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS', fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
instructionMode: 'In Person', instructionMode: 'In Person',
instructors: [ instructors: [

View File

@@ -34,6 +34,7 @@ const generateCourses = (count: number): Course[] => {
'Taught as a Web-based course.', 'Taught as a Web-based course.',
], ],
flags: ['Quantitative Reasoning'], flags: ['Quantitative Reasoning'],
core: ['Natural Science and Technology, Part I'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB', fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online', instructionMode: 'Online',
instructors: [ instructors: [

View File

@@ -26,6 +26,7 @@ export const ExampleCourse: Course = new Course({
'Taught as a Web-based course.', 'Taught as a Web-based course.',
], ],
flags: ['Quantitative Reasoning'], flags: ['Quantitative Reasoning'],
core: ['Natural Science and Technology, Part I'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB', fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online', instructionMode: 'Online',
instructors: [ instructors: [

View File

@@ -11,6 +11,7 @@ const exampleGovCourse: Course = new Course({
department: 'GOV', department: 'GOV',
description: ['nah', 'aint typing this', 'corndog'], description: ['nah', 'aint typing this', 'corndog'],
flags: ['no flag for you >:)'], flags: ['no flag for you >:)'],
core: ['American and Texas Government'],
fullName: 'GOV 312L Something something', fullName: 'GOV 312L Something something',
instructionMode: 'Online', instructionMode: 'Online',
instructors: [ instructors: [
@@ -43,6 +44,7 @@ const examplePsyCourse: Course = new Course({
department: 'PSY', department: 'PSY',
description: ['nah', 'aint typing this', 'corndog'], description: ['nah', 'aint typing this', 'corndog'],
flags: ['no flag for you >:)'], flags: ['no flag for you >:)'],
core: ['Social and Behavioral Sciences'],
fullName: 'PSY 317L Yada yada', fullName: 'PSY 317L Yada yada',
instructionMode: 'Online', instructionMode: 'Online',
scrapedAt: Date.now(), scrapedAt: Date.now(),

View File

@@ -16,6 +16,7 @@ export const exampleCourse: Course = new Course({
'Taught as a Web-based course.', 'Taught as a Web-based course.',
], ],
flags: ['Quantitative Reasoning'], flags: ['Quantitative Reasoning'],
core: ['Natural Science and Technology, Part I'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB', fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online', instructionMode: 'Online',
scrapedAt: Date.now(), scrapedAt: Date.now(),
@@ -99,6 +100,7 @@ export const bevoCourse: Course = new Course({
}, },
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/', url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Independent Inquiry', 'Writing'], flags: ['Independent Inquiry', 'Writing'],
core: ['Humanities'],
instructionMode: 'In Person', instructionMode: 'In Person',
semester: { semester: {
code: '12345', code: '12345',
@@ -154,6 +156,7 @@ export const mikeScottCS314Course: Course = new Course({
}, },
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/50825/', url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/50825/',
flags: ['Writing', 'Independent Inquiry'], flags: ['Writing', 'Independent Inquiry'],
core: ['Natural Science and Technology, Part II'],
instructionMode: 'In Person', instructionMode: 'In Person',
semester: { semester: {
code: '12345', code: '12345',

View File

@@ -1,4 +1,5 @@
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
import clsx from 'clsx';
import React from 'react'; import React from 'react';
/** /**
@@ -14,26 +15,60 @@ export const flagMap = {
'Independent Inquiry': 'II', 'Independent Inquiry': 'II',
} as const satisfies Record<string, Flag>; } as const satisfies Record<string, Flag>;
interface Props { /**
* A type that represents the core curriculum aspects that a course can satisfy.
*/
export type Core = 'ID' | 'C1' | 'HU' | 'GO' | 'HI' | 'SB' | 'MA' | 'N1' | 'N2' | 'VP';
export const coreMap = {
'First-Year Signature Course': 'ID',
'English Composition': 'C1',
Humanities: 'HU',
'American and Texas Government': 'GO',
'U.S. History': 'HI',
'Social and Behavioral Sciences': 'SB',
'Natural Science and Technology, Part I': 'N1',
'Natural Science and Technology, Part II': 'N2',
Mathematics: 'MA',
'Visual and Performing Arts': 'VP',
} as const satisfies Record<string, Core>;
type Props =
| {
variant: 'core';
label: Core;
}
| {
variant: 'flag';
label: Flag; label: Flag;
} };
/** /**
* A reusable chip component that follows the design system of the extension. * A reusable chip component that follows the design system of the extension.
* @returns * @returns
*/ */
export function Chip({ label }: React.PropsWithChildren<Props>): JSX.Element { export function Chip({ variant, label }: React.PropsWithChildren<Props>): JSX.Element {
const longFlagName = Object.entries(flagMap).find(([full, short]) => short === label)?.[0] ?? label; let labelMap;
switch (variant) {
case 'core':
labelMap = coreMap;
break;
case 'flag':
labelMap = flagMap;
break;
default:
labelMap = {};
}
const longName = Object.entries(labelMap).find(([full, short]) => short === label)?.[0] ?? label;
return ( return (
<Text <Text
as='div' as='div'
variant='h4' variant='h4'
className='min-w-5 inline-flex items-center justify-center gap-2.5 rounded-lg px-1.5 py-0.5' className={clsx('min-w-5 inline-flex items-center justify-center gap-2.5 rounded-lg px-1.5 py-0.5', {
style={{ 'bg-ut-yellow text-black': variant === 'flag',
backgroundColor: '#FFD600', 'bg-ut-blue text-white': variant === 'core',
}} })}
title={`${longFlagName} flag`} title={variant === 'flag' ? `${longName} flag` : `${longName} core curriculum requirement`}
> >
{label} {label}
</Text> </Text>

View File

@@ -3,7 +3,7 @@ import type { Course } from '@shared/types/Course';
import type Instructor from '@shared/types/Instructor'; import type Instructor from '@shared/types/Instructor';
import type { UserSchedule } from '@shared/types/UserSchedule'; import type { UserSchedule } from '@shared/types/UserSchedule';
import { Button } from '@views/components/common/Button'; import { Button } from '@views/components/common/Button';
import { Chip, flagMap } from '@views/components/common/Chip'; import { Chip, coreMap, flagMap } from '@views/components/common/Chip';
import Divider from '@views/components/common/Divider'; import Divider from '@views/components/common/Divider';
import Link from '@views/components/common/Link'; import Link from '@views/components/common/Link';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
@@ -49,7 +49,7 @@ const capitalizeString = (str: string) => str.charAt(0).toUpperCase() + str.slic
* @returns {JSX.Element} The rendered component. * @returns {JSX.Element} The rendered component.
*/ */
export default function HeadingAndActions({ course, activeSchedule, onClose }: HeadingAndActionProps): JSX.Element { export default function HeadingAndActions({ course, activeSchedule, onClose }: HeadingAndActionProps): JSX.Element {
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course; const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule, core } = course;
const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId); const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId);
const formattedUniqueId = uniqueId.toString().padStart(5, '0'); const formattedUniqueId = uniqueId.toString().padStart(5, '0');
const isInCalendar = useCalendar(); const isInCalendar = useCalendar();
@@ -152,6 +152,14 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
<Chip <Chip
key={flagMap[flag as keyof typeof flagMap]} key={flagMap[flag as keyof typeof flagMap]}
label={flagMap[flag as keyof typeof flagMap]} label={flagMap[flag as keyof typeof flagMap]}
variant='flag'
/>
))}
{core.map((coreVal: string) => (
<Chip
key={coreMap[coreVal as keyof typeof coreMap]}
label={coreMap[coreVal as keyof typeof coreMap]}
variant='core'
/> />
))} ))}
</div> </div>

View File

@@ -19,6 +19,7 @@ const TableDataSelector = {
SCHEDULE_HOURS: 'td[data-th="Hour"]>span', SCHEDULE_HOURS: 'td[data-th="Hour"]>span',
SCHEDULE_LOCATION: 'td[data-th="Room"]>span', SCHEDULE_LOCATION: 'td[data-th="Room"]>span',
FLAGS: 'td[data-th="Flags"] ul li', FLAGS: 'td[data-th="Flags"] ul li',
CORE_CURRICULUM: 'td[data-th="Core"] ul li',
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
type TableDataSelectorType = (typeof TableDataSelector)[keyof typeof TableDataSelector]; type TableDataSelectorType = (typeof TableDataSelector)[keyof typeof TableDataSelector];
@@ -99,6 +100,7 @@ export class CourseCatalogScraper {
semester: this.getSemester(), semester: this.getSemester(),
scrapedAt: Date.now(), scrapedAt: Date.now(),
colors: getCourseColors('emerald', 500), colors: getCourseColors('emerald', 500),
core: this.getCore(row),
}); });
courses.push({ courses.push({
element: row, element: row,
@@ -337,6 +339,21 @@ export class CourseCatalogScraper {
return Array.from(lis).map(li => li.textContent || ''); return Array.from(lis).map(li => li.textContent || '');
} }
/**
* Get the list of core curriculum requirements the course satisfies
* @param row
* @returns an array of core curriculum codes
*/
getCore(row: HTMLTableRowElement): string[] {
const lis = row.querySelectorAll(TableDataSelector.CORE_CURRICULUM);
return (
Array.from(lis)
// ut schedule is weird and puts a blank core curriculum element even if there aren't any core requirements so filter those out
.filter(li => li.getAttribute('title') !== ' core curriculum requirement')
.map(li => li.textContent || '')
);
}
/** /**
* This will scrape all the time information from the course catalog table row and return it as a CourseSchedule object, which represents all of the meeting timiestimes/places of the course. * This will scrape all the time information from the course catalog table row and return it as a CourseSchedule object, which represents all of the meeting timiestimes/places of the course.
* @param row the row of the course catalog table * @param row the row of the course catalog table