Merge branch 'NonVirtualList' into hackathon

This commit is contained in:
knownotunknown
2024-02-17 10:49:39 -06:00
9 changed files with 1604 additions and 717 deletions

View File

@@ -17,6 +17,7 @@
"build-storybook": "storybook build"
},
"dependencies": {
"@hello-pangea/dnd": "^16.5.0",
"@types/sql.js": "^1.4.9",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.51",
@@ -26,6 +27,7 @@
"react": "^18.2.0",
"react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0",
"react-window": "^1.8.10",
"sass": "^1.70.0",
"sql.js": "1.10.2",
"uuid": "^9.0.1"
@@ -34,30 +36,30 @@
"@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/material-symbols": "^1.1.72",
"@storybook/addon-designs": "^7.0.9",
"@storybook/addon-essentials": "^7.6.12",
"@storybook/addon-links": "^7.6.12",
"@storybook/blocks": "^7.6.12",
"@storybook/react": "^7.6.12",
"@storybook/react-vite": "^7.6.12",
"@storybook/test": "^7.6.12",
"@storybook/addon-essentials": "^7.6.13",
"@storybook/addon-links": "^7.6.13",
"@storybook/blocks": "^7.6.13",
"@storybook/react": "^7.6.13",
"@storybook/react-vite": "^7.6.13",
"@storybook/test": "^7.6.13",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.260",
"@types/node": "^20.11.16",
"@types/node": "^20.11.17",
"@types/prompts": "^2.4.9",
"@types/react": "^18.2.51",
"@types/react-dom": "^18.2.18",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@types/semver": "^7.5.6",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@unocss/eslint-config": "^0.58.4",
"@unocss/postcss": "^0.58.4",
"@unocss/preset-uno": "^0.58.4",
"@unocss/preset-web-fonts": "^0.58.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@unocss/eslint-config": "^0.58.5",
"@unocss/postcss": "^0.58.5",
"@unocss/preset-uno": "^0.58.5",
"@unocss/preset-web-fonts": "^0.58.5",
"@unocss/reset": "^0.58.5",
"@unocss/transformer-directives": "^0.58.4",
"@unocss/transformer-variant-group": "^0.58.4",
"@unocss/transformer-directives": "^0.58.5",
"@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-react-swc": "^3.6.0",
"chromatic": "^10.9.1",
"cssnano": "^6.0.3",
@@ -71,7 +73,7 @@
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.0.4",
"eslint-plugin-jsdoc": "^48.0.6",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
@@ -79,15 +81,15 @@
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-storybook": "^0.6.15",
"path": "^0.12.7",
"postcss": "^8.4.33",
"prettier": "^3.2.4",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"react-dev-utils": "^12.0.1",
"react-devtools": "^5.0.0",
"storybook": "^7.6.12",
"storybook": "^7.6.13",
"typescript": "^5.3.3",
"unocss": "^0.58.4",
"unplugin-icons": "^0.18.3",
"vite": "^5.0.12",
"unocss": "^0.58.5",
"unplugin-icons": "^0.18.5",
"vite": "^5.1.1",
"vite-plugin-inspect": "^0.8.3"
},
"pnpm": {

1854
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,62 @@
import { Meta, StoryObj } from '@storybook/react';
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor';
import CalendarCourse from '@views/components/common/CalendarCourseBlock/CalendarCourseMeeting';
const meta = {
title: 'Components/Common/CalendarCourseMeeting',
component: CalendarCourse,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
course: { control: 'object' },
meetingIdx: { control: 'number' },
color: { control: 'color' },
rightIcon: { control: 'object' },
},
} satisfies Meta<typeof CalendarCourse>;
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.OPEN,
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

@@ -0,0 +1,96 @@
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor';
import { Meta, StoryObj } from '@storybook/react';
import List from '@views/components/common/List/List';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import React from 'react';
import { test_colors } from './PopupCourseBlock.stories';
const numberOfCourses = 5;
const generateCourses = count => {
const courses = [];
for (let i = 0; i < count; i++) {
const 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 + i, // Make uniqueId different for each course
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
courses.push(course);
}
return courses;
};
const exampleCourses = generateCourses(numberOfCourses);
const generateCourseBlocks = (exampleCourses, colors) =>
exampleCourses.map((course, i) => <PopupCourseBlock key={course.uniqueId} course={course} colors={colors[i]} />);
const exampleCourseBlocks = generateCourseBlocks(exampleCourses, test_colors);
const meta = {
title: 'Components/Common/List',
component: List,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
draggableElements: { control: 'object' },
itemHeight: { control: 'number' },
listHeight: { control: 'number' },
listWidth: { control: 'number' },
gap: { control: 'number' },
},
} satisfies Meta<typeof List>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
draggableElements: exampleCourseBlocks,
itemHeight: 55,
listHeight: 300,
listWidth: 300,
gap: 12,
},
};

View File

