feat: Calendar Schedule component finished, fix: list didn't allow updates when adding a new schedule (#115)
* Temporarily uninstalling husky cause github desktop has issues with it * Cleaned up some code. Removed unnecessary state value on injected popup * Should've fixed popup alignment issue. Still need to integrate course schedule with calendar. Still debugging. * Updated CalendarGridStories * Fix: change to ExampleCourse from exampleCourse * setCourse and calendar header need work * Update as part of merge * Fix: fixed build errors * Fix: Added Todo * Chore: Cleaned up useFlattenedCourseSchedule hook * fix: List now keeps track of state when existing items are switched, while adding new items to the end * Added back husky * Update src/views/components/calendar/Calendar/Calendar.tsx Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> * refactor: added type-safety, destructuring, etc. ready for re-review * refactor: got rid of ts-ignore in openNewTabFromContentScript * Update src/views/components/calendar/CalendarHeader/CalenderHeader.tsx Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> * refactor: using path aliasing Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> * refactor: using path aliasing Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> * refactor: using satisfies instead of as Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> * refactor: using satisfies instead of as Co-authored-by: doprz <52579214+doprz@users.noreply.github.com> * style: reformatted spacing * style: eslint import order * refactor: added new constructor for UserSchedule to avoid passing down null values to child props --------- Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
"highcharts": "^11.3.0",
|
"highcharts": "^11.3.0",
|
||||||
"highcharts-react-official": "^3.2.1",
|
"highcharts-react-official": "^3.2.1",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
|
"husky": "^9.0.11",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-devtools-core": "^5.0.0",
|
"react-devtools-core": "^5.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
@@ -96,7 +97,6 @@
|
|||||||
"eslint-plugin-restrict-import-depth": "file:custom-eslint-rules",
|
"eslint-plugin-restrict-import-depth": "file:custom-eslint-rules",
|
||||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||||
"eslint-plugin-storybook": "^0.6.15",
|
"eslint-plugin-storybook": "^0.6.15",
|
||||||
"husky": "^9.0.11",
|
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.4.35",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
|
|||||||
649
pnpm-lock.yaml
generated
649
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,20 @@ export class UserSchedule {
|
|||||||
name: string;
|
name: string;
|
||||||
hours: number;
|
hours: number;
|
||||||
|
|
||||||
constructor(schedule: Serialized<UserSchedule>) {
|
constructor(schedule: Serialized<UserSchedule>);
|
||||||
this.courses = schedule.courses.map(c => new Course(c));
|
constructor(courses: Course[], name: string, hours: number);
|
||||||
this.name = schedule.name;
|
constructor(coursesOrSchedule: Course[] | Serialized<UserSchedule>, name?: string, hours?: number) {
|
||||||
this.hours = 0;
|
if (Array.isArray(coursesOrSchedule)) {
|
||||||
for (const course of this.courses) {
|
this.courses = coursesOrSchedule;
|
||||||
this.hours += course.creditHours;
|
this.name = name || '';
|
||||||
|
this.hours = hours || 0;
|
||||||
|
} else {
|
||||||
|
this.courses = coursesOrSchedule.courses.map(c => new Course(c));
|
||||||
|
this.name = coursesOrSchedule.name;
|
||||||
|
this.hours = 0;
|
||||||
|
for (const course of this.courses) {
|
||||||
|
this.hours += course.creditHours;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|||||||
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
|
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
|
||||||
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
||||||
|
|
||||||
|
import { ExampleCourse } from '../PopupCourseBlock.stories';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Calendar/CalendarGrid',
|
title: 'Components/Calendar/CalendarGrid',
|
||||||
component: CalendarGrid,
|
component: CalendarGrid,
|
||||||
@@ -32,6 +34,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.OPEN,
|
status: Status.OPEN,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
calendarGridPoint: {
|
calendarGridPoint: {
|
||||||
@@ -45,6 +48,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.OPEN,
|
status: Status.OPEN,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
calendarGridPoint: {
|
calendarGridPoint: {
|
||||||
@@ -58,6 +62,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.CLOSED,
|
status: Status.CLOSED,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
calendarGridPoint: {
|
calendarGridPoint: {
|
||||||
@@ -71,6 +76,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.OPEN,
|
status: Status.OPEN,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
calendarGridPoint: {
|
calendarGridPoint: {
|
||||||
@@ -84,6 +90,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.CLOSED,
|
status: Status.CLOSED,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
calendarGridPoint: {
|
calendarGridPoint: {
|
||||||
@@ -97,6 +104,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.CLOSED,
|
status: Status.CLOSED,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
calendarGridPoint: {
|
calendarGridPoint: {
|
||||||
@@ -110,6 +118,7 @@ const testData: CalendarGridCourse[] = [
|
|||||||
status: Status.CLOSED,
|
status: Status.CLOSED,
|
||||||
colors: getCourseColors('emerald', 500),
|
colors: getCourseColors('emerald', 500),
|
||||||
},
|
},
|
||||||
|
course: ExampleCourse,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { handleOpenCalendar } from '@views/components/injected/CourseCatalogInje
|
|||||||
import useSchedules from '@views/hooks/useSchedules';
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
|
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { tailwindColorways } from 'src/shared/util/storybook';
|
import { tailwindColorways } from 'src/shared/util/storybook';
|
||||||
|
|
||||||
import CalendarIcon from '~icons/material-symbols/calendar-month';
|
import CalendarIcon from '~icons/material-symbols/calendar-month';
|
||||||
@@ -21,14 +22,21 @@ import SettingsIcon from '~icons/material-symbols/settings';
|
|||||||
* This component displays the main schedule, courses, and options buttons.
|
* This component displays the main schedule, courses, and options buttons.
|
||||||
*/
|
*/
|
||||||
export default function PopupMain() {
|
export default function PopupMain() {
|
||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule, schedules] = useSchedules();
|
||||||
|
const coursesLength = activeSchedule ? activeSchedule.courses.length : 0;
|
||||||
|
if (!activeSchedule) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const draggableElements = activeSchedule?.courses.map((course, i) => (
|
const draggableElements = activeSchedule?.courses.map((course, i) => (
|
||||||
<PopupCourseBlock key={course.uniqueId} course={course} colors={tailwindColorways[i]} />
|
<PopupCourseBlock
|
||||||
|
key={course.uniqueId}
|
||||||
|
course={course}
|
||||||
|
colors={tailwindColorways[i]}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
const handleOpenOptions = async () => {
|
const handleOpenOptions = async () => {
|
||||||
// Not sure if it's bad practice to export this
|
|
||||||
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);
|
||||||
};
|
};
|
||||||
@@ -60,11 +68,11 @@ export default function PopupMain() {
|
|||||||
<Divider orientation='horizontal' className='my-4' size='100%' />
|
<Divider orientation='horizontal' className='my-4' size='100%' />
|
||||||
<div className='mb-4 border border-ut-offwhite rounded p-2 text-left'>
|
<div className='mb-4 border border-ut-offwhite rounded p-2 text-left'>
|
||||||
<Text as='div' variant='h1-course' className='color-ut-burntorange'>
|
<Text as='div' variant='h1-course' className='color-ut-burntorange'>
|
||||||
MAIN SCHEDULE:
|
{`${activeSchedule.name}`}:
|
||||||
</Text>
|
</Text>
|
||||||
<div className='flex items-center justify-start gap2.5 color-ut-black'>
|
<div className='flex items-center justify-start gap2.5 color-ut-black'>
|
||||||
<Text variant='h1'>22 HOURS</Text>
|
<Text variant='h1'>{`${activeSchedule.hours} HOURS`}</Text>
|
||||||
<Text variant='h2-course'>8 Courses</Text>
|
<Text variant='h2-course'>{`${coursesLength} Courses`}</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Integrate the List component here */}
|
{/* Integrate the List component here */}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
|
import type { Course } from '@shared/types/Course';
|
||||||
import { CalendarBottomBar } from '@views/components/calendar/CalendarBottomBar/CalendarBottomBar';
|
import { CalendarBottomBar } from '@views/components/calendar/CalendarBottomBar/CalendarBottomBar';
|
||||||
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
|
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
|
||||||
import CalendarHeader from '@views/components/calendar/CalendarHeader/CalenderHeader';
|
import CalendarHeader from '@views/components/calendar/CalendarHeader/CalenderHeader';
|
||||||
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
|
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
|
||||||
import ImportantLinks from '@views/components/calendar/ImportantLinks';
|
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 from 'react';
|
||||||
|
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'];
|
||||||
|
|
||||||
@@ -16,9 +22,12 @@ interface Props {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Calendar(): JSX.Element {
|
export function Calendar(): JSX.Element {
|
||||||
|
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
|
||||||
|
const [course, setCourse] = React.useState<Course | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className = 'flex flex-col'>
|
||||||
<CalendarHeader />
|
<CalendarHeader totalHours={activeSchedule.hours} scheduleName={activeSchedule.name} totalCourses={activeSchedule?.courses.length}/>
|
||||||
<div className='h-screen w-full flex flex-col md:flex-row'>
|
<div className='h-screen w-full flex flex-col md:flex-row'>
|
||||||
<div className='min-h-[30%] flex flex-col items-start gap-2.5 p-5 pl-7'>
|
<div className='min-h-[30%] flex flex-col items-start gap-2.5 p-5 pl-7'>
|
||||||
<div className='min-h-[30%]'>
|
<div className='min-h-[30%]'>
|
||||||
@@ -26,15 +35,18 @@ export function Calendar(): JSX.Element {
|
|||||||
</div>
|
</div>
|
||||||
<ImportantLinks />
|
<ImportantLinks />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-grow flex-col gap-4 overflow-hidden'>
|
<div className='flex flex-grow flex-col gap-4 overflow-hidden pr-12'>
|
||||||
<div className='flex-grow overflow-auto'>
|
<div className='flex-grow overflow-auto'>
|
||||||
<CalendarGrid />
|
<CalendarGrid courseCells = {courseCells} setCourse={setCourse}/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CalendarBottomBar />
|
<CalendarBottomBar />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
{/* TODO: Doesn't work when exampleCourse is replaced with an actual course through setCourse.
|
||||||
|
Check CalendarGrid.tsx and AccountForCourseConflicts for an example */}
|
||||||
|
{course ? <CourseCatalogInjectedPopup course = {ExampleCourse} activeSchedule = {activeSchedule} onClose={() => setCourse(null)}/> : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface CalendarCourseCellProps {
|
|||||||
status: StatusType;
|
status: StatusType;
|
||||||
colors: CourseColors;
|
colors: CourseColors;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +40,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
|||||||
status,
|
status,
|
||||||
colors,
|
colors,
|
||||||
className,
|
className,
|
||||||
|
onClick,
|
||||||
}: CalendarCourseCellProps) => {
|
}: CalendarCourseCellProps) => {
|
||||||
let rightIcon: React.ReactNode | null = null;
|
let rightIcon: React.ReactNode | null = null;
|
||||||
if (status === Status.WAITLISTED) {
|
if (status === Status.WAITLISTED) {
|
||||||
@@ -54,10 +56,11 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('h-full w-full flex justify-center rounded p-2 overflow-x-hidden', fontColor, className)}
|
className={clsx('h-full w-full flex justify-center rounded p-2 overflow-x-hidden cursor-default hover:cursor-pointer', fontColor, className)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.primaryColor,
|
backgroundColor: colors.primaryColor,
|
||||||
}}
|
}}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className='flex flex-1 flex-col gap-1 overflow-x-hidden'>
|
<div className='flex flex-1 flex-col gap-1 overflow-x-hidden'>
|
||||||
<Text
|
<Text
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
import type { Course } from '@shared/types/Course';
|
||||||
/* import calIcon from 'src/assets/icons/cal.svg';
|
/* import calIcon from 'src/assets/icons/cal.svg';
|
||||||
import pngIcon from 'src/assets/icons/png.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 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, useRef, useState } from 'react';
|
||||||
@@ -10,34 +12,19 @@ import { DAY_MAP } from 'src/shared/types/CourseMeeting';
|
|||||||
|
|
||||||
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);
|
|
||||||
const grid = [];
|
|
||||||
for (let i = 0; i < 13; i++) {
|
|
||||||
const row = [];
|
|
||||||
let hour = hoursOfDay[i];
|
|
||||||
row.push(
|
|
||||||
<div key={hour} className={styles.timeBlock}>
|
|
||||||
<div className={styles.timeLabelContainer}>
|
|
||||||
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
row.push(Array.from({ length: 5 }, (_, j) => <CalendarCell key={j} />));
|
|
||||||
grid.push(row);
|
|
||||||
} */
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
courseCells?: CalendarGridCourse[];
|
courseCells?: CalendarGridCourse[];
|
||||||
saturdayClass?: boolean;
|
saturdayClass?: boolean;
|
||||||
|
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 }: React.PropsWithChildren<Props>): JSX.Element {
|
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 daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
|
||||||
@@ -76,39 +63,32 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
|
|||||||
});
|
});
|
||||||
}; */
|
}; */
|
||||||
|
|
||||||
useEffect(() => {
|
// TODO: Change to useMemo hook once we start calculating grid size based on if there's a Saturday class or not
|
||||||
const newGrid = [];
|
const grid = [];
|
||||||
for (let i = 0; i < 13; i++) {
|
for (let i = 0; i < 13; i++) {
|
||||||
const row = [];
|
const row = [];
|
||||||
let hour = hoursOfDay[i];
|
let hour = hoursOfDay[i];
|
||||||
let styleProp = {
|
let styleProp = {
|
||||||
gridColumn: '1',
|
gridColumn: '1',
|
||||||
gridRow: `${2 * i + 2}`,
|
gridRow: `${2 * i + 2}`,
|
||||||
};
|
};
|
||||||
row.push(
|
row.push(
|
||||||
<div key={hour} className={styles.timeBlock} style={styleProp}>
|
<div key={hour} className={styles.timeBlock} style={styleProp}>
|
||||||
<div className={styles.timeLabelContainer}>
|
<div className={styles.timeLabelContainer}>
|
||||||
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
|
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
for (let k = 0; k < 5; k++) {
|
);
|
||||||
// let shouldRender = false;
|
for (let k = 0; k < 5; k++) {
|
||||||
styleProp = {
|
styleProp = {
|
||||||
gridColumn: `${k + 2}`,
|
gridColumn: `${k + 2}`,
|
||||||
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
|
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
|
||||||
};
|
};
|
||||||
/* let shouldRenderChild = courseCells[iterator]?.calendarGridPoint &&
|
row.push(<CalendarCell key={k} styleProp={styleProp} />);
|
||||||
k === courseCells[iterator].calendarGridPoint.dayIndex && i === courseCells[iterator].calendarGridPoint.startIndex;
|
|
||||||
let childElement = <div className={styles.dot}/>; */
|
|
||||||
/* let completeGridCell = shouldRenderChild ? <CalendarCell key={k} children={childElement}/>
|
|
||||||
: <CalendarCell key={k} />; */
|
|
||||||
row.push(<CalendarCell key={k} styleProp={styleProp} />);
|
|
||||||
}
|
|
||||||
newGrid.push(row);
|
|
||||||
}
|
}
|
||||||
setGrid(newGrid);
|
grid.push(row);
|
||||||
}, [hoursOfDay]);
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.calendarGrid}>
|
<div className={styles.calendarGrid}>
|
||||||
@@ -120,23 +100,7 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{grid.map((row, rowIndex) => row)}
|
{grid.map((row, rowIndex) => row)}
|
||||||
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} /> : null}
|
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse}/> : null}
|
||||||
{/* courseCells.map((block: CalendarGridCourse) => (
|
|
||||||
<div
|
|
||||||
key={`${block}`}
|
|
||||||
style={{
|
|
||||||
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
|
|
||||||
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CalendarCourseCell
|
|
||||||
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
|
||||||
timeAndLocation={block.componentProps.timeAndLocation}
|
|
||||||
status={block.componentProps.status}
|
|
||||||
colors={block.componentProps.colors}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)) */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -145,9 +109,10 @@ export default CalendarGrid;
|
|||||||
|
|
||||||
interface AccountForCourseConflictsProps {
|
interface AccountForCourseConflictsProps {
|
||||||
courseCells: CalendarGridCourse[];
|
courseCells: CalendarGridCourse[];
|
||||||
|
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsProps): JSX.Element[] {
|
function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseConflictsProps): JSX.Element[] {
|
||||||
// Groups by dayIndex to identify overlaps
|
// Groups by dayIndex to identify overlaps
|
||||||
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
|
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
|
||||||
const { dayIndex } = cell.calendarGridPoint;
|
const { dayIndex } = cell.calendarGridPoint;
|
||||||
@@ -173,11 +138,9 @@ function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsPro
|
|||||||
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
|
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
|
||||||
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
|
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
|
||||||
if (isOverlapping) {
|
if (isOverlapping) {
|
||||||
console.log('Found overlapping element');
|
|
||||||
// Adjust columnIndex to not overlap with the otherCell
|
// Adjust columnIndex to not overlap with the otherCell
|
||||||
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
|
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
|
||||||
columnIndex = otherCell.gridColumnStart + 1;
|
columnIndex = otherCell.gridColumnStart + 1;
|
||||||
console.log(columnIndex);
|
|
||||||
}
|
}
|
||||||
cell.totalColumns += 1;
|
cell.totalColumns += 1;
|
||||||
}
|
}
|
||||||
@@ -188,12 +151,15 @@ function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsPro
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Part of TODO: block.course is definitely a course object
|
||||||
|
console.log(courseCells);
|
||||||
|
|
||||||
return courseCells.map(block => (
|
return courseCells.map(block => (
|
||||||
<div
|
<div
|
||||||
key={`${block}`}
|
key={`${block}`}
|
||||||
style={{
|
style={{
|
||||||
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
|
gridColumn: `${block.calendarGridPoint.dayIndex + 2}`,
|
||||||
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
|
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
|
||||||
width: `calc(100% / ${block.totalColumns})`,
|
width: `calc(100% / ${block.totalColumns})`,
|
||||||
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
|
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
|
||||||
padding: '0px 10px 4px 0px',
|
padding: '0px 10px 4px 0px',
|
||||||
@@ -203,7 +169,9 @@ function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsPro
|
|||||||
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
|
||||||
timeAndLocation={block.componentProps.timeAndLocation}
|
timeAndLocation={block.componentProps.timeAndLocation}
|
||||||
status={block.componentProps.status}
|
status={block.componentProps.status}
|
||||||
colors={block.componentProps.colors}
|
// TODO: Change to block.componentProps.colors when colors are integrated to the rest of the project
|
||||||
|
colors={getCourseColors('emerald', 500) /* block.componentProps.colors */}
|
||||||
|
onClick={() => setCourse(block.course)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import CourseStatus from '@views/components/common/CourseStatus/CourseStatus';
|
|||||||
import Divider from '@views/components/common/Divider/Divider';
|
import Divider from '@views/components/common/Divider/Divider';
|
||||||
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
|
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
|
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import calIcon from 'src/assets/logo.png';
|
import calIcon from 'src/assets/logo.png';
|
||||||
|
|
||||||
@@ -12,12 +13,13 @@ 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';
|
||||||
|
|
||||||
/**
|
const handleOpenOptions = async () => {
|
||||||
* Renders the header component for the calendar.
|
const url = chrome.runtime.getURL('/src/pages/options/index.html');
|
||||||
* @returns The CalendarHeader component.
|
await openTabFromContentScript(url);
|
||||||
*/
|
};
|
||||||
const CalendarHeader = () => (
|
|
||||||
<div className='min-h-79px min-w-672px flex px-0 py-15'>
|
const CalendarHeader = ( { totalHours, totalCourses, scheduleName } ) => (
|
||||||
|
<div className='min-h-79px min-w-672px w-full flex px-0 py-15'>
|
||||||
<div className='flex flex-row gap-20'>
|
<div className='flex flex-row gap-20'>
|
||||||
<div className='flex gap-10'>
|
<div className='flex gap-10'>
|
||||||
<div className='flex gap-1'>
|
<div className='flex gap-1'>
|
||||||
@@ -47,7 +49,7 @@ const CalendarHeader = () => (
|
|||||||
<div className='flex flex-row'>
|
<div className='flex flex-row'>
|
||||||
<Button variant='single' icon={UndoIcon} color='ut-black' />
|
<Button variant='single' icon={UndoIcon} color='ut-black' />
|
||||||
<Button variant='single' icon={RedoIcon} color='ut-black' />
|
<Button variant='single' icon={RedoIcon} color='ut-black' />
|
||||||
<Button variant='single' icon={SettingsIcon} color='ut-black' />
|
<Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
import createSchedule from '@pages/background/lib/createSchedule';
|
||||||
|
import switchSchedule from '@pages/background/lib/switchSchedule';
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import List from '@views/components/common/List/List';
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
import ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem';
|
import React, { useEffect,useState } from 'react';
|
||||||
import Text from '@views/components/common/Text/Text';
|
|
||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
import AddSchedule from '~icons/material-symbols/add';
|
import AddSchedule from '~icons/material-symbols/add';
|
||||||
|
|
||||||
/**
|
import List from '../../common/List/List';
|
||||||
* Props for the CalendarSchedules component.
|
import ScheduleListItem from '../../common/ScheduleListItem/ScheduleListItem';
|
||||||
*/
|
import Text from '../../common/Text/Text';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
dummySchedules?: UserSchedule[];
|
dummySchedules?: UserSchedule[];
|
||||||
@@ -22,11 +23,39 @@ export type Props = {
|
|||||||
* @returns The rendered component.
|
* @returns The rendered component.
|
||||||
*/
|
*/
|
||||||
export function CalendarSchedules(props: Props) {
|
export function CalendarSchedules(props: Props) {
|
||||||
const [activeScheduleIndex, setActiveScheduleIndex] = useState(props.dummyActiveIndex || 0);
|
const [activeScheduleIndex, setActiveScheduleIndex] = useState(0);
|
||||||
const [schedules, setSchedules] = useState(props.dummySchedules || []);
|
const [newSchedule, setNewSchedule] = useState('');
|
||||||
|
const [activeSchedule, schedules] = useSchedules();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const index = schedules.findIndex(schedule => schedule.name === activeSchedule.name);
|
||||||
|
if (index !== -1) {
|
||||||
|
setActiveScheduleIndex(index);
|
||||||
|
}
|
||||||
|
}, [activeSchedule, schedules]);
|
||||||
|
|
||||||
|
const handleKeyDown = event => {
|
||||||
|
if (event.code === 'Enter') {
|
||||||
|
createSchedule(newSchedule);
|
||||||
|
setNewSchedule('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScheduleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setNewSchedule(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectItem = (index: number) => {
|
||||||
|
setActiveScheduleIndex(index);
|
||||||
|
switchSchedule(schedules[index].name);
|
||||||
|
};
|
||||||
|
|
||||||
const scheduleComponents = schedules.map((schedule, index) => (
|
const scheduleComponents = schedules.map((schedule, index) => (
|
||||||
<ScheduleListItem active={index === activeScheduleIndex} name={schedule.name} />
|
<ScheduleListItem
|
||||||
|
active={index === activeScheduleIndex}
|
||||||
|
name={schedule.name}
|
||||||
|
onClick={() => selectItem(index)}
|
||||||
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,8 +68,16 @@ export function CalendarSchedules(props: Props) {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='flex flex-col space-y-2.5'>
|
||||||
<List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={30} listWidth={240} />
|
<List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={30} listWidth={240} />
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
placeholder='Enter new schedule'
|
||||||
|
value={newSchedule}
|
||||||
|
onChange={handleScheduleInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useEffect,useState } from 'react';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
|
* Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
|
||||||
@@ -19,9 +19,9 @@ export interface ListProps {
|
|||||||
gap: number; // Impacts the spacing between items in the list
|
gap: number; // Impacts the spacing between items in the list
|
||||||
}
|
}
|
||||||
|
|
||||||
function initial(draggableElements: any[] = []) {
|
function initial(count: number, draggableElements: any[] = []) {
|
||||||
return draggableElements.map((element, index) => ({
|
return draggableElements.map((element, index) => ({
|
||||||
id: `id:${index}`,
|
id: `id:${index + count}`,
|
||||||
content: element as ReactElement,
|
content: element as ReactElement,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,16 @@ const Row: React.FC<RowProps> = React.memo(({ data: { items, gap }, index, style
|
|||||||
* <List draggableElements={elements} />
|
* <List draggableElements={elements} />
|
||||||
*/
|
*/
|
||||||
const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight, listWidth, gap = 12 }: ListProps) => {
|
const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight, listWidth, gap = 12 }: ListProps) => {
|
||||||
const [items, setItems] = useState(() => initial(draggableElements));
|
const [items, setItems] = useState(() => initial(0, draggableElements));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setItems((prevItems) => {
|
||||||
|
const prevItemIds = prevItems.map(item => item.id);
|
||||||
|
const newElements = draggableElements.filter((_, index) => !prevItemIds.includes(`id:${index}`));
|
||||||
|
const newItems = initial(prevItems.length, newElements);
|
||||||
|
return [...prevItems, ...newItems];
|
||||||
|
});
|
||||||
|
}, [draggableElements]);
|
||||||
|
|
||||||
const onDragEnd = useCallback(
|
const onDragEnd = useCallback(
|
||||||
result => {
|
result => {
|
||||||
@@ -113,8 +122,7 @@ const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight,
|
|||||||
const transform = style?.transform;
|
const transform = style?.transform;
|
||||||
|
|
||||||
if (snapshot.isDragging && transform) {
|
if (snapshot.isDragging && transform) {
|
||||||
console.log(transform);
|
let [, , y] = transform.match(/translate\(([-\d]+)px, ([-\d]+)px\)/) || [];
|
||||||
let [, _x, y] = transform.match(/translate\(([-\d]+)px, ([-\d]+)px\)/) || [];
|
|
||||||
|
|
||||||
style.transform = `translate3d(0px, ${y}px, 0px)`; // Apply constrained y value
|
style.transform = `translate3d(0px, ${y}px, 0px)`; // Apply constrained y value
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,15 @@ export type Props = {
|
|||||||
active?: boolean;
|
active?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
dragHandleProps?: any;
|
dragHandleProps?: any;
|
||||||
|
onClick?: (index) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
||||||
*/
|
*/
|
||||||
export default function ScheduleListItem(props: Props) {
|
export default function ScheduleListItem(props: Props) {
|
||||||
const { dragHandleProps } = props;
|
const { dragHandleProps, onClick } = props;
|
||||||
console.log(props);
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...props.style }} className='items-center'>
|
<div style={{ ...props.style }} className='items-center'>
|
||||||
<li className='w-100% flex cursor-pointer items-center self-stretch justify-left text-ut-burntorange'>
|
<li className='w-100% flex cursor-pointer items-center self-stretch justify-left text-ut-burntorange'>
|
||||||
@@ -31,7 +32,10 @@ export default function ScheduleListItem(props: Props) {
|
|||||||
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
||||||
</div>
|
</div>
|
||||||
<div className='inline-flex items-center justify-center gap-1.5'>
|
<div className='inline-flex items-center justify-center gap-1.5'>
|
||||||
<div className='h-5.5 w-5.5 flex items-center justify-center border-2px border-current rounded-full btn-transition group-active:scale-95'>
|
<div
|
||||||
|
className='h-5.5 w-5.5 flex items-center justify-center border-2px border-current rounded-full btn-transition group-active:scale-95'
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'bg-current h-3 w-3 rounded-full transition tansform scale-100 ease-out-expo duration-250',
|
'bg-current h-3 w-3 rounded-full transition tansform scale-100 ease-out-expo duration-250',
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ export const handleOpenCalendar = async () => {
|
|||||||
*/
|
*/
|
||||||
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
|
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
|
||||||
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
|
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
|
||||||
const [courseAdded, setCourseAdded] = useState<boolean>(
|
const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId);
|
||||||
activeSchedule.courses.some(course => course.uniqueId === uniqueId)
|
|
||||||
);
|
|
||||||
|
|
||||||
const instructorString = instructors
|
const instructorString = instructors
|
||||||
.map(instructor => {
|
.map(instructor => {
|
||||||
@@ -82,7 +80,6 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, a
|
|||||||
} else {
|
} else {
|
||||||
await removeCourse(activeSchedule.name, course);
|
await removeCourse(activeSchedule.name, course);
|
||||||
}
|
}
|
||||||
setCourseAdded(!courseAdded);
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className='w-full pb-3 pt-6'>
|
<div className='w-full pb-3 pt-6'>
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import type { Course, StatusType } from '@shared/types/Course';
|
||||||
|
import type { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||||
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
||||||
|
|
||||||
import useSchedules from './useSchedules';
|
import useSchedules from './useSchedules';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const dayToNumber: { [day: string]: number } = {
|
const dayToNumber: { [day: string]: number } = {
|
||||||
Monday: 0,
|
Monday: 0,
|
||||||
Tuesday: 1,
|
Tuesday: 1,
|
||||||
@@ -16,17 +21,27 @@ interface CalendarGridPoint {
|
|||||||
endIndex: number;
|
endIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface componentProps {
|
||||||
|
calendarCourseCellProps: CalendarCourseCellProps;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return type of useFlattenedCourseSchedule
|
* Return type of useFlattenedCourseSchedule
|
||||||
*/
|
*/
|
||||||
export interface CalendarGridCourse {
|
export interface CalendarGridCourse {
|
||||||
calendarGridPoint: CalendarGridPoint;
|
calendarGridPoint: CalendarGridPoint;
|
||||||
componentProps: CalendarCourseCellProps;
|
componentProps: CalendarCourseCellProps;
|
||||||
|
course: Course;
|
||||||
gridColumnStart?: number;
|
gridColumnStart?: number;
|
||||||
gridColumnEnd?: number;
|
gridColumnEnd?: number;
|
||||||
totalColumns?: number;
|
totalColumns?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FlattenedCourseSchedule {
|
||||||
|
courseCells: CalendarGridCourse[];
|
||||||
|
activeSchedule?: UserSchedule;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts minutes to an index value.
|
* Converts minutes to an index value.
|
||||||
* @param minutes The number of minutes.
|
* @param minutes The number of minutes.
|
||||||
@@ -38,74 +53,168 @@ export const convertMinutesToIndex = (minutes: number): number => Math.floor((mi
|
|||||||
* Get the active schedule, and convert it to be render-able into a calendar.
|
* Get the active schedule, and convert it to be render-able into a calendar.
|
||||||
* @returns CalendarGridCourse
|
* @returns CalendarGridCourse
|
||||||
*/
|
*/
|
||||||
export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
|
export function useFlattenedCourseSchedule(): FlattenedCourseSchedule {
|
||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
const { courses } = activeSchedule;
|
|
||||||
|
|
||||||
return courses
|
if (!activeSchedule) {
|
||||||
.flatMap(course => {
|
return {
|
||||||
const {
|
courseCells: [] as CalendarGridCourse[],
|
||||||
status,
|
activeSchedule: new UserSchedule([], 'Something may have went wrong', 0),
|
||||||
department,
|
} as FlattenedCourseSchedule;
|
||||||
instructors,
|
}
|
||||||
schedule: { meetings },
|
|
||||||
} = course;
|
|
||||||
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
|
|
||||||
|
|
||||||
if (meetings.length === 0) {
|
if (activeSchedule.courses.length === 0) {
|
||||||
// asynch, online course
|
return {
|
||||||
return [
|
courseCells: [] as CalendarGridCourse[],
|
||||||
{
|
activeSchedule
|
||||||
calendarGridPoint: {
|
} satisfies FlattenedCourseSchedule;
|
||||||
dayIndex: 0,
|
|
||||||
startIndex: 0,
|
|
||||||
endIndex: 0,
|
|
||||||
},
|
|
||||||
componentProps: {
|
|
||||||
courseDeptAndInstr,
|
|
||||||
status,
|
|
||||||
colors: {
|
|
||||||
// TODO: figure out colors - these are defaults
|
|
||||||
primaryColor: 'ut-gray',
|
|
||||||
secondaryColor: 'ut-gray',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// in-person
|
}
|
||||||
return meetings.flatMap(meeting => {
|
|
||||||
const { days, startTime, endTime, location } = meeting;
|
|
||||||
const time = meeting.getTimeString({ separator: '-', capitalize: true });
|
|
||||||
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
|
|
||||||
|
|
||||||
return days.map(d => ({
|
const { courses, name, hours } = activeSchedule;
|
||||||
calendarGridPoint: {
|
|
||||||
dayIndex: dayToNumber[d],
|
const processedCourses = courses.flatMap((course: Course) => {
|
||||||
startIndex: convertMinutesToIndex(startTime),
|
const { status, courseDeptAndInstr, meetings } = extractCourseInfo(course);
|
||||||
endIndex: convertMinutesToIndex(endTime),
|
|
||||||
},
|
if (meetings.length === 0) {
|
||||||
componentProps: {
|
return processAsyncCourses({ courseDeptAndInstr, status, course });
|
||||||
courseDeptAndInstr,
|
}
|
||||||
timeAndLocation,
|
|
||||||
status,
|
return meetings.flatMap((meeting: CourseMeeting) =>
|
||||||
colors: {
|
processInPersonMeetings(meeting, { courseDeptAndInstr, status, course })
|
||||||
// TODO: figure out colors - these are defaults
|
);
|
||||||
primaryColor: 'ut-orange',
|
}).sort(sortCourses);
|
||||||
secondaryColor: 'ut-orange',
|
|
||||||
},
|
return {
|
||||||
},
|
courseCells: processedCourses as CalendarGridCourse[],
|
||||||
}));
|
activeSchedule: { name, courses, hours } as UserSchedule,
|
||||||
});
|
} satisfies FlattenedCourseSchedule;
|
||||||
})
|
|
||||||
.sort((a: CalendarGridCourse, b: CalendarGridCourse) => {
|
|
||||||
if (a.calendarGridPoint.dayIndex !== b.calendarGridPoint.dayIndex) {
|
|
||||||
return a.calendarGridPoint.dayIndex - b.calendarGridPoint.dayIndex;
|
|
||||||
}
|
|
||||||
if (a.calendarGridPoint.startIndex !== b.calendarGridPoint.startIndex) {
|
|
||||||
return a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex;
|
|
||||||
}
|
|
||||||
return a.calendarGridPoint.endIndex - b.calendarGridPoint.endIndex;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to extract and format basic course information
|
||||||
|
function extractCourseInfo(course: Course) {
|
||||||
|
const {
|
||||||
|
status,
|
||||||
|
department,
|
||||||
|
instructors,
|
||||||
|
schedule: { meetings },
|
||||||
|
} = course;
|
||||||
|
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
|
||||||
|
return { status, courseDeptAndInstr, meetings, course };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to process each in-person class into its distinct meeting objects for calendar grid
|
||||||
|
*/
|
||||||
|
function processAsyncCourses({ courseDeptAndInstr, status, course }: { courseDeptAndInstr: string, status: StatusType, course: Course }) {
|
||||||
|
return [{
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: 0,
|
||||||
|
startIndex: 0,
|
||||||
|
endIndex: 0,
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr,
|
||||||
|
status,
|
||||||
|
colors: {
|
||||||
|
primaryColor: 'ut-gray',
|
||||||
|
secondaryColor: 'ut-gray',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
course,
|
||||||
|
}] satisfies CalendarGridCourse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to process each in-person class into its distinct meeting objects for calendar grid
|
||||||
|
*/
|
||||||
|
function processInPersonMeetings( { days, startTime, endTime, location }: CourseMeeting, { courseDeptAndInstr, status, course }) {
|
||||||
|
const time = getTimeString({ separator: '-', capitalize: true }, startTime, endTime);
|
||||||
|
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
|
||||||
|
return days.map(day => ({
|
||||||
|
calendarGridPoint: {
|
||||||
|
dayIndex: dayToNumber[day],
|
||||||
|
startIndex: convertMinutesToIndex(startTime),
|
||||||
|
endIndex: convertMinutesToIndex(endTime),
|
||||||
|
},
|
||||||
|
componentProps: {
|
||||||
|
courseDeptAndInstr,
|
||||||
|
timeAndLocation,
|
||||||
|
status,
|
||||||
|
colors: {
|
||||||
|
primaryColor: 'ut-orange',
|
||||||
|
secondaryColor: 'ut-orange',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
course,
|
||||||
|
})) satisfies CalendarGridCourse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to sort courses for the calendar grid
|
||||||
|
*/
|
||||||
|
function sortCourses(a, b) {
|
||||||
|
const { dayIndex: dayIndexA, startIndex: startIndexA, endIndex: endIndexA } = a.calendarGridPoint;
|
||||||
|
const { dayIndex: dayIndexB, startIndex: startIndexB, endIndex: endIndexB } = b.calendarGridPoint;
|
||||||
|
|
||||||
|
if (dayIndexA !== dayIndexB) {
|
||||||
|
return dayIndexA - dayIndexB;
|
||||||
|
}
|
||||||
|
if (startIndexA !== startIndexB) {
|
||||||
|
return startIndexA - startIndexB;
|
||||||
|
}
|
||||||
|
return endIndexA - endIndexB;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function also present in CourseMeeting object. Wasn't being found at runtime, so I copied it over.
|
||||||
|
*/
|
||||||
|
function getTimeString(options: TimeStringOptions, startTime: number, endTime: number): string {
|
||||||
|
const startHour = Math.floor(startTime / 60);
|
||||||
|
const startMinute = startTime % 60;
|
||||||
|
const endHour = Math.floor(endTime / 60);
|
||||||
|
const endMinute = endTime % 60;
|
||||||
|
|
||||||
|
let startTimeString = '';
|
||||||
|
let endTimeString = '';
|
||||||
|
|
||||||
|
if (startHour === 0) {
|
||||||
|
startTimeString = '12';
|
||||||
|
} else if (startHour > 12) {
|
||||||
|
startTimeString = `${startHour - 12}`;
|
||||||
|
} else {
|
||||||
|
startTimeString = `${startHour}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
startTimeString += startMinute === 0 ? ':00' : `:${startMinute}`;
|
||||||
|
startTimeString += startHour >= 12 ? 'pm' : 'am';
|
||||||
|
|
||||||
|
if (endHour === 0) {
|
||||||
|
endTimeString = '12';
|
||||||
|
} else if (endHour > 12) {
|
||||||
|
endTimeString = `${endHour - 12}`;
|
||||||
|
} else {
|
||||||
|
endTimeString = `${endHour}`;
|
||||||
|
}
|
||||||
|
endTimeString += endMinute === 0 ? ':00' : `:${endMinute}`;
|
||||||
|
endTimeString += endHour >= 12 ? 'pm' : 'am';
|
||||||
|
|
||||||
|
if (options.capitalize) {
|
||||||
|
startTimeString = startTimeString.toUpperCase();
|
||||||
|
endTimeString = endTimeString.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${startTimeString} ${options.separator} ${endTimeString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options to control the format of the time string
|
||||||
|
*/
|
||||||
|
type TimeStringOptions = {
|
||||||
|
/** the separator between the start and end times */
|
||||||
|
separator: string;
|
||||||
|
/** capitalizes the AM/PM */
|
||||||
|
capitalize?: boolean;
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ const messenger = createMessenger<MyMessages>('background');
|
|||||||
*/
|
*/
|
||||||
export async function openTabFromContentScript(url: string) {
|
export async function openTabFromContentScript(url: string) {
|
||||||
messenger
|
messenger
|
||||||
.openNewTab({ url })
|
.openNewTab({ url }) // Fix: Pass the url as a property of an object
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('New tab opened with URL:', url);
|
console.log('New tab opened with URL:', url);
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user