diff --git a/package.json b/package.json index 4cda8c17..3a3cd1b5 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "clsx": "^2.1.0", "highcharts": "^11.3.0", "highcharts-react-official": "^3.2.1", + "html2canvas": "^1.4.1", "react": "^18.2.0", "react-devtools-core": "^5.0.0", "react-dom": "^18.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7795c182..452e870f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ dependencies: highcharts-react-official: specifier: ^3.2.1 version: 3.2.1(highcharts@11.3.0)(react@18.2.0) + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -5853,6 +5856,11 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true @@ -6545,6 +6553,12 @@ packages: postcss: 8.4.35 dev: true + /css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + dependencies: + utrie: 1.0.2 + dev: false + /css-select@5.1.0: resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} dependencies: @@ -8649,6 +8663,14 @@ packages: engines: {node: '>=8'} dev: true + /html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + dev: false + /htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} dependencies: @@ -12408,6 +12430,12 @@ packages: minimatch: 3.1.2 dev: true + /text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + dependencies: + utrie: 1.0.2 + dev: false + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true @@ -12950,6 +12978,12 @@ packages: engines: {node: '>= 0.4.0'} dev: true + /utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + dependencies: + base64-arraybuffer: 1.0.2 + dev: false + /uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true diff --git a/src/stories/components/CalendarBottomBar.stories.tsx b/src/stories/components/CalendarBottomBar.stories.tsx new file mode 100644 index 00000000..a1f1c5c7 --- /dev/null +++ b/src/stories/components/CalendarBottomBar.stories.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { Meta, StoryObj } from '@storybook/react'; +import { Course, Status } from '@shared/types/Course'; +import Instructor from '@shared/types/Instructor'; +import { CalendarBottomBar } from '@views/components/common/CalendarBottomBar/CalendarBottomBar'; +import { getCourseColors } from '../../shared/util/colors'; + +const exampleGovCourse: Course = new Course({ + courseName: 'Nope', + creditHours: 3, + department: 'GOV', + description: ['nah', 'aint typing this', 'corndog'], + flags: ['no flag for you >:)'], + fullName: 'GOV 312L Something something', + instructionMode: 'Online', + instructors: [ + new Instructor({ + firstName: 'Bevo', + lastName: 'Barrymore', + fullName: 'Bevo Barrymore', + }), + ], + isReserved: false, + number: '312L', + schedule: { + meetings: [], + }, + semester: { + code: '12345', + season: 'Spring', + year: 2024, + }, + status: Status.OPEN, + uniqueId: 12345, + url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/', +}); + +const examplePsyCourse: Course = new Course({ + courseName: 'Nope Again', + creditHours: 3, + department: 'PSY', + description: ['nah', 'aint typing this', 'corndog'], + flags: ['no flag for you >:)'], + fullName: 'PSY 317L Yada yada', + instructionMode: 'Online', + instructors: [ + new Instructor({ + firstName: 'Bevo', + lastName: 'Etz', + fullName: 'Bevo Etz', + }), + ], + isReserved: false, + number: '317L', + schedule: { + meetings: [], + }, + semester: { + code: '12346', + season: 'Spring', + year: 2024, + }, + status: Status.CLOSED, + uniqueId: 12346, + url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/', +}); + +const meta = { + title: 'Components/Common/CalendarBottomBar', + component: CalendarBottomBar, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: {}, +} satisfies Meta; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + courses: [ + { + colors: getCourseColors('pink', 200), + courseDeptAndInstr: `${exampleGovCourse.department} ${exampleGovCourse.number} – ${exampleGovCourse.instructors[0].lastName}`, + status: exampleGovCourse.status, + }, + { + colors: getCourseColors('slate', 500), + courseDeptAndInstr: `${examplePsyCourse.department} ${examplePsyCourse.number} – ${examplePsyCourse.instructors[0].lastName}`, + status: examplePsyCourse.status, + }, + ], + }, + render: props => ( +
+ +
+ ), +}; diff --git a/src/stories/components/CalendarHeader.stories.tsx b/src/stories/components/CalendarHeader.stories.tsx index 6833b7b1..047d8694 100644 --- a/src/stories/components/CalendarHeader.stories.tsx +++ b/src/stories/components/CalendarHeader.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from '@storybook/react'; import CalendarHeader from '@views/components/common/CalendarHeader/CalenderHeader'; const meta = { - title: 'Components/CalendarHeader', + title: 'Components/Common/CalendarHeader', component: CalendarHeader, parameters: { layout: 'centered', diff --git a/src/views/components/ImportantLinks.tsx b/src/views/components/ImportantLinks.tsx index 5e7c52d1..f301a54d 100644 --- a/src/views/components/ImportantLinks.tsx +++ b/src/views/components/ImportantLinks.tsx @@ -17,7 +17,7 @@ export default function ImportantLinks({ className }: Props) { Important Links @@ -26,7 +26,7 @@ export default function ImportantLinks({ className }: Props) { @@ -35,7 +35,7 @@ export default function ImportantLinks({ className }: Props) { @@ -44,7 +44,7 @@ export default function ImportantLinks({ className }: Props) { @@ -53,7 +53,7 @@ export default function ImportantLinks({ className }: Props) { diff --git a/src/views/components/common/CalendarBottomBar/CalendarBottomBar.tsx b/src/views/components/common/CalendarBottomBar/CalendarBottomBar.tsx new file mode 100644 index 00000000..5b99ab8d --- /dev/null +++ b/src/views/components/common/CalendarBottomBar/CalendarBottomBar.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import clsx from 'clsx'; +import Text from '../Text/Text'; +import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell'; +import { Button } from '../Button/Button'; +import ImageIcon from '~icons/material-symbols/image'; +import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; + +type CalendarBottomBarProps = { + courses: CalendarCourseCellProps[]; +}; + +/** + * + */ +export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => { + if (courses.length === -1) console.log('foo'); // dumb line to make eslint happy + return ( +
+
+ Async. and Other: +
+ {courses.map(course => ( + + ))} +
+
+
+ + +
+
+ ); +}; diff --git a/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx b/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx index e67d3484..1ea60c25 100644 --- a/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx +++ b/src/views/components/common/CalendarCourseCell/CalendarCourseCell.tsx @@ -12,6 +12,7 @@ export interface CalendarCourseCellProps { timeAndLocation?: string; status: Status; colors: CourseColors; + className?: string; } const CalendarCourseCell: React.FC = ({ @@ -19,6 +20,7 @@ const CalendarCourseCell: React.FC = ({ timeAndLocation, status, colors, + className, }: CalendarCourseCellProps) => { let rightIcon: React.ReactNode | null = null; if (status === Status.WAITLISTED) { @@ -34,7 +36,7 @@ const CalendarCourseCell: React.FC = ({ return (
!['S', 'SU'].includes(key)); const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8); @@ -34,13 +36,27 @@ interface Props { * Grid of CalendarGridCell components forming the user's course schedule calendar view * @param props */ -function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren ): JSX.Element { +function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren): JSX.Element { const [iterator, setIterator] = useState(0); + const calendarRef = useRef(null); // Create a ref for the calendar grid + + const saveAsPNG = () => { + if (calendarRef.current) { + html2canvas(calendarRef.current).then(canvas => { + // Create an a element to trigger download + const a = document.createElement('a'); + a.href = canvas.toDataURL('image/png'); + a.download = 'calendar.png'; + a.click(); + }); + } + }; + return (
{/* Displaying the rest of the calendar */} -
+
{/*
{hoursOfDay.map((hour) => ( @@ -81,18 +97,18 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren
- {/*
-
+
+
{/* First divider */} -
- -
*/} +
); } diff --git a/src/views/components/common/CalendarHeader/CalenderHeader.tsx b/src/views/components/common/CalendarHeader/CalenderHeader.tsx index b5d07274..7f11d348 100644 --- a/src/views/components/common/CalendarHeader/CalenderHeader.tsx +++ b/src/views/components/common/CalendarHeader/CalenderHeader.tsx @@ -12,40 +12,41 @@ import ScheduleTotalHoursAndCourses from '../ScheduleTotalHoursAndCourses/Schedu import CourseStatus from '../CourseStatus/CourseStatus'; const CalendarHeader = () => ( -
-
+
+ + DATA UPDATED ON: 12:00 AM 02/01/2024 +
+
+
+
+ + + +
+
+
+
- - - - - - - -
-
- -
); diff --git a/src/views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses.tsx b/src/views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses.tsx index fd91d1f7..11e6a5b4 100644 --- a/src/views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses.tsx +++ b/src/views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses.tsx @@ -21,7 +21,7 @@ export default function ScheduleTotalHoursAndCourses({ totalCourses, }: ScheduleTotalHoursAndCoursesProps): JSX.Element { return ( -
+
{`${scheduleName}: `} diff --git a/src/views/hooks/useFlattenedCourseSchedule.ts b/src/views/hooks/useFlattenedCourseSchedule.ts index 60f52cdc..da1a3bb3 100644 --- a/src/views/hooks/useFlattenedCourseSchedule.ts +++ b/src/views/hooks/useFlattenedCourseSchedule.ts @@ -1,7 +1,7 @@ import { CalendarCourseCellProps } from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell'; import useSchedules from './useSchedules'; -const dayToNumber = { +const dayToNumber: { [day: string]: number } = { Monday: 0, Tuesday: 1, Wednesday: 2, @@ -15,71 +15,88 @@ interface CalendarGridPoint { endIndex: number; } +/** + * Return type of useFlattenedCourseSchedule + */ export interface CalendarGridCourse { - calendarGridPoint?: CalendarGridPoint; + calendarGridPoint: CalendarGridPoint; componentProps: CalendarCourseCellProps; } const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30); -export function useFlattenedCourseSchedule() { +/** + * Get the active schedule, and convert it to be render-able into a calendar. + * @returns CalendarGridCourse + */ +export function useFlattenedCourseSchedule(): CalendarGridCourse[] { const [activeSchedule] = useSchedules(); const { courses } = activeSchedule; - const out = courses.flatMap(course => { - const { - status, - department, - instructors, - schedule: { meetings }, - } = course; - const courseDeptAndInstr = `${department} ${instructors[0].lastName}`; + return courses + .flatMap(course => { + const { + status, + department, + instructors, + schedule: { meetings }, + } = course; + const courseDeptAndInstr = `${department} ${instructors[0].lastName}`; - if (meetings.length === 0) { - // asynch, online course - return [ - { - componentProps: { - courseDeptAndInstr, - status, - colors: { - primaryColor: 'ut-gray', - secondaryColor: 'ut-gray', + if (meetings.length === 0) { + // asynch, online course + return [ + { + calendarGridPoint: { + dayIndex: 0, + startIndex: 0, + endIndex: 0, + }, + componentProps: { + courseDeptAndInstr, + status, + colors: { + // TODO: figure out colors - these are defaults + primaryColor: 'ut-gray', + secondaryColor: 'ut-gray', + }, }, }, - }, - ]; - } - return meetings.flatMap(meeting => { - const { days, startTime, endTime, location } = meeting; - const time = meeting.getTimeString({ separator: ' - ', capitalize: true }); - const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`; + ]; + } - return days.map(d => { - const dayIndex = dayToNumber[d]; - const startIndex = convertMinutesToIndex(startTime); - const endIndex = convertMinutesToIndex(endTime); - const calendarGridPoint: CalendarGridPoint = { - dayIndex, - startIndex, - endIndex, - }; + // in-person + return meetings.flatMap(meeting => { + const { days, startTime, endTime, location } = meeting; + const time = meeting.getTimeString({ separator: '-', capitalize: true }); + const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`; - return { - calendarGridPoint, + return days.map(d => ({ + calendarGridPoint: { + dayIndex: dayToNumber[d], + startIndex: convertMinutesToIndex(startTime), + endIndex: convertMinutesToIndex(endTime), + }, componentProps: { courseDeptAndInstr, timeAndLocation, status, colors: { + // TODO: figure out colors - these are defaults primaryColor: 'ut-orange', secondaryColor: 'ut-orange', }, }, - }; + })); }); + }) + .sort((a: CalendarGridCourse, b: CalendarGridCourse) => { + if (a.calendarGridPoint.dayIndex !== b.calendarGridPoint.dayIndex) { + return a.calendarGridPoint.dayIndex - b.calendarGridPoint.dayIndex; + } + if (a.calendarGridPoint.startIndex !== b.calendarGridPoint.startIndex) { + return a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex; + } + return a.calendarGridPoint.endIndex - b.calendarGridPoint.endIndex; }); - }); - - return out; }