@@ -95,7 +95,7 @@ export const Variants: Story = {
),
};
const colors = Object.keys(theme.colors)
export const test_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)
@@ -104,7 +104,7 @@ const colors = Object.keys(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) => (
{test_colors.map((color, i) => (
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
))}
</div>

View File

@@ -0,0 +1,34 @@
.component {
display: flex;
padding: 7px 7px 9px 7px;
flex-direction: column;
align-items: flex-start;
gap: 5px;
flex: 1 0 0;
align-self: stretch;
border-radius: 4px;
background: #cbd5e1;
.content {
display: flex;
gap: 7px;
.course-detail {
display: flex;
flex-direction: column;
gap: 5px;
width: 154px;
.course {
font-size: 16px;
line-height: 16px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time-and-location {
font-size: 11px;
line-height: 11px;
font-weight: 400;
}
}
}
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import styles from './CalendarCourseMeeting.module.scss';
/**
* Props for the CalendarCourseMeeting component.
*/
export interface CalendarCourseMeetingProps {
/** 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;
/** 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} color="red" rightIcon={<Icon />} />
*/
const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
course,
meetingIdx,
}: CalendarCourseMeetingProps) => {
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : 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}
</div>
<div className={styles['time-and-location']}>
{`${meeting.getTimeString({ separator: '-', capitalize: true })}${
meeting.location ? ` - ${meeting.location.building}` : ''
}`}
</div>
</div>
</div>
</div>
);
};
export default CalendarCourseMeeting;

View File

@@ -0,0 +1,168 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import React, { ReactElement, useCallback, useState } from 'react';
import { areEqual } from 'react-window';
/*
* Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
*
*/
/**
* Props for the List component.
*/
export interface ListProps {
draggableElements: any[]; // Will later define draggableElements based on what types
// of components are draggable.
itemHeight: number;
listHeight: number;
listWidth: number;
gap: number; // Impacts the spacing between items in the list
}
function initial(draggableElements: any[] = []) {
return draggableElements.map((element, index) => ({
id: `id:${index}`,
content: element as ReactElement,
}));
}
function reorder(list: { id: string; content: ReactElement }[], startIndex: number, endIndex: number) {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
}
function getStyle({ provided, style /* , isDragging, gap */ }) {
const combined = {
...style,
...provided.draggableProps.style,
};
return combined;
}
function Item({ provided, item, style, isDragging /* , gap */ }) {
return (
<div
{...provided.draggableProps}
ref={provided.innerRef}
style={getStyle({ provided, style /* , isDragging, gap */ })}
className={`item ${isDragging ? 'is-dragging' : ''}`}
>
{React.cloneElement(item.content, { dragHandleProps: provided.dragHandleProps })}
</div>
);
}
interface RowProps {
data: any; // DraggableElements[]; Need to define DraggableElements interface once those components are ready
index: number;
style: React.CSSProperties;
}
const Row: React.FC<RowProps> = React.memo(({ data: { items, gap }, index, style }) => {
const item = items[index];
const adjustedStyle = {
...style,
height: `calc(${style.height}px - ${gap}px)`, // Reduce the height by gap to accommodate the margin
marginBottom: `${gap}px`, // Add gap as bottom margin
};
return (
<Draggable draggableId={item.id} index={index} key={item.id}>
{/* @ts-ignore */}
{provided => <Item provided={provided} item={item} style={adjustedStyle} gap={gap} />}
</Draggable>
);
}, areEqual);
/**
* `List` is a functional component that displays a course meeting.
*
* @example
* <List draggableElements={elements} />
*/
const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight, listWidth, gap = 12 }: ListProps) => {
const [items, setItems] = useState(() => initial(draggableElements));
const onDragEnd = useCallback(
result => {
if (!result.destination) {
return;
}
if (result.source.index === result.destination.index) {
return;
}
const newItems = reorder(items, result.source.index, result.destination.index);
setItems(newItems as { id: string; content: React.ReactElement }[]);
},
[items]
);
return (
<div style={{ overflow: 'hidden', width: listWidth }}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable
droppableId='droppable'
direction='vertical'
renderClone={(provided, snapshot, rubric) => {
let { style } = provided.draggableProps;
const transform = style?.transform;
if (snapshot.isDragging && transform) {
console.log(transform);
let [, _x, y] = transform.match(/translate\(([-\d]+)px, ([-\d]+)px\)/) || [];
style.transform = `translate3d(0px, ${y}px, 0px)`; // Apply constrained y value
}
return (
<Item
provided={provided}
isDragging={snapshot.isDragging}
item={items[rubric.source.index]}
style={{
style,
}}
/>
);
}}
>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={{ width: `${listWidth}px` }}
className=''
>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{draggableProvided => (
<div
ref={draggableProvided.innerRef}
{...draggableProvided.draggableProps}
{...draggableProvided.dragHandleProps}
style={{
...draggableProvided.draggableProps.style,
// if last item, don't add margin
marginBottom: index === items.length - 1 ? '0px' : `${gap}px`,
}}
>
{item.content}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
};
export default List;

View File

@@ -13,6 +13,7 @@ export interface PopupCourseBlockProps {
className?: string;
course: Course;
colors: CourseColors;
dragHandleProps?: any;
}
/**
@@ -20,7 +21,7 @@ export interface PopupCourseBlockProps {
*
* @param props PopupCourseBlockProps
*/
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element {
export default function PopupCourseBlock({ className, course, colors, dragHandleProps }: PopupCourseBlockProps): JSX.Element {
// whiteText based on secondaryColor
const fontColor = pickFontColor(colors.primaryColor);
@@ -36,6 +37,7 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
backgroundColor: colors.secondaryColor,
}}
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
{...dragHandleProps}
>
<DragIndicatorIcon className='h-6 w-6 text-white' />
</div>