diff --git a/package.json b/package.json index 4994bca5..c2c63fbd 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "@unocss/reset": "^0.58.5", "@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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8f3b44e..8f530c7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,9 +25,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 @@ -6111,10 +6111,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'} @@ -6166,6 +6162,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: diff --git a/src/shared/util/icons.tsx b/src/shared/util/icons.tsx new file mode 100644 index 00000000..88f4ebfd --- /dev/null +++ b/src/shared/util/icons.tsx @@ -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 & { status: Status }): React.ReactElement { + const { status, ...rest } = props; + + switch (props.status) { + case Status.WAITLISTED: + return ; + case Status.CLOSED: + return ; + case Status.CANCELLED: + return ; + default: + } +} diff --git a/src/stories/components/CalendarCourseCell.stories.tsx b/src/stories/components/CalendarCourseCell.stories.tsx new file mode 100644 index 00000000..647a766d --- /dev/null +++ b/src/stories/components/CalendarCourseCell.stories.tsx @@ -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) => ( +
+ +
+ ), +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +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', + }, +}; diff --git a/src/views/components/common/Button/Button.tsx b/src/views/components/common/Button/Button.tsx index c5638970..f52119b8 100644 --- a/src/views/components/common/Button/Button.tsx +++ b/src/views/components/common/Button/Button.tsx @@ -1,4 +1,4 @@ -import classNames from 'classnames'; +import clsx from 'clsx'; import React from 'react'; import styles from './Button.module.scss'; @@ -43,7 +43,7 @@ export function Button({ } as React.CSSProperties } 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.disabled]: disabled && useScss, 'disabled:(cursor-not-allowed opacity-50)': disabled && !useScss, diff --git a/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx b/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx new file mode 100644 index 00000000..27b2519e --- /dev/null +++ b/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx @@ -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 = ({ 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 = ; + } else if (course.status === Status.CLOSED) { + rightIcon = ; + } else if (course.status === Status.CANCELLED) { + rightIcon = ; + } + + return ( +
+
+ + {course.department} {course.number} - {course.instructors[0].lastName} + + + {`${meeting.getTimeString({ separator: '–', capitalize: true })}${ + meeting.location ? ` – ${meeting.location.building}` : '' + }`} + +
+ {rightIcon && ( +
+ {rightIcon} +
+ )} +
+ ); +}; + +export default CalendarCourseBlock; diff --git a/src/views/components/common/CalendarGrid/CalendarGrid.module.scss b/src/views/components/common/CalendarGrid/CalendarGrid.module.scss index 6f1cf6c8..a2e208a4 100644 --- a/src/views/components/common/CalendarGrid/CalendarGrid.module.scss +++ b/src/views/components/common/CalendarGrid/CalendarGrid.module.scss @@ -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); -} \ No newline at end of file diff --git a/src/views/components/common/CalendarGrid/CalendarGrid.tsx b/src/views/components/common/CalendarGrid/CalendarGrid.tsx index f653b5bd..4b90edf1 100644 --- a/src/views/components/common/CalendarGrid/CalendarGrid.tsx +++ b/src/views/components/common/CalendarGrid/CalendarGrid.tsx @@ -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) => ( + + )) +); /** * Grid of CalendarGridCell components forming the user's course schedule calendar view * @param props */ const Calendar: React.FC = (props) => { - - return ( -
-
- {daysOfWeek.map((day, dayIndex) => ( -
-
-
{day}
-
- {hoursOfDay.map((hour) => ( -
-
- {hour}:00 -
- -
- ))} + return ( +
+
+ {/* Empty cell in the top-left corner */} +
+ {/* Displaying day labels */} + {daysOfWeek.map(day => ( +
+ {day}
))}
- ); - }; - - export default Calendar; \ No newline at end of file + {/* Displaying the rest of the calendar */} +
+
+ {hoursOfDay.map((hour) => ( +
+
+

{hour % 12 === 0 ? 12 : hour % 12}:00 {hour < 12 ? 'AM' : 'PM'}

+
+
+ ))} +
+
+ {grid.map((row, rowIndex) => ( + row + ))} +
+
+
+ ) +}; + +export default Calendar; \ No newline at end of file diff --git a/src/views/components/common/CalendarGridCell/CalendarGridCell.module.scss b/src/views/components/common/CalendarGridCell/CalendarGridCell.module.scss index 52db015a..2199d866 100644 --- a/src/views/components/common/CalendarGridCell/CalendarGridCell.module.scss +++ b/src/views/components/common/CalendarGridCell/CalendarGridCell.module.scss @@ -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; - } + 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 { - position: absolute; - left: 0; - right: 0; - top: 50%; - border-top: 1px solid black; /* Adjust line styles as needed */ - } - \ No newline at end of file +.hourLine { + width: 165px; + height: 1px; + border-radius: var(--border-radius-none, 0px); + background: rgba(218, 220, 224, 0.25); +} \ No newline at end of file diff --git a/src/views/components/common/CalendarGridCell/CalendarGridCell.tsx b/src/views/components/common/CalendarGridCell/CalendarGridCell.tsx index 03bbfdf0..cdc2c1b1 100644 --- a/src/views/components/common/CalendarGridCell/CalendarGridCell.tsx +++ b/src/views/components/common/CalendarGridCell/CalendarGridCell.tsx @@ -8,7 +8,9 @@ import styles from './CalendarGridCell.module.scss'; const CalendarCell: React.FC = (props) => { return (
-
+
+ +
); }; diff --git a/src/views/components/common/Card/Card.tsx b/src/views/components/common/Card/Card.tsx index f063c4b8..725490a2 100644 --- a/src/views/components/common/Card/Card.tsx +++ b/src/views/components/common/Card/Card.tsx @@ -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 (
diff --git a/src/views/components/common/Divider/Divider.tsx b/src/views/components/common/Divider/Divider.tsx index d515bc3b..3ad13117 100644 --- a/src/views/components/common/Divider/Divider.tsx +++ b/src/views/components/common/Divider/Divider.tsx @@ -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
; + return
; } diff --git a/src/views/components/common/Icon/Icon.tsx b/src/views/components/common/Icon/Icon.tsx index 084c7111..9f6192e3 100644 --- a/src/views/components/common/Icon/Icon.tsx +++ b/src/views/components/common/Icon/Icon.tsx @@ -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) { {props.name} diff --git a/src/views/components/common/Link/Link.tsx b/src/views/components/common/Link/Link.tsx index f4e9a9a1..51a21eca 100644 --- a/src/views/components/common/Link/Link.tsx +++ b/src/views/components/common/Link/Link.tsx @@ -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) { color='bluebonnet' {...passedProps} span - className={classNames( + className={clsx( styles.link, { [styles.disabled]: isDisabled, diff --git a/src/views/components/common/Popup/Popup.tsx b/src/views/components/common/Popup/Popup.tsx index 24f8e495..15264de2 100644 --- a/src/views/components/common/Popup/Popup.tsx +++ b/src/views/components/common/Popup/Popup.tsx @@ -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
-
+
{children}
diff --git a/src/views/components/common/Spinner/Spinner.tsx b/src/views/components/common/Spinner/Spinner.tsx index 6d15c2c5..6aa0620f 100644 --- a/src/views/components/common/Spinner/Spinner.tsx +++ b/src/views/components/common/Spinner/Spinner.tsx @@ -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
; + return
; } diff --git a/src/views/components/common/Text/Text.tsx b/src/views/components/common/Text/Text.tsx index 99d62d28..c444619c 100644 --- a/src/views/components/common/Text/Text.tsx +++ b/src/views/components/common/Text/Text.tsx @@ -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) { - const mergedClassName = classNames(styles.text, styles[variant], className); + const mergedClassName = clsx(styles.text, styles[variant], className); if (as === 'div') return
; diff --git a/src/views/components/injected/CoursePopup/CourseDescription/CourseDescription.tsx b/src/views/components/injected/CoursePopup/CourseDescription/CourseDescription.tsx index a130948a..c1eedc25 100644 --- a/src/views/components/injected/CoursePopup/CourseDescription/CourseDescription.tsx +++ b/src/views/components/injected/CoursePopup/CourseDescription/CourseDescription.tsx @@ -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'),