Merge branch 'main' into abhinavchadaga/course-catalog-popup

pulling from main
This commit is contained in:
Abhinav Chadaga
2024-02-14 14:33:16 -06:00
24 changed files with 583 additions and 140 deletions

26
.github/workflows/chromatic.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: "Chromatic"
on: [push, pull_request_target]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 8
- name: Install dependencies
run: pnpm install
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
autoAcceptChanges: "main"

View File

@@ -20,7 +20,7 @@
"@types/sql.js": "^1.4.9",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.51",
"classnames": "^2.5.1",
"clsx": "^2.1.0",
"highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1",
"react": "^18.2.0",
@@ -55,9 +55,11 @@
"@unocss/postcss": "^0.58.4",
"@unocss/preset-uno": "^0.58.4",
"@unocss/preset-web-fonts": "^0.58.4",
"@unocss/reset": "^0.58.5",
"@unocss/transformer-directives": "^0.58.4",
"@unocss/transformer-variant-group": "^0.58.4",
"@vitejs/plugin-react-swc": "^3.6.0",
"chromatic": "^10.9.1",
"cssnano": "^6.0.3",
"cssnano-preset-advanced": "^6.0.3",
"dotenv": "^16.4.1",

38
pnpm-lock.yaml generated
View File

@@ -22,9 +22,9 @@ dependencies:
chrome-extension-toolkit:
specifier: ^0.0.51
version: 0.0.51
classnames:
specifier: ^2.5.1
version: 2.5.1
clsx:
specifier: ^2.1.0
version: 2.1.0
highcharts:
specifier: ^11.3.0
version: 11.3.0
@@ -123,6 +123,9 @@ devDependencies:
'@unocss/preset-web-fonts':
specifier: ^0.58.4
version: 0.58.4
'@unocss/reset':
specifier: ^0.58.5
version: 0.58.5
'@unocss/transformer-directives':
specifier: ^0.58.4
version: 0.58.4
@@ -132,6 +135,9 @@ devDependencies:
'@vitejs/plugin-react-swc':
specifier: ^3.6.0
version: 3.6.0(vite@5.0.12)
chromatic:
specifier: ^10.9.1
version: 10.9.1
cssnano:
specifier: ^6.0.3
version: 6.0.3(postcss@8.4.33)
@@ -5046,6 +5052,10 @@ packages:
resolution: {integrity: sha512-ZZTrAdl4WWmMjQdOqcOSWdgFH6kdFKZjPu4c6Ijxk7KvY2BW3nttTTBa7IYeuXFHVfcExUFqlOgRurt+NeWYyQ==}
dev: true
/@unocss/reset@0.58.5:
resolution: {integrity: sha512-2wMrkCj3SSb5hrx9TKs5jZa34QIRkHv9FotbJutAPo7o8hx+XXn56ogzdoUrcFPJZJUx2R2nyOVbSlGMIjtFtw==}
dev: true
/@unocss/rule-utils@0.58.4:
resolution: {integrity: sha512-52Jp4I+joGTaDm7ehB/7uZ2kJL+9BZcYRDUVk4IDacDH5W9yxf1F75LzYT8jJVWXD/HIhiS0r9V6qhcBq2OWZw==}
engines: {node: '>=14'}
@@ -6077,6 +6087,19 @@ packages:
engines: {node: '>=10'}
dev: true
/chromatic@10.9.1:
resolution: {integrity: sha512-6Ypho74fQu45ns48KaBuIMKqlzik0fJo//dLB3lkiphz8vCiMm75uU3xhkfZh4NbOS51MbWZKjPfefM53bIHpg==}
hasBin: true
peerDependencies:
'@chromatic-com/cypress': ^0.5.2 || ^1.0.0
'@chromatic-com/playwright': ^0.5.2 || ^1.0.0
peerDependenciesMeta:
'@chromatic-com/cypress':
optional: true
'@chromatic-com/playwright':
optional: true
dev: true
/chrome-extension-toolkit@0.0.51:
resolution: {integrity: sha512-XzOOE2+/aYG43bJOwuJT4oWcn80jBJr5mwGyrSzKKFoqALixT15AsPcfZId/UOoc4pIavu2XcHeJga6ng0m1jQ==}
dependencies:
@@ -6104,10 +6127,6 @@ packages:
consola: 3.2.3
dev: true
/classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
dev: false
/clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@@ -6159,6 +6178,11 @@ packages:
engines: {node: '>=0.8'}
dev: true
/clsx@2.1.0:
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
engines: {node: '>=6'}
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:

50
src/shared/util/colors.ts Normal file
View File

@@ -0,0 +1,50 @@
import { theme } from 'unocss/preset-mini';
export interface CourseColors {
primaryColor: string;
secondaryColor: string;
}
// calculates luminance of a hex string
function getLuminance(hex: string): number {
let r = parseInt(hex.substring(1, 3), 16);
let g = parseInt(hex.substring(3, 5), 16);
let b = parseInt(hex.substring(5, 7), 16);
[r, g, b] = [r, g, b].map(color => {
let c = color / 255;
c = c > 0.03928 ? ((c + 0.055) / 1.055) ** 2.4 : (c /= 12.92);
return c;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
// calculates contrast ratio between two hex strings
function contrastRatioPair(hex1: string, hex2: string) {
const lum1 = getLuminance(hex1);
const lum2 = getLuminance(hex2);
return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05);
}
/**
* Generate a tailwind classname for the font color based on the background color
* @param bgColor the tailwind classname for background ex. "bg-emerald-500"
*/
export function pickFontColor(bgColor: string): 'text-white' | 'text-black' {
return contrastRatioPair(bgColor, '#606060') > contrastRatioPair(bgColor, '#ffffff') ? 'text-black' : 'text-white';
}
/**
* Get primary and secondary colors from a tailwind colorway
* @param colorway the tailwind colorway ex. "emerald"
*/
export function getCourseColors(colorway: keyof typeof theme.colors): CourseColors {
return {
primaryColor: theme.colors[colorway][600] as string,
secondaryColor: theme.colors[colorway][800] as string,
};
}

24
src/shared/util/icons.tsx Normal file
View File

@@ -0,0 +1,24 @@
import React, { SVGProps } from 'react';
import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse';
import CancelledIcon from '~icons/material-symbols/warning';
import { Status } from '../types/Course';
/**
* Get Icon component based on status
* @param props.status status
* @returns React.ReactElement - the icon component
*/
export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: Status }): React.ReactElement {
const { status, ...rest } = props;
switch (props.status) {
case Status.WAITLISTED:
return <WaitlistIcon {...rest} />;
case Status.CLOSED:
return <ClosedIcon {...rest} />;
case Status.CANCELLED:
return <CancelledIcon {...rest} />;
default:
}
}

View File

@@ -0,0 +1,48 @@
import React from 'react';
import { Status } from '@shared/types/Course';
import { Meta, StoryObj } from '@storybook/react';
import CourseStatus from '@views/components/common/CourseStatus/CourseStatus';
const meta = {
title: 'Components/Common/CourseStatus',
component: CourseStatus,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
args: {
status: Status.WAITLISTED,
size: 'small',
},
argTypes: {
status: {
options: Object.values(Status),
mapping: Object.values(Status),
control: {
type: 'select',
labels: Object.keys(Status),
},
},
size: {
options: ['small', 'mini'],
control: {
type: 'radio',
},
},
},
} satisfies Meta<typeof CourseStatus>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Variants: Story = {
render: args => (
<div className='flex flex-col gap-4 items-center'>
<CourseStatus {...args} size='small' />
<CourseStatus {...args} size='mini' />
</div>
),
};

View File

@@ -0,0 +1,118 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import Instructor from 'src/shared/types/Instructor';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import { getCourseColors } from 'src/shared/util/colors';
import { theme } from 'unocss/preset-mini';
const exampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
description: [
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
'May be counted toward the Quantitative Reasoning flag requirement.',
'Designed to accommodate 100 or more students.',
'Taught as a Web-based course.',
],
flags: ['Quantitative Reasoning'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'Bevo',
lastName: 'Bevo',
fullName: 'Bevo Bevo',
}),
],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 12345,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Components/Common/PopupCourseBlock',
component: PopupCourseBlock,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
colors: getCourseColors('emerald'),
course: exampleCourse,
},
argTypes: {
colors: {
description: 'the colors to use for the course block',
control: 'object',
},
course: {
description: 'the course to show data for',
control: 'object',
},
},
} satisfies Meta<typeof PopupCourseBlock>;
export default meta;
type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Default: Story = {
args: {},
};
export const Variants: Story = {
render: props => (
<div className='grid grid-cols-2 max-w-2xl w-90vw gap-x-4 gap-y-2'>
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.OPEN })} />
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CLOSED })} />
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.WAITLISTED })} />
<PopupCourseBlock {...props} course={new Course({ ...exampleCourse, status: Status.CANCELLED })} />
</div>
),
};
const colors = Object.keys(theme.colors)
// check that the color is a colorway (is an object)
.filter(color => typeof theme.colors[color] === 'object')
.slice(0, 17)
.map(color => getCourseColors(color as keyof typeof theme.colors));
export const AllColors: Story = {
render: props => (
<div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'>
{colors.map((color, i) => (
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
))}
</div>
),
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1046-6714&mode=design&t=5Bjr7qGHNXmjfMTc-0',
},
},
};

