From d9ee23c5bbe27240a0e2849aae2eae81d7960bb5 Mon Sep 17 00:00:00 2001 From: Lukas Zenick Date: Fri, 1 Mar 2024 14:06:05 -0600 Subject: [PATCH] feat: working PNG and CAL downloads (#119) * working save as PNG * cleanup * feat(cal): working ICS file --- .../calendar/CalendarBottomBar.stories.tsx | 1 + .../components/calendar/Calendar/Calendar.tsx | 7 +- .../CalendarBottomBar/CalendarBottomBar.tsx | 102 +++++++++++++++++- .../calendar/CalendarGrid/CalendarGrid.tsx | 2 + 4 files changed, 104 insertions(+), 8 deletions(-) diff --git a/src/stories/components/calendar/CalendarBottomBar.stories.tsx b/src/stories/components/calendar/CalendarBottomBar.stories.tsx index 10b90746..743841af 100644 --- a/src/stories/components/calendar/CalendarBottomBar.stories.tsx +++ b/src/stories/components/calendar/CalendarBottomBar.stories.tsx @@ -92,6 +92,7 @@ export const Default: Story = { status: examplePsyCourse.status, }, ], + calendarRef: { current: null }, }, render: props => (
diff --git a/src/views/components/calendar/Calendar/Calendar.tsx b/src/views/components/calendar/Calendar/Calendar.tsx index f32f4132..5f8bbcf3 100644 --- a/src/views/components/calendar/Calendar/Calendar.tsx +++ b/src/views/components/calendar/Calendar/Calendar.tsx @@ -6,7 +6,7 @@ import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/ import ImportantLinks from '@views/components/calendar/ImportantLinks'; import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup'; import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule'; -import React from 'react'; +import React, { useRef } from 'react'; import { ExampleCourse } from 'src/stories/components/PopupCourseBlock.stories'; export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II']; @@ -20,6 +20,7 @@ interface Props { * @returns */ export function Calendar(): JSX.Element { + const calendarRef = useRef(null); const { courseCells, activeSchedule } = useFlattenedCourseSchedule(); const [course, setCourse] = React.useState(null); @@ -38,11 +39,11 @@ export function Calendar(): JSX.Element {
-
+
- +
diff --git a/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx b/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx index e769836e..05190fb1 100644 --- a/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx +++ b/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx @@ -1,21 +1,113 @@ -import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell'; -import CalendarCourseBlock from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell'; +import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { Button } from '@views/components/common/Button/Button'; import Text from '@views/components/common/Text/Text'; import clsx from 'clsx'; +import { toPng } from 'html-to-image'; import React from 'react'; import CalendarMonthIcon from '~icons/material-symbols/calendar-month'; import ImageIcon from '~icons/material-symbols/image'; +import type { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell'; +import CalendarCourseBlock from '../CalendarCourseCell/CalendarCourseCell'; + +const CAL_MAP = { + Sunday: 'SU', + Monday: 'MO', + Tuesday: 'TU', + Wednesday: 'WE', + Thursday: 'TH', + Friday: 'FR', + Saturday: 'SA', +}; + type CalendarBottomBarProps = { courses?: CalendarCourseCellProps[]; + calendarRef: React.RefObject; }; +async function getSchedule() { + const schedules = await UserScheduleStore.get('schedules'); + const activeIndex = await UserScheduleStore.get('activeIndex'); + const schedule = schedules[activeIndex]; + return schedule; +} + /** * */ -export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => { +export const CalendarBottomBar = ({ courses, calendarRef }: CalendarBottomBarProps): JSX.Element => { + const saveAsPng = () => { + if (calendarRef.current) { + toPng(calendarRef.current, { cacheBust: true }) + .then(dataUrl => { + const link = document.createElement('a'); + link.download = 'my-calendar.png'; + link.href = dataUrl; + link.click(); + }) + .catch(err => { + console.log(err); + }); + } + }; + + function formatToHHMMSS(minutes) { + const hours = String(Math.floor(minutes / 60)).padStart(2, '0'); + const mins = String(minutes % 60).padStart(2, '0'); + return `${hours}${mins}00`; + } + + function downloadICS(data) { + const blob = new Blob([data], { type: 'text/calendar' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = 'schedule.ics'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + const saveAsCal = async () => { + const schedule = await getSchedule(); // Assumes this fetches the current active schedule + + let icsString = 'BEGIN:VCALENDAR\nVERSION:2.0\nCALSCALE:GREGORIAN\nX-WR-CALNAME:My Schedule\n'; + + schedule.courses.forEach(course => { + course.schedule.meetings.forEach(meeting => { + const { startTime, endTime, days, location } = meeting; + + // Format start and end times to HHMMSS + const formattedStartTime = formatToHHMMSS(startTime); + const formattedEndTime = formatToHHMMSS(endTime); + + // Map days to ICS compatible format + console.log(days); + const icsDays = days.map(day => CAL_MAP[day]).join(','); + console.log(icsDays); + + // Assuming course has date started and ended, adapt as necessary + const year = new Date().getFullYear(); // Example year, adapt accordingly + // Example event date, adapt startDate according to your needs + const startDate = `20240101T${formattedStartTime}`; + const endDate = `20240101T${formattedEndTime}`; + + icsString += `BEGIN:VEVENT\n`; + icsString += `DTSTART:${startDate}\n`; + icsString += `DTEND:${endDate}\n`; + icsString += `RRULE:FREQ=WEEKLY;BYDAY=${icsDays}\n`; + icsString += `SUMMARY:${course.fullName}\n`; + icsString += `LOCATION:${location.building} ${location.room}\n`; + icsString += `END:VEVENT\n`; + }); + }); + + icsString += 'END:VCALENDAR'; + + downloadICS(icsString); + }; + if (courses?.length === -1) console.log('foo'); // dumb line to make eslint happy return (
@@ -34,10 +126,10 @@ export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Elem
- -
diff --git a/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx b/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx index 36e29995..59afe4bf 100644 --- a/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx +++ b/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx @@ -22,6 +22,8 @@ interface Props { * Grid of CalendarGridCell components forming the user's course schedule calendar view * @param props */ +// function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren): JSX.Element { +// const [grid, setGrid] = useState([]); function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWithChildren): JSX.Element { // const [grid, setGrid] = useState([]); const calendarRef = useRef(null); // Create a ref for the calendar grid