Skip to content

Commit

Permalink
feat: working PNG and CAL downloads (#119)
Browse files Browse the repository at this point in the history
* working save as PNG

* cleanup

* feat(cal): working ICS file
  • Loading branch information
Lukas-Zenick authored and doprz committed Mar 6, 2024
1 parent d62b8d1 commit d9ee23c
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const Default: Story = {
status: examplePsyCourse.status,
},
],
calendarRef: { current: null },
},
render: props => (
<div className='outline-red outline w-292.5!'>
Expand Down
7 changes: 4 additions & 3 deletions src/views/components/calendar/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -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<Course | null>(null);

Expand All @@ -38,11 +39,11 @@ export function Calendar(): JSX.Element {
<ImportantLinks />
</div>
<div className='flex flex-grow flex-col gap-4 overflow-hidden pr-12'>
<div className='flex-grow overflow-auto'>
<div ref={calendarRef} className='flex-grow overflow-auto'>
<CalendarGrid courseCells={courseCells} setCourse={setCourse} />
</div>
<div>
<CalendarBottomBar />
<CalendarBottomBar calendarRef={calendarRef} />
</div>
</div>
</div>
Expand Down
102 changes: 97 additions & 5 deletions src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>;
};

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 (
<div className='w-full flex py-1.25'>
Expand All @@ -34,10 +126,10 @@ export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Elem
</div>
</div>
<div className='flex items-center pl-2.5 pr-7.5'>
<Button variant='single' color='ut-black' icon={CalendarMonthIcon}>
<Button variant='single' color='ut-black' icon={CalendarMonthIcon} onClick={saveAsCal}>
Save as .CAL
</Button>
<Button variant='single' color='ut-black' icon={ImageIcon}>
<Button variant='single' color='ut-black' icon={ImageIcon} onClick={saveAsPng}>
Save as .PNG
</Button>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/views/components/calendar/CalendarGrid/CalendarGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Props>): JSX.Element {
// const [grid, setGrid] = useState([]);
function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWithChildren<Props>): JSX.Element {
// const [grid, setGrid] = useState([]);
const calendarRef = useRef(null); // Create a ref for the calendar grid
Expand Down

0 comments on commit d9ee23c

Please sign in to comment.