diff --git a/package.json b/package.json index 3dc79fb5..73dca272 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-beautiful-dnd": "^13.1.1", "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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 667c2f51..9bfc12db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ dependencies: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + react-window: + specifier: ^1.8.10 + version: 1.8.10(react-dom@18.2.0)(react@18.2.0) sass: specifier: ^1.70.0 version: 1.70.0 @@ -10743,6 +10746,19 @@ packages: tslib: 2.6.2 dev: true + /react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.9 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react@18.2.0: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} diff --git a/src/stories/components/List.stories.tsx b/src/stories/components/List.stories.tsx index f6a4b3d0..ec19c075 100644 --- a/src/stories/components/List.stories.tsx +++ b/src/stories/components/List.stories.tsx @@ -1,43 +1,76 @@ import React from 'react'; import { Meta, StoryObj } from '@storybook/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 List from 'src/views/components/common/List/List'; -import CalendarCourseMeeting from 'src/views/components/common/CalendarCourseBlock/CalendarCourseMeeting'; +import { Course, Status } from '@shared/types/Course'; +import { CourseMeeting } from '@shared/types/CourseMeeting'; +import Instructor from '@shared/types/Instructor'; +import List from '@views/components/common/List/List'; +import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock'; +import { getCourseColors } from 'src/shared/util/colors'; +import { test_colors } from './PopupCourseBlock.stories'; -const placeholderCourse: 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', - }, - }), + +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.', ], - }), - instructionMode: 'In Person', - semester: { - year: 2024, - season: 'Spring', - }, -}); + 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) => { + return exampleCourses.map((course, i) => ( + + )) +} +const exampleCourseBlocks = generateCourseBlocks(exampleCourses, test_colors); + const meta = { title: 'Components/Common/List', @@ -48,6 +81,10 @@ const meta = { tags: ['autodocs'], argTypes: { draggableElements: { control: 'object' }, + itemHeight: { control: 'number' }, + listHeight: { control: 'number' }, + listWidth: { control: 'number' }, + gap: { control: 'number' }, }, } satisfies Meta; export default meta; @@ -56,12 +93,10 @@ type Story = StoryObj; export const Default: Story = { args: { - draggableElements: [ - , - , - , - , - - ], + draggableElements: exampleCourseBlocks, + itemHeight: 55, + listHeight: 300, + listWidth: 300, + gap: 8, }, }; \ No newline at end of file diff --git a/src/stories/components/PopupCourseBlock.stories.tsx b/src/stories/components/PopupCourseBlock.stories.tsx index 636617f5..adda6062 100644 --- a/src/stories/components/PopupCourseBlock.stories.tsx +++ b/src/stories/components/PopupCourseBlock.stories.tsx @@ -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 => (
- {colors.map((color, i) => ( + {test_colors.map((color, i) => ( ))}
diff --git a/src/views/components/common/List/List.tsx b/src/views/components/common/List/List.tsx index 5dc1bb9f..cc95737d 100644 --- a/src/views/components/common/List/List.tsx +++ b/src/views/components/common/List/List.tsx @@ -1,8 +1,12 @@ import React from 'react'; import { useState } from 'react'; -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; -import { FixedSizeList, areEqual } from "react-window"; +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; +import { FixedSizeList, areEqual } from 'react-window'; +import { ReactElement } from 'react'; +/*Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code) + * + */ /** * Props for the List component. @@ -10,48 +14,45 @@ import { FixedSizeList, areEqual } from "react-window"; 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 + content: element as ReactElement, })); } -function reorder(list, startIndex: number, endIndex: number) { +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 }) { +function getStyle({ provided, style, isDragging, gap }) { const combined = { ...style, ...provided.draggableProps.style }; - - const marginBottom = 8; - const withSpacing = { - ...combined, - height: isDragging ? combined.height : combined.height - marginBottom, - marginBottom - }; - return withSpacing; - //return style; + + return combined; } -function Item({ provided, item, style, isDragging }) { +function Item({ provided, item, style, isDragging, gap }) { return (
- {item.content} + {React.cloneElement(item.content, {dragHandleProps: provided.dragHandleProps})}
); } @@ -59,15 +60,20 @@ function Item({ provided, item, style, isDragging }) { interface RowProps { data: any, //DraggableElements[]; Need to define DraggableElements interface once those components are ready index: number, - style: any + style: React.CSSProperties, } -const Row: React.FC = React.memo(({ data: items, index, style }) => { +const Row: React.FC = 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 ( {/*@ts-ignore*/} - {provided => } + {provided => } ); }, areEqual); @@ -78,15 +84,14 @@ const Row: React.FC = React.memo(({ data: items, index, style }) => { * @example * */ -const List: React.FC = ({ - draggableElements -}: ListProps) => { +const List: React.FC = ( { draggableElements, itemHeight, listHeight, listWidth, gap=8 }: ListProps) => { const [items, setItems] = useState(() => initial(draggableElements)); function onDragEnd(result) { if (!result.destination) { return; } + if (result.source.index === result.destination.index) { return; } @@ -96,10 +101,11 @@ const List: React.FC = ({ result.source.index, result.destination.index ); - setItems(newItems as {id: string, content: any}[]); + setItems(newItems as {id: string, content: ReactElement}[]); } return ( +
= ({ .split(',') .map((v) => parseInt(v, 10)); - style.transform = `translate(0px, ${y}px)`; + const minTranslateY = -1 * rubric.source.index * itemHeight; + const maxTranslateY = (items.length - rubric.source.index - 1) * itemHeight; + if (y < minTranslateY) { + + } + else if (y > maxTranslateY) { + + } + else { + + } + + style.transform = `translate(0px, ${y}px)`; // Apply constrained y value } - - style.fontFamily = "Inter"; - + return ( = ({ style = {{ style }} + gap = {gap} /> )} } > {provided => ( {Row} @@ -153,7 +170,8 @@ const List: React.FC = ({
+
); }; -export default List; +export default List; \ No newline at end of file diff --git a/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx b/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx index d1389649..2d703caa 100644 --- a/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx +++ b/src/views/components/common/PopupCourseBlock/PopupCourseBlock.tsx @@ -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} >