chore: merge branch 'main' into sgunter/button-component

for clsx, and other features
This commit is contained in:
Samuel Gunter
2024-02-07 00:27:55 -06:00
18 changed files with 282 additions and 89 deletions

View File

@@ -21,7 +21,7 @@
"@unocss/reset": "^0.58.5", "@unocss/reset": "^0.58.5",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.51", "chrome-extension-toolkit": "^0.0.51",
"classnames": "^2.5.1", "clsx": "^2.1.0",
"highcharts": "^11.3.0", "highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1", "highcharts-react-official": "^3.2.1",
"react": "^18.2.0", "react": "^18.2.0",

15
pnpm-lock.yaml generated
View File

@@ -25,9 +25,9 @@ dependencies:
chrome-extension-toolkit: chrome-extension-toolkit:
specifier: ^0.0.51 specifier: ^0.0.51
version: 0.0.51 version: 0.0.51
classnames: clsx:
specifier: ^2.5.1 specifier: ^2.1.0
version: 2.5.1 version: 2.1.0
highcharts: highcharts:
specifier: ^11.3.0 specifier: ^11.3.0
version: 11.3.0 version: 11.3.0
@@ -6111,10 +6111,6 @@ packages:
consola: 3.2.3 consola: 3.2.3
dev: true dev: true
/classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
dev: false
/clean-stack@2.2.0: /clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -6166,6 +6162,11 @@ packages:
engines: {node: '>=0.8'} engines: {node: '>=0.8'}
dev: true dev: true
/clsx@2.1.0:
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
engines: {node: '>=6'}
dev: false
/color-convert@1.9.3: /color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies: dependencies:

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,67 @@
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import CalendarCourseCell from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell';
const meta = {
title: 'Components/Common/CalendarCourseCell',
component: CalendarCourseCell,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
course: { control: 'object' },
meetingIdx: { control: 'number' },
color: { control: 'color' },
},
render: (args: any) => (
<div className="w-45">
<CalendarCourseCell {...args} />
</div>
),
} satisfies Meta<typeof CalendarCourseCell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
course: new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Spring',
},
}),
meetingIdx: 0,
color: 'red',
},
};

View File

