Merge branch 'main' into ListComponentContinuation

This commit is contained in:
knownotunknown
2024-02-09 15:54:44 -06:00
38 changed files with 23470 additions and 1033 deletions

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

@@ -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

@@ -0,0 +1,94 @@
@use 'sass:color';
@use 'src/views/styles/colors.module.scss';
.dayLabelContainer {
display: flex;
flex-direction: row;
height: 13px;
min-width: 40px;
min-height: 13px;
padding-bottom: 15px;
justify-content: center;
align-items: center;
gap: 10px;
flex: 1 0 0;
}
.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-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
.timeAndGrid {
display: flex;
}
.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 {
display: flex;
width: 50px;
height: 40.279px;
min-width: 50px;
max-width: 50px;
min-height: 40px;
flex-direction: column;
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;
}
}

View File

@@ -0,0 +1,52 @@
import React from 'react';
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).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}>
{/* 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;

View File

@@ -0,0 +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;
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

@@ -0,0 +1,18 @@
import React from 'react';
import styles from './CalendarGridCell.module.scss';
/**
* Component representing each 1 hour time block of a calendar
* @param props
*/
const CalendarCell: React.FC = (props) => {
return (
<div className={styles.calendarCell}>
<div className={styles.hourLine}>
</div>
</div>
);
};
export default CalendarCell;

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

@@ -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,9 @@
import React from 'react';
import styles from './ExtensionRoot.module.scss';
import '@unocss/reset/tailwind-compat.css';
import 'uno.css';
interface Props {
testId?: string;
}

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,60 +1,67 @@
@use 'src/views/styles/colors.module.scss';
@use 'src/views/styles/fonts.module.scss';
.text {
font-family: 'Inter', sans-serif;
color: colors.$charcoal;
line-height: initial;
}
@layer theme {
.text {
font-family: 'Roboto Flex', sans-serif;
line-height: normal;
font-style: normal;
}
.light_weight {
font-weight: fonts.$light_weight;
}
.mini {
font-size: 0.79rem;
font-weight: 500;
}
.regular_weight {
font-weight: fonts.$regular_weight;
}
.small {
font-size: 0.88875rem;
font-weight: 500;
}
.normal_weight {
font-weight: fonts.$normal_weight;
}
.p {
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.025rem;
}
.semi_bold_weight {
font-weight: fonts.$semi_bold_weight;
}
.h4 {
font-size: 1.125rem;
font-weight: 500;
}
.bold_weight {
font-weight: fonts.$bold_weight;
}
.h3-course {
font-size: 0.6875rem;
font-weight: 400;
line-height: 100%; /* 0.6875rem */
}
.black_weight {
font-weight: fonts.$black_weight;
}
.h3 {
font-size: 1.26563rem;
font-weight: 600;
text-transform: uppercase;
}
.x_small_size {
font-size: fonts.$x_small_size;
}
.h2-course {
font-size: 1rem;
font-weight: 500;
letter-spacing: 0.03125rem;
text-transform: capitalize;
}
.xx_small_size {
font-size: fonts.$xx_small_size;
}
.h2 {
font-size: 1.42375rem;
font-weight: 500;
}
.small_size {
font-size: fonts.$small_size;
}
.h1-course {
font-size: 1rem;
font-weight: 600;
text-transform: capitalize;
}
.medium_size {
font-size: fonts.$medium_size;
}
.large_size {
font-size: fonts.$large_size;
}
.x_large_size {
font-size: fonts.$x_large_size;
}
.xx_large_size {
font-size: fonts.$xx_large_size;
.h1 {
font-size: 1.60188rem;
font-weight: 700;
text-transform: uppercase;
}
}

View File

@@ -1,50 +1,32 @@
import classNames from 'classnames';
import clsx from 'clsx';
import React, { PropsWithChildren } from 'react';
import colors, { Color } from '@views/styles/colors.module.scss';
import { Size, Weight } from '@views/styles/fonts.module.scss';
import styles from './Text.module.scss';
/**
*
*/
export type TextProps = {
color?: Color;
weight?: Weight;
size?: Size;
span?: boolean;
className?: string;
onClick?: () => void;
title?: string;
align?: React.CSSProperties['textAlign'];
style?: React.CSSProperties;
};
variant?: Variant;
} & (
| (React.HTMLAttributes<HTMLSpanElement> & { as?: 'span' })
| (React.HTMLAttributes<HTMLDivElement> & { as: 'div' })
);
const variants = ['mini', 'small', 'p', 'h4', 'h3-course', 'h3', 'h2-course', 'h2', 'h1-course', 'h1'] as const;
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(props: PropsWithChildren<TextProps>) {
const style: React.CSSProperties = {
...props.style,
textAlign: props.align,
color: props.color ? colors[props.color] : undefined,
};
export default function Text({ variant, as, className, ...props }: PropsWithChildren<TextProps>) {
const mergedClassName = clsx(styles.text, styles[variant], className);
const weightClass = `${props.weight ?? 'regular'}_weight`;
const fontSizeClass = `${props.size ?? 'medium'}_size`;
const className = classNames(styles.text, styles[weightClass], styles[fontSizeClass], props.className);
if (props.span) {
return (
<span title={props.title} className={className} style={style} onClick={props.onClick}>
{props.children}
</span>
);
}
if (as === 'div') return <div className={mergedClassName} {...props} />;
return (
<div title={props.title} className={className} style={style} onClick={props.onClick}>
<span className={mergedClassName} {...props}>
{props.children}
</div>
</span>
);
}

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'),

View File

@@ -1,13 +1,20 @@
@each $weights in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
@each $weight in '100' '200' '300' '400' '500' '600' '700' '800' '900' {
@font-face {
font-family: 'Inter';
src: url('@public/fonts/inter-#{$weights}.woff2') format('woff2');
src: url('@public/fonts/inter-#{$weight}.woff2') format('woff2');
font-display: auto;
font-style: normal;
font-weight: #{$weights};
font-weight: #{$weight};
}
}
@font-face {
font-family: 'Roboto Flex';
src: url('@public/fonts/roboto-flex.woff2') format('woff2');
font-display: swap;
font-style: normal;
}
@font-face {
font-family: 'Material Icons Round';
font-style: normal;
@@ -16,34 +23,12 @@
src: url('@public/fonts/material-icons.woff2') format('woff2');
}
$light_weight: 300;
$regular_weight: 400;
$normal_weight: 500;
$semi_bold_weight: 600;
$bold_weight: 700;
$black_weight: 900;
$normal_weight: 500; // Used by <Icon>, will be removed later
$xx_small_size: 4px;
$x_small_size: 8px;
$small_size: 12px;
$medium_size: 16px;
$large_size: 20px;
$x_large_size: 32px;
$xx_large_size: 48px;
:export {
light_weight: $light_weight;
regular_weight: $regular_weight;
normal_weight: $normal_weight;
semi_bold_weight: $semi_bold_weight;
bold_weight: $bold_weight;
black_weight: $black_weight;
xx_small_size: $xx_small_size;
x_small_size: $x_small_size;
small_size: $small_size;
medium_size: $medium_size;
large_size: $large_size;
x_large_size: $x_large_size;
xx_large_size: $xx_large_size;
}

View File

@@ -2,25 +2,14 @@
* the type for all the weight scss variables exported from fonts.module.scss
*/
export interface IWeights {
light_weight: number;
regular_weight: number;
normal_weight: number;
bold_weight: number;
semi_bold_weight: number;
black_weight: number;
}
/**
* the type for all the size scss variables exported from fonts.module.scss
*/
export interface ISizes {
xx_small_size: number;
x_small_size: number;
small_size: number;
medium_size: number;
large_size: number;
x_large_size: number;
xx_large_size: number;
}
/** A utility type that removes the _weight postfix from the variable names for weights */