diff --git a/src/pages/background/lib/addCourse.ts b/src/pages/background/lib/addCourse.ts index a987828e..cd2cd31d 100644 --- a/src/pages/background/lib/addCourse.ts +++ b/src/pages/background/lib/addCourse.ts @@ -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 { const schedules = await UserScheduleStore.get('schedules'); diff --git a/src/views/components/calendar/Calendar/Calendar.tsx b/src/views/components/calendar/Calendar/Calendar.tsx index 5f8bbcf3..6c23b906 100644 --- a/src/views/components/calendar/Calendar/Calendar.tsx +++ b/src/views/components/calendar/Calendar/Calendar.tsx @@ -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(null); diff --git a/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx b/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx index 2b66f19b..2cdf3e5e 100644 --- a/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx +++ b/src/views/components/calendar/CalendarBottomBar/CalendarBottomBar.tsx @@ -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'; @@ -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; }; -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 (
Async. and Other:
- {courses?.map(course => ( + {courses?.map(({ courseDeptAndInstr, status, colors, className }) => ( ))}
@@ -132,10 +46,10 @@ export const CalendarBottomBar = ({ courses, calendarRef }: CalendarBottomBarPro Save as .CAL -
); -}; +} diff --git a/src/views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting.tsx b/src/views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting.tsx index 525cd915..710a49e0 100644 --- a/src/views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting.tsx +++ b/src/views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting.tsx @@ -24,12 +24,12 @@ export interface CalendarCourseMeetingProps { * @example * } /> */ -const CalendarCourseMeeting: React.FC = ({ +export default function CalendarCourseMeeting({ course, meetingIdx, color, rightIcon, -}: CalendarCourseMeetingProps) => { +}: CalendarCourseMeetingProps): JSX.Element { let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null; return (
@@ -47,6 +47,4 @@ const CalendarCourseMeeting: React.FC = ({
); -}; - -export default CalendarCourseMeeting; +} diff --git a/src/views/components/calendar/CalendarCourseCell/CalendarCourseCell.tsx b/src/views/components/calendar/CalendarCourseCell/CalendarCourseCell.tsx index b9ec4fb0..5c42c3d3 100644 --- a/src/views/components/calendar/CalendarCourseCell/CalendarCourseCell.tsx +++ b/src/views/components/calendar/CalendarCourseCell/CalendarCourseCell.tsx @@ -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 = ({ +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 = ; @@ -95,6 +95,4 @@ const CalendarCourseCell: React.FC = ({ )} ); -}; - -export default CalendarCourseBlock; +} diff --git a/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx b/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx index 3436ed91..d2fe5256 100644 --- a/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx +++ b/src/views/components/calendar/CalendarGrid/CalendarGrid.tsx @@ -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; @@ -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): JSX.Element { +export default function CalendarGrid({ + courseCells, + saturdayClass, + setCourse, +}: React.PropsWithChildren): 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( -
-
-

{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}

-
-
- ); - 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(); + row.push( +
+
+

{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}

+
+
+ ); + for (let k = 0; k < 5; k++) { + styleProp = { + gridColumn: `${k + 2}`, + gridRow: `${2 * i + 2} / ${2 * i + 4}`, + }; + row.push(); + } + grid.push(row); } - grid.push(row); - } + }); return (
@@ -97,14 +66,12 @@ function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWith {day}
))} - {grid.map((row, rowIndex) => row)} + {grid.map(row => row)} {courseCells ? : null} ); } -export default CalendarGrid; - interface AccountForCourseConflictsProps { courseCells: CalendarGridCourse[]; setCourse: React.Dispatch>; @@ -178,16 +145,3 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC ); }); } - -/*
-
- -
- -
*/ diff --git a/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx b/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx index f30b9bd7..9927b4df 100644 --- a/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx +++ b/src/views/components/calendar/CalendarHeader/CalenderHeader.tsx @@ -13,47 +13,55 @@ import RedoIcon from '~icons/material-symbols/redo'; import SettingsIcon from '~icons/material-symbols/settings'; import UndoIcon from '~icons/material-symbols/undo'; +/** + * Opens the options page in a new tab. + * @returns {Promise} A promise that resolves when the options page is opened. + */ const handleOpenOptions = async () => { const url = chrome.runtime.getURL('/src/pages/options/index.html'); await openTabFromContentScript(url); }; -const CalendarHeader = ({ totalHours, totalCourses, scheduleName }) => ( -
-
-
-
-
+ +
+ + + DATA UPDATED ON: 12:00 AM 02/01/2024 + +
- -
- - - DATA UPDATED ON: 12:00 AM 02/01/2024 - -
-
-
-
- - - -
-
-
-
-); - -export default CalendarHeader; + ); +} diff --git a/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx b/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx index 26bf5f0a..68ae8e30 100644 --- a/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx +++ b/src/views/components/calendar/CalendarSchedules/CalendarSchedules.tsx @@ -9,6 +9,9 @@ import React, { useEffect, useState } from 'react'; import AddSchedule from '~icons/material-symbols/add'; +/** + * Props for the CalendarSchedules component. + */ export type Props = { style?: React.CSSProperties; dummySchedules?: UserSchedule[]; @@ -21,7 +24,7 @@ export type Props = { * @param props - The component props. * @returns The rendered component. */ -export function CalendarSchedules(props: Props) { +export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: Props) { const [activeScheduleIndex, setActiveScheduleIndex] = useState(0); const [newSchedule, setNewSchedule] = useState(''); const [activeSchedule, schedules] = useSchedules(); @@ -58,13 +61,13 @@ export function CalendarSchedules(props: Props) { )); const fixBuildError = { - dummySchedules: props.dummySchedules, - dummyActiveIndex: props.dummyActiveIndex, + dummySchedules, + dummyActiveIndex, }; console.log(fixBuildError); return ( -
+
MY SCHEDULES
diff --git a/src/views/components/calendar/ImportantLinks.tsx b/src/views/components/calendar/ImportantLinks.tsx index 39cc6e1d..26414cfb 100644 --- a/src/views/components/calendar/ImportantLinks.tsx +++ b/src/views/components/calendar/ImportantLinks.tsx @@ -12,7 +12,7 @@ type Props = { * The "Important Links" section of the calendar website * @returns */ -export default function ImportantLinks({ className }: Props) { +export default function ImportantLinks({ className }: Props): JSX.Element { return (
Important Links diff --git a/src/views/components/calendar/utils.test.ts b/src/views/components/calendar/utils.test.ts new file mode 100644 index 00000000..cc2e6997 --- /dev/null +++ b/src/views/components/calendar/utils.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { formatToHHMMSS } from './utils'; + +describe('formatToHHMMSS', () => { + it('should format minutes to HHMMSS format', () => { + const minutes = 125; + const expected = '020500'; + const result = formatToHHMMSS(minutes); + expect(result).toBe(expected); + }); + + it('should handle single digit minutes', () => { + const minutes = 5; + const expected = '000500'; + const result = formatToHHMMSS(minutes); + expect(result).toBe(expected); + }); + + it('should handle zero minutes', () => { + const minutes = 0; + const expected = '000000'; + const result = formatToHHMMSS(minutes); + expect(result).toBe(expected); + }); +}); diff --git a/src/views/components/calendar/utils.ts b/src/views/components/calendar/utils.ts new file mode 100644 index 00000000..4d41abe2 --- /dev/null +++ b/src/views/components/calendar/utils.ts @@ -0,0 +1,114 @@ +import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; +import { toPng } from 'html-to-image'; + +export const CAL_MAP = { + Sunday: 'SU', + Monday: 'MO', + Tuesday: 'TU', + Wednesday: 'WE', + Thursday: 'TH', + Friday: 'FR', + Saturday: 'SA', +} as const satisfies Record; + +/** + * Retrieves the schedule from the UserScheduleStore based on the active index. + * @returns {Promise} A promise that resolves to the retrieved schedule. + */ +const getSchedule = async () => { + const schedules = await UserScheduleStore.get('schedules'); + const activeIndex = await UserScheduleStore.get('activeIndex'); + const schedule = schedules[activeIndex]; + + return schedule; +}; + +/** + * Formats the given number of minutes into a string representation of HHMMSS format. + * + * @param minutes - The number of minutes to format. + * @returns A string representation of the given minutes in HHMMSS format. + */ +export const formatToHHMMSS = (minutes: number) => { + const hours = String(Math.floor(minutes / 60)).padStart(2, '0'); + const mins = String(minutes % 60).padStart(2, '0'); + return `${hours}${mins}00`; +}; + +/** + * Downloads an ICS file with the given data. + * + * @param data - The data to be included in the ICS file. + */ +const downloadICS = (data: BlobPart) => { + const blob: 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); +}; + +/** + * Saves the current schedule as a calendar file in the iCalendar format (ICS). + * Fetches the current active schedule and converts it into an ICS string. + * Downloads the ICS file to the user's device. + */ +export 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); +}; + +/** + * Saves the calendar as a PNG image. + */ +export const saveCalAsPng = (calendarRef: React.RefObject) => { + 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.error(err); + }); + } +};