@@ -1,4 +1,4 @@
import classNames from 'classnames'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import styles from './Button.module.scss'; import styles from './Button.module.scss';
@@ -43,7 +43,7 @@ export function Button({
} as React.CSSProperties } as React.CSSProperties
} }
data-testid={testId} data-testid={testId}
className={classNames(useScss ? styles.button : BUTTON_BASE_CLASS, className, { className={clsx(useScss ? styles.button : BUTTON_BASE_CLASS, className, {
[styles[variant]]: useScss, [styles[variant]]: useScss,
[styles.disabled]: disabled && useScss, [styles.disabled]: disabled && useScss,
'disabled:(cursor-not-allowed opacity-50)': disabled && !useScss, 'disabled:(cursor-not-allowed opacity-50)': disabled && !useScss,

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse';
import CancelledIcon from '~icons/material-symbols/warning';
import Text from '../Text/Text';
export interface CalendarCourseBlockProps {
/** The Course that the meeting is for. */
course: Course;
/* index into course meeting array to display */
meetingIdx?: number;
/** The background color for the course. */
color: string;
}
const CalendarCourseBlock: React.FC<CalendarCourseBlockProps> = ({ course, meetingIdx }: CalendarCourseBlockProps) => {
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
let rightIcon: React.ReactNode | null = null;
if (course.status === Status.WAITLISTED) {
rightIcon = <WaitlistIcon className='h-5 w-5' />;
} else if (course.status === Status.CLOSED) {
rightIcon = <ClosedIcon className='h-5 w-5' />;
} else if (course.status === Status.CANCELLED) {
rightIcon = <CancelledIcon className='h-5 w-5' />;
}
return (
<div className='w-full flex justify-center rounded bg-slate-300 p-2 text-ut-black'>
<div className='flex flex-1 flex-col gap-1'>
<Text variant='h1-course' className='leading-[75%]!'>
{course.department} {course.number} - {course.instructors[0].lastName}
</Text>
<Text variant='h3-course' className='leading-[75%]!'>
{`${meeting.getTimeString({ separator: '', capitalize: true })}${
meeting.location ? ` ${meeting.location.building}` : ''
}`}
</Text>
</div>
{rightIcon && (
<div className='h-fit flex items-center justify-center justify-self-start rounded bg-slate-700 p-0.5 text-white'>
{rightIcon}
</div>
)}
</div>
);
};
export default CalendarCourseBlock;

View File

@@ -3,6 +3,7 @@
.dayLabelContainer { .dayLabelContainer {
display: flex; display: flex;
flex-direction: row;
height: 13px; height: 13px;
min-width: 40px; min-width: 40px;
min-height: 13px; min-height: 13px;
@@ -13,24 +14,44 @@
flex: 1 0 0; 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; color: colors.$burnt_orange;
text-align: center; text-align: center;
font-family: Roboto;
font-size: 14.22px; font-size: 14.22px;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
line-height: normal; line-height: normal;
} }
.calendar { .timeAndGrid {
display: grid; display: flex;
grid-template-columns: auto repeat(5, 1fr);
gap: 10px;
} }
.day { .timeColumn {
gap: 5px; 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 { .timeBlock {
@@ -41,18 +62,33 @@
max-width: 50px; max-width: 50px;
min-height: 40px; min-height: 40px;
flex-direction: column; flex-direction: column;
justify-content: flex-end;
align-items: 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 CalendarCell from '../CalendarGridCell/CalendarGridCell';
import { DAY_MAP } from 'src/shared/types/CourseMeeting'; 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 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 * Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props * @param props
*/ */
const Calendar: React.FC = (props) => { const Calendar: React.FC = (props) => {
return (
return ( <div className={styles.calendar}>
<div className={styles.calendar}> <div className={styles.dayLabelContainer}>
<div className={styles.dayLabelContainer}></div> {/* Empty cell in the top-left corner */}
{daysOfWeek.map((day, dayIndex) => ( <div className={styles.day} />
<div key={dayIndex} className={styles.day}> {/* Displaying day labels */}
<div className={styles.dayLabelContainer}> {daysOfWeek.map(day => (
<div className={styles.dayLabel}>{day}</div> <div key={day} className={styles.day}>
</div> {day}
{hoursOfDay.map((hour) => (
<div key={`${day}-${hour}`} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<span>{hour}:00</span>
</div>
<CalendarCell />
</div>
))}
</div> </div>
))} ))}
</div> </div>
); {/* Displaying the rest of the calendar */}
}; <div className={styles.timeAndGrid}>
<div className={styles.timeColumn}>
export default Calendar; {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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Course } from '@shared/types/Course'; import { Course } from '@shared/types/Course';
import classNames from 'classnames'; import clsx from 'clsx';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Spinner from '@views/components/common/Spinner/Spinner'; import Spinner from '@views/components/common/Spinner/Spinner';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
@@ -64,7 +64,7 @@ interface LineProps {
function DescriptionLine({ line }: LineProps) { function DescriptionLine({ line }: LineProps) {
const lowerCaseLine = line.toLowerCase(); const lowerCaseLine = line.toLowerCase();
const className = classNames({ const className = clsx({
[styles.prerequisite]: lowerCaseLine.includes('prerequisite'), [styles.prerequisite]: lowerCaseLine.includes('prerequisite'),
[styles.onlyOne]: [styles.onlyOne]:
lowerCaseLine.includes('may be') || lowerCaseLine.includes('only one') || lowerCaseLine.includes('may not'), lowerCaseLine.includes('may be') || lowerCaseLine.includes('only one') || lowerCaseLine.includes('may not'),