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:
@@ -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 ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 {
|
/**
|
||||||
label: Flag;
|
* 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;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user