Skip to content

Commit

Permalink
feat: refactor calendar
Browse files Browse the repository at this point in the history
  • Loading branch information
doprz committed Mar 6, 2024
1 parent 745f9dd commit 28f1924
Show file tree
Hide file tree
Showing 11 changed files with 251 additions and 236 deletions.
6 changes: 5 additions & 1 deletion src/pages/background/lib/addCourse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Course } from '@shared/types/Course';

/**
*
* Adds a course to a user's schedule.
* @param scheduleName - The name of the schedule to add the course to.
* @param course - The course to add.
* @returns A promise that resolves to void.
* @throws An error if the schedule is not found.
*/
export default async function addCourse(scheduleName: string, course: Course): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
Expand Down
6 changes: 1 addition & 5 deletions src/views/components/calendar/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ import { ExampleCourse } from 'src/stories/components/PopupCourseBlock.stories';

export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];

interface Props {
label: string;
}

/**
* A reusable chip component that follows the design system of the extension.
* @returns
*/
export function Calendar(): JSX.Element {
export default function Calendar(): JSX.Element {
const calendarRef = useRef(null);
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
const [course, setCourse] = React.useState<Course | null>(null);
Expand Down
114 changes: 14 additions & 100 deletions src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { saveAsCal, saveCalAsPng } from '@views/components/calendar/utils';
import { Button } from '@views/components/common/Button/Button';
import Divider from '@views/components/common/Divider/Divider';
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';
Expand All @@ -12,116 +11,31 @@ 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;
}

/**
* Renders the bottom bar of the calendar component.
*
* @param {Object[]} courses - The list of courses to display in the calendar.
* @param {React.RefObject} calendarRef - The reference to the calendar component.
* @returns {JSX.Element} The rendered bottom bar component.
*/
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
export default function CalendarBottomBar({ courses, calendarRef }: CalendarBottomBarProps): JSX.Element {
return (
<div className='w-full flex py-1.25'>
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
<Text variant='h4'>Async. and Other:</Text>
<div className='h-14 inline-flex gap-2.5'>
{courses?.map(course => (
{courses?.map(({ courseDeptAndInstr, status, colors, className }) => (
<CalendarCourseBlock
courseDeptAndInstr={course.courseDeptAndInstr}
status={course.status}
colors={course.colors}
key={course.courseDeptAndInstr}
className={clsx(course.className, 'w-35!')}
courseDeptAndInstr={courseDeptAndInstr}
status={status}
colors={colors}
key={courseDeptAndInstr}
className={clsx(className, 'w-35!')}
/>
))}
</div>
Expand All @@ -132,10 +46,10 @@ export const CalendarBottomBar = ({ courses, calendarRef }: CalendarBottomBarPro
Save as .CAL
</Button>
<Divider orientation='vertical' size='1rem' className='mx-1.25' />
<Button variant='single' color='ut-black' icon={ImageIcon} onClick={saveAsPng}>
<Button variant='single' color='ut-black' icon={ImageIcon} onClick={() => saveCalAsPng(calendarRef)}>
Save as .PNG
</Button>
</div>
</div>
);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ export interface CalendarCourseMeetingProps {
* @example
* <CalendarCourseMeeting course={course} meeting={meeting} color="red" rightIcon={<Icon />} />
*/
const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
export default function CalendarCourseMeeting({
course,
meetingIdx,
color,
rightIcon,
}: CalendarCourseMeetingProps) => {
}: CalendarCourseMeetingProps): JSX.Element {
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
return (
<div className={styles.component}>
Expand All @@ -47,6 +47,4 @@ const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
</div>
</div>
);
};

export default CalendarCourseMeeting;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ export interface CalendarCourseCellProps {
* @param {string} props.className - Additional CSS class name for the cell.
* @returns {JSX.Element} The rendered component.
*/
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
export default function CalendarCourseCell({
courseDeptAndInstr,
timeAndLocation,
status,
colors,
className,
onClick,
}: CalendarCourseCellProps) => {
}: CalendarCourseCellProps): JSX.Element {
let rightIcon: React.ReactNode | null = null;
if (status === Status.WAITLISTED) {
rightIcon = <WaitlistIcon className='h-5 w-5' />;
Expand Down Expand Up @@ -95,6 +95,4 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
)}
</div>
);
};

export default CalendarCourseBlock;
}
118 changes: 36 additions & 82 deletions src/views/components/calendar/CalendarGrid/CalendarGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import type { Course } from '@shared/types/Course';
// import html2canvas from 'html2canvas';
import { DAY_MAP } from '@shared/types/CourseMeeting';
/* import calIcon from 'src/assets/icons/cal.svg';
import pngIcon from 'src/assets/icons/png.svg';
*/
import { getCourseColors } from '@shared/util/colors';
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import CalendarCell from '@views/components/calendar/CalendarGridCell/CalendarGridCell';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect } from 'react';

import styles from './CalendarGrid.module.scss';

const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);

interface Props {
courseCells?: CalendarGridCourse[];
saturdayClass?: boolean;
Expand All @@ -22,71 +21,41 @@ interface Props {
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWithChildren<Props>): JSX.Element {
export default 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

const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);

/* const saveAsPNG = () => {
htmlToImage
.toPng(calendarRef.current, {
backgroundColor: 'white',
style: {
background: 'white',
marginTop: '20px',
marginBottom: '20px',
marginRight: '20px',
marginLeft: '20px',
},
})
.then(dataUrl => {
let img = new Image();
img.src = dataUrl;
fetch(dataUrl)
.then(response => response.blob())
.then(blob => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = 'my-schedule.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(error => console.error('Error downloading file:', error));
})
.catch(error => {
console.error('oops, something went wrong!', error);
});
}; */

// TODO: Change to useMemo hook once we start calculating grid size based on if there's a Saturday class or not
// const calendarRef = useRef(null); // Create a ref for the calendar grid
const grid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
let styleProp = {
gridColumn: '1',
gridRow: `${2 * i + 2}`,
};
row.push(
<div key={hour} className={styles.timeBlock} style={styleProp}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
</div>
);
for (let k = 0; k < 5; k++) {
styleProp = {
gridColumn: `${k + 2}`,
gridRow: `${2 * i + 2} / ${2 * i + 4}`,

// Run once to create the grid on initial render
useEffect(() => {
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
let styleProp = {
gridColumn: '1',
gridRow: `${2 * i + 2}`,
};
row.push(<CalendarCell key={k} styleProp={styleProp} />);
row.push(
<div key={hour} className={styles.timeBlock} style={styleProp}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
</div>
);
for (let k = 0; k < 5; k++) {
styleProp = {
gridColumn: `${k + 2}`,
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
};
row.push(<CalendarCell key={k} styleProp={styleProp} />);
}
grid.push(row);
}
grid.push(row);
}
});

return (
<div className={styles.calendarGrid}>
Expand All @@ -97,14 +66,12 @@ function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWith
{day}
</div>
))}
{grid.map((row, rowIndex) => row)}
{grid.map(row => row)}
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} /> : null}
</div>
);
}

export default CalendarGrid;

interface AccountForCourseConflictsProps {
courseCells: CalendarGridCourse[];
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
Expand Down Expand Up @@ -178,16 +145,3 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
);
});
}

/* <div className={styles.buttonContainer}>
<div className={styles.divider} />
<button className={styles.calendarButton}>
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
Save as .CAL
</button>
<div className={styles.divider} />
<button onClick={saveAsPNG} className={styles.calendarButton}>
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
Save as .PNG
</button>
</div> */
Loading

0 comments on commit 28f1924

Please sign in to comment.