feat: refactor calendar
This commit is contained in:
@@ -2,7 +2,11 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
|||||||
import type { Course } from '@shared/types/Course';
|
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> {
|
export default async function addCourse(scheduleName: string, course: Course): Promise<void> {
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
|
|||||||
@@ -11,15 +11,11 @@ import { ExampleCourse } from 'src/stories/components/PopupCourseBlock.stories';
|
|||||||
|
|
||||||
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];
|
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.
|
* A reusable chip component that follows the design system of the extension.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Calendar(): JSX.Element {
|
export default function Calendar(): JSX.Element {
|
||||||
const calendarRef = useRef(null);
|
const calendarRef = useRef(null);
|
||||||
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
|
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
|
||||||
const [course, setCourse] = React.useState<Course | null>(null);
|
const [course, setCourse] = React.useState<Course | null>(null);
|
||||||
|
|||||||
@@ -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 { Button } from '@views/components/common/Button/Button';
|
||||||
import Divider from '@views/components/common/Divider/Divider';
|
import Divider from '@views/components/common/Divider/Divider';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { toPng } from 'html-to-image';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
|
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 type { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
|
||||||
import CalendarCourseBlock 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 = {
|
type CalendarBottomBarProps = {
|
||||||
courses?: CalendarCourseCellProps[];
|
courses?: CalendarCourseCellProps[];
|
||||||
calendarRef: React.RefObject<HTMLDivElement>;
|
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 => {
|
export default function 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 (
|
return (
|
||||||
<div className='w-full flex py-1.25'>
|
<div className='w-full flex py-1.25'>
|
||||||
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
|
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
|
||||||
<Text variant='h4'>Async. and Other:</Text>
|
<Text variant='h4'>Async. and Other:</Text>
|
||||||
<div className='h-14 inline-flex gap-2.5'>
|
<div className='h-14 inline-flex gap-2.5'>
|
||||||
{courses?.map(course => (
|
{courses?.map(({ courseDeptAndInstr, status, colors, className }) => (
|
||||||
<CalendarCourseBlock
|
<CalendarCourseBlock
|
||||||
courseDeptAndInstr={course.courseDeptAndInstr}
|
courseDeptAndInstr={courseDeptAndInstr}
|
||||||
status={course.status}
|
status={status}
|
||||||
colors={course.colors}
|
colors={colors}
|
||||||
key={course.courseDeptAndInstr}
|
key={courseDeptAndInstr}
|
||||||
className={clsx(course.className, 'w-35!')}
|
className={clsx(className, 'w-35!')}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -132,10 +46,10 @@ export const CalendarBottomBar = ({ courses, calendarRef }: CalendarBottomBarPro
|
|||||||
Save as .CAL
|
Save as .CAL
|
||||||
</Button>
|
</Button>
|
||||||
<Divider orientation='vertical' size='1rem' className='mx-1.25' />
|
<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
|
Save as .PNG
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ export interface CalendarCourseMeetingProps {
|
|||||||
* @example
|
* @example
|
||||||
* <CalendarCourseMeeting course={course} meeting={meeting} color="red" rightIcon={<Icon />} />
|
* <CalendarCourseMeeting course={course} meeting={meeting} color="red" rightIcon={<Icon />} />
|
||||||
*/
|
*/
|
||||||
const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
|
export default function CalendarCourseMeeting({
|
||||||
course,
|
course,
|
||||||
meetingIdx,
|
meetingIdx,
|
||||||
color,
|
color,
|
||||||
rightIcon,
|
rightIcon,
|
||||||
}: CalendarCourseMeetingProps) => {
|
}: CalendarCourseMeetingProps): JSX.Element {
|
||||||
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
||||||
return (
|
return (
|
||||||
<div className={styles.component}>
|
<div className={styles.component}>
|
||||||
@@ -47,6 +47,4 @@ const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CalendarCourseMeeting;
|
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ export interface CalendarCourseCellProps {
|
|||||||
* @param {string} props.className - Additional CSS class name for the cell.
|
* @param {string} props.className - Additional CSS class name for the cell.
|
||||||
* @returns {JSX.Element} The rendered component.
|
* @returns {JSX.Element} The rendered component.
|
||||||
*/
|
*/
|
||||||
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
export default function CalendarCourseCell({
|
||||||
courseDeptAndInstr,
|
courseDeptAndInstr,
|
||||||
timeAndLocation,
|
timeAndLocation,
|
||||||
status,
|
status,
|
||||||
colors,
|
colors,
|
||||||
className,
|
className,
|
||||||
onClick,
|
onClick,
|
||||||
}: CalendarCourseCellProps) => {
|
}: CalendarCourseCellProps): JSX.Element {
|
||||||
let rightIcon: React.ReactNode | null = null;
|
let rightIcon: React.ReactNode | null = null;
|
||||||
if (status === Status.WAITLISTED) {
|
if (status === Status.WAITLISTED) {
|
||||||
rightIcon = <WaitlistIcon className='h-5 w-5' />;
|
rightIcon = <WaitlistIcon className='h-5 w-5' />;
|
||||||
@@ -95,6 +95,4 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CalendarCourseBlock;
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
// import html2canvas from 'html2canvas';
|
|
||||||
import { DAY_MAP } from '@shared/types/CourseMeeting';
|
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 { getCourseColors } from '@shared/util/colors';
|
||||||
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
||||||
import CalendarCell from '@views/components/calendar/CalendarGridCell/CalendarGridCell';
|
import CalendarCell from '@views/components/calendar/CalendarGridCell/CalendarGridCell';
|
||||||
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
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';
|
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 {
|
interface Props {
|
||||||
courseCells?: CalendarGridCourse[];
|
courseCells?: CalendarGridCourse[];
|
||||||
saturdayClass?: boolean;
|
saturdayClass?: boolean;
|
||||||
@@ -22,71 +21,41 @@ interface Props {
|
|||||||
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
* Grid of CalendarGridCell components forming the user's course schedule calendar view
|
||||||
* @param props
|
* @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 [grid, setGrid] = useState([]);
|
||||||
const calendarRef = useRef(null); // Create a ref for the calendar grid
|
// 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 grid = [];
|
const grid = [];
|
||||||
for (let i = 0; i < 13; i++) {
|
|
||||||
const row = [];
|
// Run once to create the grid on initial render
|
||||||
let hour = hoursOfDay[i];
|
useEffect(() => {
|
||||||
let styleProp = {
|
for (let i = 0; i < 13; i++) {
|
||||||
gridColumn: '1',
|
const row = [];
|
||||||
gridRow: `${2 * i + 2}`,
|
let hour = hoursOfDay[i];
|
||||||
};
|
let styleProp = {
|
||||||
row.push(
|
gridColumn: '1',
|
||||||
<div key={hour} className={styles.timeBlock} style={styleProp}>
|
gridRow: `${2 * i + 2}`,
|
||||||
<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} />);
|
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 (
|
return (
|
||||||
<div className={styles.calendarGrid}>
|
<div className={styles.calendarGrid}>
|
||||||
@@ -97,14 +66,12 @@ function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWith
|
|||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{grid.map((row, rowIndex) => row)}
|
{grid.map(row => row)}
|
||||||
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} /> : null}
|
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CalendarGrid;
|
|
||||||
|
|
||||||
interface AccountForCourseConflictsProps {
|
interface AccountForCourseConflictsProps {
|
||||||
courseCells: CalendarGridCourse[];
|
courseCells: CalendarGridCourse[];
|
||||||
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
|
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
|
||||||
@@ -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> */
|
|
||||||
|
|||||||
@@ -13,47 +13,55 @@ import RedoIcon from '~icons/material-symbols/redo';
|
|||||||
import SettingsIcon from '~icons/material-symbols/settings';
|
import SettingsIcon from '~icons/material-symbols/settings';
|
||||||
import UndoIcon from '~icons/material-symbols/undo';
|
import UndoIcon from '~icons/material-symbols/undo';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the options page in a new tab.
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the options page is opened.
|
||||||
|
*/
|
||||||
const handleOpenOptions = async () => {
|
const handleOpenOptions = async () => {
|
||||||
const url = chrome.runtime.getURL('/src/pages/options/index.html');
|
const url = chrome.runtime.getURL('/src/pages/options/index.html');
|
||||||
await openTabFromContentScript(url);
|
await openTabFromContentScript(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const CalendarHeader = ({ totalHours, totalCourses, scheduleName }) => (
|
/**
|
||||||
<div className='min-h-79px min-w-672px w-full flex px-0 py-15'>
|
* Renders the header component for the calendar.
|
||||||
<div className='flex flex-row gap-20'>
|
* @returns The JSX element representing the calendar header.
|
||||||
<div className='flex gap-10'>
|
*/
|
||||||
<div className='flex gap-1'>
|
export default function CalendarHeader(): JSX.Element {
|
||||||
<Button className='self-center' variant='single' icon={MenuIcon} color='ut-gray' />
|
return (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='min-h-79px min-w-672px w-full flex px-0 py-15'>
|
||||||
<img src={calIcon} className='max-w-[48px] min-w-[48px]' alt='UT Registration Plus Logo' />
|
<div className='flex flex-row gap-20'>
|
||||||
<div className='flex flex-col whitespace-nowrap'>
|
<div className='flex gap-10'>
|
||||||
<Text className='text-lg text-ut-burntorange font-medium'>UT Registration</Text>
|
<div className='flex gap-1'>
|
||||||
<Text className='text-lg text-ut-orange font-medium'>Plus</Text>
|
<Button className='self-center' variant='single' icon={MenuIcon} color='ut-gray' />
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<img src={calIcon} className='max-w-[48px] min-w-[48px]' alt='UT Registration Plus Logo' />
|
||||||
|
<div className='flex flex-col whitespace-nowrap'>
|
||||||
|
<Text className='text-lg text-ut-burntorange font-medium'>UT Registration</Text>
|
||||||
|
<Text className='text-lg text-ut-orange font-medium'>Plus</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Divider className='self-center' size='2.5rem' orientation='vertical' />
|
||||||
|
<div className='flex flex-col self-center'>
|
||||||
|
<ScheduleTotalHoursAndCourses scheduleName='SCHEDULE' totalHours={22} totalCourses={8} />
|
||||||
|
<Text variant='h4' className='text-xs text-gray font-medium leading-normal'>
|
||||||
|
DATA UPDATED ON: 12:00 AM 02/01/2024
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider className='self-center' size='2.5rem' orientation='vertical' />
|
<div className='flex flex-row items-center justify-end space-x-8'>
|
||||||
<div className='flex flex-col self-center'>
|
<div className='flex flex-row space-x-4'>
|
||||||
<ScheduleTotalHoursAndCourses scheduleName='SCHEDULE' totalHours={22} totalCourses={8} />
|
<CourseStatus size='small' status={Status.WAITLISTED} />
|
||||||
<Text variant='h4' className='text-xs text-gray font-medium leading-normal'>
|
<CourseStatus size='small' status={Status.CLOSED} />
|
||||||
DATA UPDATED ON: 12:00 AM 02/01/2024
|
<CourseStatus size='small' status={Status.CANCELLED} />
|
||||||
</Text>
|
</div>
|
||||||
</div>
|
<div className='flex flex-row'>
|
||||||
</div>
|
<Button variant='single' icon={UndoIcon} color='ut-black' />
|
||||||
<div className='flex flex-row items-center justify-end space-x-8'>
|
<Button variant='single' icon={RedoIcon} color='ut-black' />
|
||||||
<div className='flex flex-row space-x-4'>
|
<Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions} />
|
||||||
<CourseStatus size='small' status={Status.WAITLISTED} />
|
</div>
|
||||||
<CourseStatus size='small' status={Status.CLOSED} />
|
|
||||||
<CourseStatus size='small' status={Status.CANCELLED} />
|
|
||||||
</div>
|
|
||||||
<div className='flex flex-row'>
|
|
||||||
<Button variant='single' icon={UndoIcon} color='ut-black' />
|
|
||||||
<Button variant='single' icon={RedoIcon} color='ut-black' />
|
|
||||||
<Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
|
|
||||||
export default CalendarHeader;
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import React, { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import AddSchedule from '~icons/material-symbols/add';
|
import AddSchedule from '~icons/material-symbols/add';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for the CalendarSchedules component.
|
||||||
|
*/
|
||||||
export type Props = {
|
export type Props = {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
dummySchedules?: UserSchedule[];
|
dummySchedules?: UserSchedule[];
|
||||||
@@ -21,7 +24,7 @@ export type Props = {
|
|||||||
* @param props - The component props.
|
* @param props - The component props.
|
||||||
* @returns The rendered component.
|
* @returns The rendered component.
|
||||||
*/
|
*/
|
||||||
export function CalendarSchedules(props: Props) {
|
export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: Props) {
|
||||||
const [activeScheduleIndex, setActiveScheduleIndex] = useState(0);
|
const [activeScheduleIndex, setActiveScheduleIndex] = useState(0);
|
||||||
const [newSchedule, setNewSchedule] = useState('');
|
const [newSchedule, setNewSchedule] = useState('');
|
||||||
const [activeSchedule, schedules] = useSchedules();
|
const [activeSchedule, schedules] = useSchedules();
|
||||||
@@ -58,13 +61,13 @@ export function CalendarSchedules(props: Props) {
|
|||||||
));
|
));
|
||||||
|
|
||||||
const fixBuildError = {
|
const fixBuildError = {
|
||||||
dummySchedules: props.dummySchedules,
|
dummySchedules,
|
||||||
dummyActiveIndex: props.dummyActiveIndex,
|
dummyActiveIndex,
|
||||||
};
|
};
|
||||||
console.log(fixBuildError);
|
console.log(fixBuildError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...props.style }} className='items-center'>
|
<div style={{ ...style }} className='items-center'>
|
||||||
<div className='m0 m-b-2 w-full flex justify-between'>
|
<div className='m0 m-b-2 w-full flex justify-between'>
|
||||||
<Text variant='h3'>MY SCHEDULES</Text>
|
<Text variant='h3'>MY SCHEDULES</Text>
|
||||||
<div className='cursor-pointer items-center justify-center btn-transition -ml-1.5 hover:text-zinc-400'>
|
<div className='cursor-pointer items-center justify-center btn-transition -ml-1.5 hover:text-zinc-400'>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ type Props = {
|
|||||||
* The "Important Links" section of the calendar website
|
* The "Important Links" section of the calendar website
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export default function ImportantLinks({ className }: Props) {
|
export default function ImportantLinks({ className }: Props): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<article className={clsx(className, 'flex flex-col gap-2')}>
|
<article className={clsx(className, 'flex flex-col gap-2')}>
|
||||||
<Text variant='h3'>Important Links</Text>
|
<Text variant='h3'>Important Links</Text>
|
||||||
|
|||||||
26
src/views/components/calendar/utils.test.ts
Normal file
26
src/views/components/calendar/utils.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
114
src/views/components/calendar/utils.ts
Normal file
114
src/views/components/calendar/utils.ts
Normal file
@@ -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<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the schedule from the UserScheduleStore based on the active index.
|
||||||
|
* @returns {Promise<any>} 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<HTMLDivElement>) => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user