View File

@@ -1,4 +1,4 @@
import classNames from 'classnames';
import clsx from 'clsx';
import React from 'react';
import styles from './Button.module.scss';
@@ -30,7 +30,7 @@ export function Button({
<button
style={style}
data-testid={testId}
className={classNames(styles.button, className, styles[type ?? 'primary'], {
className={clsx(styles.button, className, styles[type ?? 'primary'], {
[styles.disabled]: disabled,
})}
title={title}

View File

@@ -3,6 +3,7 @@
.dayLabelContainer {
display: flex;
flex-direction: row;
height: 13px;
min-width: 40px;
min-height: 13px;
@@ -13,24 +14,44 @@
flex: 1 0 0;
}
.dayLabel {
.calendarGrid {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(13, 1fr);
}
.calendarRow {
display: flex;
}
.calendar {
// display: grid;
// grid-template-columns: auto repeat(5, 1fr);
gap: 10px;
}
.day {
gap: 5px;
color: colors.$burnt_orange;
text-align: center;
font-family: Roboto;
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
.calendar {
display: grid;
grid-template-columns: auto repeat(5, 1fr);
gap: 10px;
.timeAndGrid {
display: flex;
}
.day {
gap: 5px;
.timeColumn {
display: flex;
min-height: 573px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
flex: 1 0 0;
border-radius: var(--border-radius-none, 0px);
}
.timeBlock {
@@ -41,18 +62,33 @@
max-width: 50px;
min-height: 40px;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
.timeLabelContainer {
display: flex;
max-height: 20px;
flex-direction: column;
align-items: flex-end;
gap: 17px;
flex: 1 0 0;
align-self: stretch;
border-radius: var(--border-radius-none, 0px);
}
p {
color: #1A2024;
text-align: left;
height: 6.6px;
align-self: stretch;
margin-top: 0;
margin-bottom: 0;
/* Type scale/small */
font-family: "Roboto Flex";
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
}
.timeLabelContainer {
display: flex;
max-height: 20px;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
gap: 17px;
flex: 1 0 0;
align-self: stretch;
border-radius: var(--border-radius-none, 0px);
}

View File

@@ -3,35 +3,50 @@ import styles from './CalendarGrid.module.scss';
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
const daysOfWeek = Object.values(DAY_MAP);
const daysOfWeek = Object.values(DAY_MAP).filter(d => d != "Saturday" && d != "Sunday")
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
const grid = Array.from({ length: 5 }, () =>
Array.from({ length: 13 }, (_, columnIndex) => (
<CalendarCell key={columnIndex} />
))
);
/**
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
const Calendar: React.FC = (props) => {
return (
<div className={styles.calendar}>
<div className={styles.dayLabelContainer}></div>
{daysOfWeek.map((day, dayIndex) => (
<div key={dayIndex} className={styles.day}>
<div className={styles.dayLabelContainer}>
<div className={styles.dayLabel}>{day}</div>
</div>
{hoursOfDay.map((hour) => (
<div key={`${day}-${hour}`} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<span>{hour}:00</span>
</div>
<CalendarCell />
</div>
))}
return (
<div className={styles.calendar}>
<div className={styles.dayLabelContainer}>
{/* Empty cell in the top-left corner */}
<div className={styles.day} />
{/* Displaying day labels */}
{daysOfWeek.map(day => (
<div key={day} className={styles.day}>
{day}
</div>
))}
</div>
);
};
{/* Displaying the rest of the calendar */}
<div className={styles.timeAndGrid}>
<div className={styles.timeColumn}>
{hoursOfDay.map((hour) => (
<div key={hour} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<p>{hour % 12 === 0 ? 12 : hour % 12}:00 {hour < 12 ? 'AM' : 'PM'}</p>
</div>
</div>
))}
</div>
<div className={styles.calendarGrid}>
{grid.map((row, rowIndex) => (
row
))}
</div>
</div>
</div>
)
};
export default Calendar;
export default Calendar;

View File

@@ -1,20 +1,18 @@
.calendarCell {
display: flex;
width: 165px;
height: 52.231px;
min-width: 45px;
min-height: 40px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
position: relative;
}
.hourLine {
position: absolute;
left: 0;
right: 0;
top: 50%;
border-top: 1px solid black; /* Adjust line styles as needed */
}
display: flex;
width: 165px;
height: 52.231px;
min-width: 45px;
min-height: 40px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
border: 1px solid #DADCE0;
}
.hourLine {
width: 165px;
height: 1px;
border-radius: var(--border-radius-none, 0px);
background: rgba(218, 220, 224, 0.25);
}

View File

@@ -8,7 +8,9 @@ import styles from './CalendarGridCell.module.scss';
const CalendarCell: React.FC = (props) => {
return (
<div className={styles.calendarCell}>
<div className={styles.hourLine}></div>
<div className={styles.hourLine}>
</div>
</div>
);
};

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import React, { Component } from 'react';
import clsx from 'clsx';
import React from 'react';
import styles from './Card.module.scss';
export type Props = {
@@ -17,7 +17,7 @@ export default function Card(props: Props) {
return (
<div
style={props.style}
className={classNames(styles.card, props.className)}
className={clsx(styles.card, props.className)}
onClick={props.onClick}
data-testid={props.testId}
>

View File

@@ -0,0 +1,36 @@
import { Status } from '@shared/types/Course';
import { StatusIcon } from '@shared/util/icons';
import clsx from 'clsx';
import React from 'react';
import Text from '../Text/Text';
type SizeType = 'small' | 'mini';
/**
* Props for CourseStatus
*/
export interface CourseStatusProps {
status: Status;
size: SizeType;
}
/**
* The CourseStatus component as per the Labels and Details Figma section
*
* @param props CourseStatusProps
*/
export default function CourseStatus({ status, size }: CourseStatusProps): JSX.Element {
const statusIconSizeClass = clsx({
'h-5 w-5': size === 'small',
'h-4 w-4': size === 'mini',
});
return (
<div className={`inline-flex items-center ${size === 'small' ? 'gap-2' : 'gap-1.5'}`}>
<div className='ml-1 flex items-center justify-center rounded bg-slate-700 p-1px text-white'>
<StatusIcon status={status} className={statusIconSizeClass} />
</div>
<Text variant={size}>{status}</Text>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import classnames from 'classnames';
import clsx from 'clsx';
import React from 'react';
import { Color } from '@views/styles/colors.module.scss';
import styles from './Divider.module.scss';
@@ -21,5 +21,5 @@ export default function Divider(props: Props) {
borderStyle: props.type,
};
return <hr data-testid={props.testId} style={style} className={classnames(styles.divider, props.className)} />;
return <hr data-testid={props.testId} style={style} className={clsx(styles.divider, props.className)} />;
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import styles from './ExtensionRoot.module.scss';
import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
interface Props {

View File

@@ -1,4 +1,4 @@
import classNames from 'classnames';
import clsx from 'clsx';
import React from 'react';
import colors, { Color } from '@views/styles/colors.module.scss';
import fonts, { Size } from '@views/styles/fonts.module.scss';
@@ -36,7 +36,7 @@ export default function Icon(props: Props) {
<span
data-testid={props.testId}
style={style}
className={classNames(styles.icon, props.className)}
className={clsx(styles.icon, props.className)}
onClick={props.onClick}
>
{props.name}

View File

@@ -1,5 +1,5 @@
import { background } from '@shared/messages';
import classNames from 'classnames';
import clsx from 'clsx';
import React, { PropsWithChildren } from 'react';
import Text, { TextProps } from '../Text/Text';
import styles from './Link.module.scss';
@@ -29,7 +29,7 @@ export default function Link(props: PropsWithChildren<Props>) {
color='bluebonnet'
{...passedProps}
span
className={classNames(
className={clsx(
styles.link,
{
[styles.disabled]: isDisabled,

View File

@@ -1,4 +1,4 @@
import classNames from 'classnames';
import clsx from 'clsx';
import React, { PropsWithChildren, useCallback } from 'react';
import styles from './Popup.module.scss';
@@ -46,12 +46,12 @@ export default function Popup({ onClose, children, className, style, testId, ove
<div
style={style}
ref={containerRef}
className={classNames(styles.container, {
className={clsx(styles.container, {
[styles.overlay]: overlay,
})}
data-testid={testId}
>
<div ref={bodyRef} className={classNames(styles.body, className)}>
<div ref={bodyRef} className={clsx(styles.body, className)}>
{children}
</div>
</div>

View File

@@ -0,0 +1,61 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import { Course, Status } from '@shared/types/Course';
import { StatusIcon } from '@shared/util/icons';
import { CourseColors, getCourseColors, pickFontColor } from '@shared/util/colors';
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
import Text from '../Text/Text';
/**
* Props for PopupCourseBlock
*/
export interface PopupCourseBlockProps {
className?: string;
course: Course;
colors: CourseColors;
}
/**
* The "course block" to be used in the extension popup.
*
* @param props PopupCourseBlockProps
*/
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element {
// whiteText based on secondaryColor
const fontColor = pickFontColor(colors.primaryColor);
return (
<div
style={{
backgroundColor: colors.primaryColor,
}}
className={clsx('h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3', className)}
>
<div
style={{
backgroundColor: colors.secondaryColor,
}}
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
>
<DragIndicatorIcon className='h-6 w-6 text-white' />
</div>
<Text
className={clsx('flex-1 py-3.5 text-ellipsis whitespace-nowrap overflow-hidden', fontColor)}
variant='h1-course'
>
<span className='px-0.5 font-450'>{course.uniqueId}</span> {course.department} {course.number} &ndash;{' '}
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
</Text>
{course.status !== Status.OPEN && (
<div
style={{
backgroundColor: colors.secondaryColor,
}}
className='ml-1 flex items-center justify-center justify-self-end rounded p-1px text-white'
>
<StatusIcon status={course.status} className='h-5 w-5' />
</div>
)}
</div>
);
}

View File

@@ -1,4 +1,4 @@
import classNames from 'classnames';
import clsx from 'clsx';
import React from 'react';
import styles from './Spinner.module.scss';
@@ -12,5 +12,5 @@ type Props = {
* A simple spinner component that can be used to indicate loading.
*/
export default function Spinner({ className, testId, style }: Props) {
return <div data-testid={testId} style={style} className={classNames(styles.spinner, className)} />;
return <div data-testid={testId} style={style} className={clsx(styles.spinner, className)} />;
}

View File

@@ -1,65 +1,67 @@
@use 'src/views/styles/colors.module.scss';
@use 'src/views/styles/fonts.module.scss';
.text {
font-family: 'Roboto Flex', sans-serif;
line-height: normal;
font-style: normal;
}
@layer theme {
.text {
font-family: 'Roboto Flex', sans-serif;
line-height: normal;
font-style: normal;
}
.mini {
font-size: 0.79rem;
font-weight: 500;
}
.mini {
font-size: 0.79rem;
font-weight: 500;
}
.small {
font-size: 0.88875rem;
font-weight: 500;
}
.small {
font-size: 0.88875rem;
font-weight: 500;
}
.p {
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.025rem;
}
.p {
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.025rem;
}
.h4 {
font-size: 1.125rem;
font-weight: 500;
}
.h4 {
font-size: 1.125rem;
font-weight: 500;
}
.h3-course {
font-size: 0.6875rem;
font-weight: 400;
line-height: 100%; /* 0.6875rem */
}
.h3-course {
font-size: 0.6875rem;
font-weight: 400;
line-height: 100%; /* 0.6875rem */
}
.h3 {
font-size: 1.26563rem;
font-weight: 600;
text-transform: uppercase;
}
.h3 {
font-size: 1.26563rem;
font-weight: 600;
text-transform: uppercase;
}
.h2-course {
font-size: 1rem;
font-weight: 500;
letter-spacing: 0.03125rem;
text-transform: capitalize;
}
.h2-course {
font-size: 1rem;
font-weight: 500;
letter-spacing: 0.03125rem;
text-transform: capitalize;
}
.h2 {
font-size: 1.42375rem;
font-weight: 500;
}
.h2 {
font-size: 1.42375rem;
font-weight: 500;
}
.h1-course {
font-size: 1rem;
font-weight: 600;
text-transform: capitalize;
}
.h1-course {
font-size: 1rem;
font-weight: 600;
text-transform: capitalize;
}
.h1 {
font-size: 1.60188rem;
font-weight: 700;
text-transform: uppercase;
.h1 {
font-size: 1.60188rem;
font-weight: 700;
text-transform: uppercase;
}
}

View File

@@ -1,4 +1,4 @@
import classNames from 'classnames';
import clsx from 'clsx';
import React, { PropsWithChildren } from 'react';
import styles from './Text.module.scss';
@@ -20,7 +20,7 @@ type Variant = (typeof variants)[number];
* A reusable Text component with props that build on top of the design system for the extension
*/
export default function Text({ variant, as, className, ...props }: PropsWithChildren<TextProps>) {
const mergedClassName = classNames(styles.text, styles[variant], className);
const mergedClassName = clsx(styles.text, styles[variant], className);
if (as === 'div') return <div className={mergedClassName} {...props} />;

View File

@@ -1,5 +1,5 @@
import { Course } from '@shared/types/Course';
import classNames from 'classnames';
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';
@@ -64,7 +64,7 @@ interface LineProps {
function DescriptionLine({ line }: LineProps) {
const lowerCaseLine = line.toLowerCase();
const className = classNames({
const className = clsx({
[styles.prerequisite]: lowerCaseLine.includes('prerequisite'),
[styles.onlyOne]:
lowerCaseLine.includes('may be') || lowerCaseLine.includes('only one') || lowerCaseLine.includes('may not'),