feat: enable TS strict mode (#168)
* feat: enable TS strict mode * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: colors bug with default * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: text type errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors - add definite assignment assertion * fix: strict TS errors - add definite assignment assertion * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix(ESLint): error on no-explicit-any * fix: type annotations for any types * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors (and remove packages) * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * fix: strict TS errors * feat: enable React.StrictMode * fix: strict TS errors (done!) * fix: build error * fix: replace no-explicit-any assertions * refactor: cleanup * refactor: more cleanup * style: prettier --------- Co-authored-by: Lukas Zenick <lukas@utexas.edu> Co-authored-by: Razboy20 <razboy20@gmail.com>
This commit is contained in:
@@ -20,7 +20,7 @@ interface Props {
|
||||
/**
|
||||
* This is the top level react component orchestrating the course catalog page.
|
||||
*/
|
||||
export default function CourseCatalogMain({ support }: Props): JSX.Element {
|
||||
export default function CourseCatalogMain({ support }: Props): JSX.Element | null {
|
||||
const [rows, setRows] = React.useState<ScrapedRow[]>([]);
|
||||
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
|
||||
const [showPopup, setShowPopup] = useState(false);
|
||||
@@ -53,8 +53,6 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element {
|
||||
setSelectedCourse(course);
|
||||
};
|
||||
|
||||
// useKeyPress('Escape', handleClearSelectedCourse);
|
||||
|
||||
const [activeSchedule] = useSchedules();
|
||||
|
||||
if (!activeSchedule) {
|
||||
@@ -78,7 +76,7 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element {
|
||||
)
|
||||
)}
|
||||
<CourseCatalogInjectedPopup
|
||||
course={selectedCourse}
|
||||
course={selectedCourse!} // always defined when showPopup is true
|
||||
show={showPopup}
|
||||
onClose={() => setShowPopup(false)}
|
||||
afterLeave={() => setSelectedCourse(null)}
|
||||
|
||||
@@ -20,16 +20,20 @@ import TeamLinks from '../TeamLinks';
|
||||
export default function Calendar(): JSX.Element {
|
||||
const calendarRef = useRef<HTMLDivElement>(null);
|
||||
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
|
||||
|
||||
const [course, setCourse] = useState<Course | null>((): Course | null => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const uniqueIdRaw = urlParams.get('uniqueId');
|
||||
if (uniqueIdRaw === null) return null;
|
||||
|
||||
const uniqueId = Number(uniqueIdRaw);
|
||||
const course = activeSchedule.courses.find(course => course.uniqueId === uniqueId);
|
||||
if (course === undefined) return null;
|
||||
|
||||
urlParams.delete('uniqueId');
|
||||
const newUrl = `${window.location.pathname}?${urlParams}`.replace(/\?$/, '');
|
||||
window.history.replaceState({}, '', newUrl);
|
||||
|
||||
return course;
|
||||
});
|
||||
|
||||
@@ -41,16 +45,20 @@ export default function Calendar(): JSX.Element {
|
||||
async openCoursePopup({ data, sendResponse }) {
|
||||
const course = activeSchedule.courses.find(course => course.uniqueId === data.uniqueId);
|
||||
if (course === undefined) return;
|
||||
|
||||
setCourse(course);
|
||||
setShowPopup(true);
|
||||
sendResponse(await chrome.tabs.getCurrent());
|
||||
|
||||
const currentTab = await chrome.tabs.getCurrent();
|
||||
if (currentTab === undefined) return;
|
||||
sendResponse(currentTab);
|
||||
},
|
||||
});
|
||||
|
||||
listener.listen();
|
||||
|
||||
return () => listener.unlisten();
|
||||
}, [activeSchedule.courses]);
|
||||
}, [activeSchedule]);
|
||||
|
||||
useEffect(() => {
|
||||
if (course) setShowPopup(true);
|
||||
@@ -85,7 +93,7 @@ export default function Calendar(): JSX.Element {
|
||||
</div>
|
||||
|
||||
<CourseCatalogInjectedPopup
|
||||
course={course}
|
||||
course={course!} // always defined when showPopup is true
|
||||
onClose={() => setShowPopup(false)}
|
||||
open={showPopup}
|
||||
afterLeave={() => setCourse(null)}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Course } from '@shared/types/Course';
|
||||
import type { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './CalendarCourseMeeting.module.scss';
|
||||
@@ -12,28 +11,27 @@ export interface CalendarCourseMeetingProps {
|
||||
course: Course;
|
||||
/* index into course meeting array to display */
|
||||
meetingIdx?: number;
|
||||
/** The icon to display on the right side of the course. This is optional. */
|
||||
rightIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* `CalendarCourseMeeting` is a functional component that displays a course meeting.
|
||||
*
|
||||
* @example
|
||||
* <CalendarCourseMeeting course={course} meeting={meeting} rightIcon={<Icon />} />
|
||||
* <CalendarCourseMeeting course={course} meeting={meeting} />
|
||||
*/
|
||||
export default function CalendarCourseMeeting({
|
||||
course,
|
||||
meetingIdx,
|
||||
rightIcon,
|
||||
}: CalendarCourseMeetingProps): JSX.Element {
|
||||
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
||||
export default function CalendarCourseMeeting({ course, meetingIdx }: CalendarCourseMeetingProps): JSX.Element | null {
|
||||
let meeting = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : undefined;
|
||||
|
||||
if (!meeting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.component}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles['course-detail']}>
|
||||
<div className={styles.course}>
|
||||
{course.department} {course.number} - {course.instructors[0].lastName}
|
||||
{course.department} {course.number} - {course.instructors[0]?.lastName}
|
||||
</div>
|
||||
<div className={styles['time-and-location']}>
|
||||
{`${meeting.getTimeString({ separator: '-', capitalize: true })}${
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ThemeColor, TWIndex } from '@shared/types/ThemeColors';
|
||||
import { getThemeColorHexByName } from '@shared/util/themeColors';
|
||||
import Divider from '@views/components/common/Divider/Divider';
|
||||
import React from 'react';
|
||||
@@ -30,16 +31,19 @@ const baseColors = [
|
||||
'fuchsia',
|
||||
'pink',
|
||||
'rose',
|
||||
];
|
||||
] as const;
|
||||
|
||||
const BaseColorNum = 500;
|
||||
const StartingShadeIndex = 200;
|
||||
const BaseColorNum: TWIndex = 500;
|
||||
const StartingShadeIndex: TWIndex = 200;
|
||||
const ShadeIncrement = 100;
|
||||
|
||||
const colorPatchColors = new Map<string, string[]>(
|
||||
baseColors.map((baseColor: string) => [
|
||||
baseColors.map(baseColor => [
|
||||
theme.colors[baseColor][BaseColorNum],
|
||||
Array.from({ length: 6 }, (_, index) => theme.colors[baseColor][StartingShadeIndex + ShadeIncrement * index]),
|
||||
Array.from(
|
||||
{ length: 6 },
|
||||
(_, index) => theme.colors[baseColor][(StartingShadeIndex + ShadeIncrement * index) as TWIndex]
|
||||
),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -51,7 +55,7 @@ const hexCodeToBaseColor = new Map<string, string>(
|
||||
* Props for the CourseCellColorPicker component.
|
||||
*/
|
||||
export interface CourseCellColorPickerProps {
|
||||
setSelectedColor: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
setSelectedColor: React.Dispatch<React.SetStateAction<ThemeColor | null>>;
|
||||
isInvertColorsToggled: boolean;
|
||||
setIsInvertColorsToggled: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
@@ -89,7 +93,7 @@ export default function CourseCellColorPicker({
|
||||
const [hexCode, setHexCode] = React.useState<string>(
|
||||
getThemeColorHexByName('ut-gray').slice(1).toLocaleLowerCase()
|
||||
);
|
||||
const hexCodeWithHash = `#${hexCode}`;
|
||||
const hexCodeWithHash = `#${hexCode}` as ThemeColor;
|
||||
const selectedBaseColor = hexCodeToBaseColor.get(hexCodeWithHash);
|
||||
|
||||
const handleSelectColorPatch = (baseColor: string) => {
|
||||
@@ -124,7 +128,7 @@ export default function CourseCellColorPicker({
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{hexCodeToBaseColor.has(hexCodeWithHash) && (
|
||||
{selectedBaseColor && (
|
||||
<>
|
||||
<Divider orientation='horizontal' size='100%' className='my-1' />
|
||||
<div className='grid grid-cols-6 gap-1'>
|
||||
|
||||
@@ -26,7 +26,7 @@ function CalendarHour({ hour }: { hour: number }) {
|
||||
}
|
||||
|
||||
function makeGridRow(row: number, cols: number): JSX.Element {
|
||||
const hour = hoursOfDay[row];
|
||||
const hour = hoursOfDay[row]!;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -83,14 +83,17 @@ interface AccountForCourseConflictsProps {
|
||||
// TODO: Deal with react strict mode (wacky movements)
|
||||
function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseConflictsProps): JSX.Element[] {
|
||||
// Groups by dayIndex to identify overlaps
|
||||
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
|
||||
const { dayIndex } = cell.calendarGridPoint;
|
||||
if (!acc[dayIndex]) {
|
||||
acc[dayIndex] = [];
|
||||
}
|
||||
acc[dayIndex].push(cell);
|
||||
return acc;
|
||||
}, {});
|
||||
const days = courseCells.reduce(
|
||||
(acc, cell: CalendarGridCourse) => {
|
||||
const { dayIndex } = cell.calendarGridPoint;
|
||||
if (acc[dayIndex] === undefined) {
|
||||
acc[dayIndex] = [];
|
||||
}
|
||||
acc[dayIndex]!.push(cell);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, CalendarGridCourse[]>
|
||||
);
|
||||
|
||||
// Check for overlaps within each day and adjust gridColumnIndex and totalColumns
|
||||
Object.values(days).forEach((dayCells: CalendarGridCourse[]) => {
|
||||
@@ -121,7 +124,7 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
|
||||
});
|
||||
|
||||
return courseCells.map((block, i) => {
|
||||
const { courseDeptAndInstr, timeAndLocation, status } = courseCells[i].componentProps;
|
||||
const { courseDeptAndInstr, timeAndLocation, status } = courseCells[i]!.componentProps;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -129,8 +132,8 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
|
||||
style={{
|
||||
gridColumn: `${block.calendarGridPoint.dayIndex + 3}`,
|
||||
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
|
||||
width: `calc(100% / ${block.totalColumns})`,
|
||||
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
|
||||
width: `calc(100% / ${block.totalColumns ?? 1})`,
|
||||
marginLeft: `calc(100% * ${((block.gridColumnStart ?? 0) - 1) / (block.totalColumns ?? 1)})`,
|
||||
padding: '0px 10px 4px 0px',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||
import type { Serialized } from 'chrome-extension-toolkit';
|
||||
import { toPng } from 'html-to-image';
|
||||
|
||||
export const CAL_MAP = {
|
||||
@@ -13,9 +15,9 @@ export const CAL_MAP = {
|
||||
|
||||
/**
|
||||
* Retrieves the schedule from the UserScheduleStore based on the active index.
|
||||
* @returns {Promise<any>} A promise that resolves to the retrieved schedule.
|
||||
* @returns A promise that resolves to the retrieved schedule.
|
||||
*/
|
||||
const getSchedule = async () => {
|
||||
const getSchedule = async (): Promise<Serialized<UserSchedule> | undefined> => {
|
||||
const schedules = await UserScheduleStore.get('schedules');
|
||||
const activeIndex = await UserScheduleStore.get('activeIndex');
|
||||
const schedule = schedules[activeIndex];
|
||||
@@ -61,6 +63,10 @@ export const saveAsCal = async () => {
|
||||
|
||||
let icsString = 'BEGIN:VCALENDAR\nVERSION:2.0\nCALSCALE:GREGORIAN\nX-WR-CALNAME:My Schedule\n';
|
||||
|
||||
if (!schedule) {
|
||||
throw new Error('No schedule found');
|
||||
}
|
||||
|
||||
schedule.courses.forEach(course => {
|
||||
course.schedule.meetings.forEach(meeting => {
|
||||
const { startTime, endTime, days, location } = meeting;
|
||||
@@ -85,7 +91,7 @@ export const saveAsCal = async () => {
|
||||
icsString += `DTEND:${endDate}\n`;
|
||||
icsString += `RRULE:FREQ=WEEKLY;BYDAY=${icsDays}\n`;
|
||||
icsString += `SUMMARY:${course.fullName}\n`;
|
||||
icsString += `LOCATION:${location.building} ${location.room}\n`;
|
||||
icsString += `LOCATION:${location?.building ?? ''} ${location?.room ?? ''}\n`;
|
||||
icsString += `END:VEVENT\n`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,7 +63,7 @@ export function Button({
|
||||
disabled={disabled}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
>
|
||||
{icon && <Icon className='h-6 w-6' />}
|
||||
{Icon && <Icon className='h-6 w-6' />}
|
||||
{!isIconOnly && (
|
||||
<Text variant='h4' className='translate-y-0.08'>
|
||||
{children}
|
||||
|
||||
@@ -9,7 +9,7 @@ import styles from './Card.module.scss';
|
||||
export type Props = {
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
onClick?: (...args) => void;
|
||||
onClick?: (...args: unknown[]) => void;
|
||||
children?: React.ReactNode;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export function Chip({ label }: React.PropsWithChildren<Props>): JSX.Element {
|
||||
style={{
|
||||
backgroundColor: '#FFD600',
|
||||
}}
|
||||
title={Object.keys(flagMap).find(key => flagMap[key] === label)}
|
||||
title={Object.entries(flagMap).find(([full, short]) => short === label)![0]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
|
||||
@@ -6,7 +6,7 @@ import React, { Fragment } from 'react';
|
||||
|
||||
import ExtensionRoot from '../ExtensionRoot/ExtensionRoot';
|
||||
|
||||
interface _DialogProps {
|
||||
export interface _DialogProps {
|
||||
className?: string;
|
||||
title?: JSX.Element;
|
||||
description?: JSX.Element;
|
||||
@@ -21,7 +21,7 @@ export type DialogProps = _DialogProps & Omit<TransitionRootProps<typeof HDialog
|
||||
* A reusable popup component that can be used to display content on the page
|
||||
*/
|
||||
export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Element {
|
||||
const { children, className, open, onTransitionEnd, ...rest } = props;
|
||||
const { children, className, open, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Transition show={open} as={HDialog} {...rest}>
|
||||
|
||||
@@ -34,8 +34,10 @@ export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JS
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={clsx(styles.extensionRoot, props.className)} data-testid={props.testId}>
|
||||
{props.children}
|
||||
</div>
|
||||
<React.StrictMode>
|
||||
<div className={clsx(styles.extensionRoot, props.className)} data-testid={props.testId}>
|
||||
{props.children}
|
||||
</div>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.link {
|
||||
font-family: 'Inter', sans-serif;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: not-allowed !important;
|
||||
text-decoration: none !important;
|
||||
color: #999999 !important;
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import clsx from 'clsx';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Link.module.scss';
|
||||
|
||||
type Props = TextProps<'a'> & {
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
@@ -19,7 +17,10 @@ export default function Link(props: PropsWithChildren<Props>): JSX.Element {
|
||||
let { className, href, ...passedProps } = props;
|
||||
|
||||
if (href && !props.onClick) {
|
||||
passedProps.onClick = () => background.openNewTab({ url: href });
|
||||
passedProps.onClick = e => {
|
||||
e.preventDefault();
|
||||
background.openNewTab({ url: href });
|
||||
};
|
||||
}
|
||||
const isDisabled = props.disabled || (!href && !props.onClick);
|
||||
|
||||
@@ -28,11 +29,13 @@ export default function Link(props: PropsWithChildren<Props>): JSX.Element {
|
||||
color='bluebonnet'
|
||||
{...passedProps}
|
||||
as='a'
|
||||
aria-disabled={isDisabled}
|
||||
href={!isDisabled ? href : undefined}
|
||||
tabIndex={isDisabled ? -1 : 0}
|
||||
className={clsx(
|
||||
styles.link,
|
||||
{
|
||||
[styles.disabled]: isDisabled,
|
||||
'underline cursor-pointer': !isDisabled,
|
||||
'cursor-not-allowed color-ut-gray': isDisabled,
|
||||
},
|
||||
props.className
|
||||
)}
|
||||
|
||||
@@ -31,7 +31,10 @@ function reorder<T>(list: T[], startIndex: number, endIndex: number) {
|
||||
const listCopy = [...list];
|
||||
|
||||
const [removed] = listCopy.splice(startIndex, 1);
|
||||
listCopy.splice(endIndex, 0, removed);
|
||||
if (removed) {
|
||||
listCopy.splice(endIndex, 0, removed);
|
||||
}
|
||||
|
||||
return listCopy;
|
||||
}
|
||||
|
||||
@@ -78,7 +81,7 @@ function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<
|
||||
// check if the draggables content has *actually* changed
|
||||
if (
|
||||
draggables.length === items.length &&
|
||||
draggables.every((element, index) => itemKey(element) === items[index].id)
|
||||
draggables.every((element, index) => itemKey(element) === items[index]?.id)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -86,12 +89,12 @@ function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<
|
||||
}, [draggables, itemKey, items]);
|
||||
|
||||
const onDragEnd: OnDragEndResponder = useCallback(
|
||||
result => {
|
||||
if (!result.destination) return;
|
||||
if (result.source.index === result.destination.index) return;
|
||||
({ destination, source }) => {
|
||||
if (!destination) return;
|
||||
if (source.index === destination.index) return;
|
||||
|
||||
// will reorder in place
|
||||
const reordered = reorder(items, result.source.index, result.destination.index);
|
||||
const reordered = reorder(items, source.index, destination.index);
|
||||
|
||||
setItems(reordered);
|
||||
onReordered(reordered.map(item => item.content));
|
||||
@@ -125,7 +128,7 @@ function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<
|
||||
}}
|
||||
>
|
||||
<ExtensionRoot>
|
||||
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
|
||||
{transformFunction(items[rubric.source.index]!.content, provided.dragHandleProps!)}
|
||||
</ExtensionRoot>
|
||||
</Item>
|
||||
);
|
||||
@@ -135,17 +138,22 @@ function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ marginBottom: `-${gap}px` }}>
|
||||
{items.map((item, index) => (
|
||||
<Draggable key={item.id} draggableId={item.id.toString()} index={index}>
|
||||
{draggableProvided => (
|
||||
{({ innerRef, draggableProps, dragHandleProps }) => (
|
||||
<div
|
||||
ref={draggableProvided.innerRef}
|
||||
{...draggableProvided.draggableProps}
|
||||
ref={innerRef}
|
||||
{...draggableProps}
|
||||
style={{
|
||||
...draggableProvided.draggableProps.style,
|
||||
...draggableProps.style,
|
||||
// if last item, don't add margin
|
||||
marginBottom: `${gap}px`,
|
||||
}}
|
||||
>
|
||||
{transformFunction(item.content, draggableProvided.dragHandleProps)}
|
||||
{
|
||||
transformFunction(
|
||||
item.content,
|
||||
dragHandleProps!
|
||||
) /* always exists; only doesn't when "isDragDisabled" is set */
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
|
||||
import { background } from '@shared/messages';
|
||||
import type { Course } from '@shared/types/Course';
|
||||
import { Status } from '@shared/types/Course';
|
||||
@@ -17,7 +18,7 @@ export interface PopupCourseBlockProps {
|
||||
className?: string;
|
||||
course: Course;
|
||||
colors: CourseColors;
|
||||
dragHandleProps?: any;
|
||||
dragHandleProps?: DraggableProvidedDragHandleProps;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,7 @@ import DropdownArrowUp from '~icons/material-symbols/arrow-drop-up';
|
||||
/**
|
||||
* Props for the Dropdown component.
|
||||
*/
|
||||
export type Props = {
|
||||
export type ScheduleDropdownProps = {
|
||||
defaultOpen?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export type Props = {
|
||||
/**
|
||||
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
||||
*/
|
||||
export default function ScheduleDropdown(props: Props) {
|
||||
export default function ScheduleDropdown(props: ScheduleDropdownProps) {
|
||||
const [activeSchedule] = useSchedules();
|
||||
|
||||
return (
|
||||
|
||||
@@ -59,7 +59,7 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
|
||||
<div className='group flex flex-1 items-center overflow-x-hidden'>
|
||||
<div
|
||||
className='flex flex-grow items-center gap-1.5 overflow-x-hidden'
|
||||
onClick={(...e) => !isEditing && onClick(...e)}
|
||||
onClick={(...e) => !isEditing && onClick?.(...e)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
|
||||
@@ -16,7 +16,9 @@ type OurProps<TTag extends ReactTag> = {
|
||||
ref?: React.ForwardedRef<React.ElementRef<TTag>>;
|
||||
};
|
||||
|
||||
type AsProps<TTag extends ReactTag, TOverrides = {}> = CleanProps<TTag, keyof TOverrides> & OurProps<TTag> & TOverrides;
|
||||
type AsProps<TTag extends ReactTag, TOverrides = object> = CleanProps<TTag, keyof TOverrides> &
|
||||
OurProps<TTag> &
|
||||
TOverrides;
|
||||
|
||||
const variants = ['mini', 'small', 'p', 'h4', 'h3-course', 'h3', 'h2-course', 'h2', 'h1-course', 'h1'] as const;
|
||||
|
||||
@@ -25,14 +27,15 @@ type Variant = (typeof variants)[number];
|
||||
/**
|
||||
* Props for the Text component.
|
||||
*/
|
||||
export type TextProps<TTag extends ElementType = 'span'> = PropsOf<TTag>['className'] extends string
|
||||
? AsProps<
|
||||
TTag,
|
||||
{
|
||||
variant?: Variant;
|
||||
}
|
||||
>
|
||||
: never;
|
||||
export type TextProps<TTag extends ElementType = 'span'> =
|
||||
NonNullable<PropsOf<TTag>['className']> extends string
|
||||
? AsProps<
|
||||
TTag,
|
||||
{
|
||||
variant?: Variant;
|
||||
}
|
||||
>
|
||||
: never;
|
||||
|
||||
/**
|
||||
* A reusable Text component with props that build on top of the design system for the extension
|
||||
@@ -47,4 +50,4 @@ function Text<TTag extends ElementType = 'span'>(
|
||||
return <Comp className={mergedClassName} {...rest} ref={ref} />;
|
||||
}
|
||||
|
||||
export default React.forwardRef(Text) as typeof Text;
|
||||
export default React.forwardRef(Text) as <TTag extends ElementType = 'span'>(props: TextProps<TTag>) => JSX.Element;
|
||||
|
||||
@@ -21,7 +21,7 @@ type Props = {
|
||||
* This component is responsible for loading the next page of courses when the user scrolls to the bottom of the page.
|
||||
* @returns
|
||||
*/
|
||||
export default function AutoLoad({ addRows }: Props): JSX.Element {
|
||||
export default function AutoLoad({ addRows }: Props): JSX.Element | null {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
const [status, setStatus] = useState<AutoLoadStatusType>(AutoLoadStatus.IDLE);
|
||||
|
||||
|
||||
@@ -53,40 +53,41 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
||||
const [status, setStatus] = React.useState<DataStatusType>(DataStatus.LOADING);
|
||||
const ref = React.useRef<HighchartsReact.RefObject>(null);
|
||||
|
||||
const chartData = React.useMemo(() => {
|
||||
if (status === DataStatus.FOUND && distributions[semester]) {
|
||||
return Object.entries(distributions[semester]).map(([grade, count]) => ({
|
||||
y: count,
|
||||
color: GRADE_COLORS[grade as LetterGrade],
|
||||
}));
|
||||
}
|
||||
return Array(12).fill(0);
|
||||
}, [distributions, semester, status]);
|
||||
// const chartData = React.useMemo(() => {
|
||||
// if (status === DataStatus.FOUND && distributions[semester]) {
|
||||
// return Object.entries(distributions[semester]).map(([grade, count]) => ({
|
||||
// y: count,
|
||||
// color: GRADE_COLORS[grade as LetterGrade],
|
||||
// }));
|
||||
// }
|
||||
// return Array(12).fill(0);
|
||||
// }, [distributions, semester, status]);
|
||||
// const chartData: unknown[] = [];
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchInitialData = async () => {
|
||||
try {
|
||||
const [aggregateDist, semesters] = await queryAggregateDistribution(course);
|
||||
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
|
||||
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
|
||||
const semesterDistributions = await Promise.all(semesterPromises);
|
||||
semesters.forEach((semester, i) => {
|
||||
initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
|
||||
});
|
||||
setDistributions(initialDistributions);
|
||||
setStatus(DataStatus.FOUND);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (e instanceof NoDataError) {
|
||||
setStatus(DataStatus.NOT_FOUND);
|
||||
} else {
|
||||
setStatus(DataStatus.ERROR);
|
||||
}
|
||||
}
|
||||
};
|
||||
// React.useEffect(() => {
|
||||
// const fetchInitialData = async () => {
|
||||
// try {
|
||||
// const [aggregateDist, semesters] = await queryAggregateDistribution(course);
|
||||
// const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
|
||||
// const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
|
||||
// const semesterDistributions = await Promise.all(semesterPromises);
|
||||
// semesters.forEach((semester, i) => {
|
||||
// initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
|
||||
// });
|
||||
// setDistributions(initialDistributions);
|
||||
// setStatus(DataStatus.FOUND);
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// if (e instanceof NoDataError) {
|
||||
// setStatus(DataStatus.NOT_FOUND);
|
||||
// } else {
|
||||
// setStatus(DataStatus.ERROR);
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
fetchInitialData();
|
||||
}, [course]);
|
||||
// fetchInitialData();
|
||||
// }, [course]);
|
||||
|
||||
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setSemester(event.target.value);
|
||||
@@ -129,7 +130,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
||||
{
|
||||
type: 'column',
|
||||
name: 'Grades',
|
||||
data: chartData,
|
||||
// data: chartData,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -156,7 +157,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
||||
<>
|
||||
<div className='w-full flex items-center justify-center gap-[12px]'>
|
||||
<Text variant='p'>Grade distribution for {`${course.department} ${course.number}`}</Text>
|
||||
<select
|
||||
{/* <select
|
||||
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
|
||||
onChange={handleSelectSemester}
|
||||
>
|
||||
@@ -180,7 +181,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
|
||||
{semester}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</select> */}
|
||||
</div>
|
||||
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
|
||||
</>
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
const formattedUniqueId = uniqueId.toString().padStart(5, '0');
|
||||
|
||||
const getInstructorFullName = (instructor: Instructor) => {
|
||||
const { firstName, lastName } = instructor;
|
||||
const { firstName = '', lastName = '' } = instructor;
|
||||
if (firstName === '') return capitalizeString(lastName);
|
||||
return `${capitalizeString(firstName)} ${capitalizeString(lastName)}`;
|
||||
};
|
||||
@@ -76,7 +76,7 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
|
||||
const handleOpenCES = async () => {
|
||||
const openTabs = instructors.map(instructor => {
|
||||
let { firstName, lastName } = instructor;
|
||||
let { firstName = '', lastName = '' } = instructor;
|
||||
firstName = capitalizeString(firstName);
|
||||
lastName = capitalizeString(lastName);
|
||||
return openCESPage({ instructorFirstName: firstName, instructorLastName: lastName });
|
||||
@@ -134,9 +134,12 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
.flatMap((el, i) => (i === 0 ? [el] : [', ', el]))}
|
||||
</Text>
|
||||
)}
|
||||
<div className='flex gap-1'>
|
||||
{flags.map(flag => (
|
||||
<Chip key={flagMap[flag]} label={flagMap[flag]} />
|
||||
<div className='flex items-center gap-1'>
|
||||
{flags.map((flag: string) => (
|
||||
<Chip
|
||||
key={flagMap[flag as keyof typeof flagMap]}
|
||||
label={flagMap[flag as keyof typeof flagMap]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -145,7 +148,11 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
const daysString = meeting.getDaysString({ format: 'long', separator: 'long' });
|
||||
const timeString = meeting.getTimeString({ separator: ' to ', capitalize: false });
|
||||
return (
|
||||
<Text key={daysString + timeString + meeting.location.building} variant='h4' as='p'>
|
||||
<Text
|
||||
key={daysString + timeString + (meeting.location?.building ?? '')}
|
||||
variant='h4'
|
||||
as='p'
|
||||
>
|
||||
{daysString} {timeString}
|
||||
{meeting.location && (
|
||||
<>
|
||||
|
||||
@@ -14,7 +14,7 @@ const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD'];
|
||||
* This adds a new column to the course catalog table header.
|
||||
* @returns a react portal to the new column or null if the column has not been created yet.
|
||||
*/
|
||||
export default function RecruitmentBanner(): JSX.Element {
|
||||
export default function RecruitmentBanner(): JSX.Element | null {
|
||||
const [container, setContainer] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -12,7 +12,7 @@ import styles from './TableRow.module.scss';
|
||||
interface Props {
|
||||
isSelected: boolean;
|
||||
row: ScrapedRow;
|
||||
onClick: (...args: any[]) => any;
|
||||
onClick: (...args: unknown[]) => unknown;
|
||||
activeSchedule?: UserSchedule;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
||||
const { element, course } = row;
|
||||
|
||||
useEffect(() => {
|
||||
element.classList.add(styles.row);
|
||||
element.classList.add(styles.row!);
|
||||
element.classList.add('group');
|
||||
const portalContainer = document.createElement('td');
|
||||
// portalContainer.style.textAlign = 'right';
|
||||
@@ -39,12 +39,12 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
||||
|
||||
return () => {
|
||||
portalContainer.remove();
|
||||
element.classList.remove(styles.row);
|
||||
element.classList.remove(styles.row!);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
useEffect(() => {
|
||||
element.classList[isSelected ? 'add' : 'remove'](styles.selectedRow);
|
||||
element.classList[isSelected ? 'add' : 'remove'](styles.selectedRow!);
|
||||
}, [isSelected, element.classList]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -52,10 +52,10 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
||||
|
||||
const isInSchedule = activeSchedule.containsCourse(course);
|
||||
|
||||
element.classList[isInSchedule ? 'add' : 'remove'](styles.inActiveSchedule);
|
||||
element.classList[isInSchedule ? 'add' : 'remove'](styles.inActiveSchedule!);
|
||||
|
||||
return () => {
|
||||
element.classList.remove(styles.inActiveSchedule);
|
||||
element.classList.remove(styles.inActiveSchedule!);
|
||||
};
|
||||
}, [activeSchedule, course, element.classList]);
|
||||
|
||||
@@ -72,11 +72,11 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
|
||||
}
|
||||
}
|
||||
|
||||
element.classList[conflicts.length ? 'add' : 'remove'](styles.isConflict);
|
||||
element.classList[conflicts.length ? 'add' : 'remove'](styles.isConflict!);
|
||||
setConflicts(conflicts);
|
||||
|
||||
return () => {
|
||||
element.classList.remove(styles.isConflict);
|
||||
element.classList.remove(styles.isConflict!);
|
||||
setConflicts([]);
|
||||
};
|
||||
}, [activeSchedule, course, element.classList]);
|
||||
|
||||
@@ -15,10 +15,10 @@ export default function TableSubheading({ row }: Props): JSX.Element | null {
|
||||
const { element } = row;
|
||||
|
||||
useEffect(() => {
|
||||
element.classList.add(styles.subheader);
|
||||
element.classList.add(styles.subheader!);
|
||||
|
||||
return () => {
|
||||
element.classList.remove(styles.subheader);
|
||||
element.classList.remove(styles.subheader!);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user