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:
Som Gupta
2024-02-29 20:15:45 -06:00
committed by doprz
parent 3a48859ddd
commit a99a55788a
15 changed files with 899 additions and 301 deletions

View File

@@ -1,9 +1,15 @@
import type { Course } from '@shared/types/Course';
import { CalendarBottomBar } from '@views/components/calendar/CalendarBottomBar/CalendarBottomBar';
import CalendarGrid from '@views/components/calendar/CalendarGrid/CalendarGrid';
import CalendarHeader from '@views/components/calendar/CalendarHeader/CalenderHeader';
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
import ImportantLinks from '@views/components/calendar/ImportantLinks';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
import React from 'react';
import { ExampleCourse } from 'src/stories/components/PopupCourseBlock.stories';
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];
@@ -16,9 +22,12 @@ interface Props {
* @returns
*/
export function Calendar(): JSX.Element {
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
const [course, setCourse] = React.useState<Course | null>(null);
return (
<>
<CalendarHeader />
<div className = 'flex flex-col'>
<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='min-h-[30%] flex flex-col items-start gap-2.5 p-5 pl-7'>
<div className='min-h-[30%]'>
@@ -26,15 +35,18 @@ export function Calendar(): JSX.Element {
</div>
<ImportantLinks />
</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'>
<CalendarGrid />
<CalendarGrid courseCells = {courseCells} setCourse={setCourse}/>
</div>
<div>
<CalendarBottomBar />
</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>
);
}

View File

@@ -19,6 +19,7 @@ export interface CalendarCourseCellProps {
status: StatusType;
colors: CourseColors;
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}
/**
@@ -39,6 +40,7 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
status,
colors,
className,
onClick,
}: CalendarCourseCellProps) => {
let rightIcon: React.ReactNode | null = null;
if (status === Status.WAITLISTED) {
@@ -54,10 +56,11 @@ const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
return (
<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={{
backgroundColor: colors.primaryColor,
}}
onClick={onClick}
>
<div className='flex flex-1 flex-col gap-1 overflow-x-hidden'>
<Text

View File

@@ -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 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';
@@ -10,34 +12,19 @@ import { DAY_MAP } from 'src/shared/types/CourseMeeting';
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 {
courseCells?: CalendarGridCourse[];
saturdayClass?: boolean;
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
}
/**
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
const [grid, setGrid] = useState([]);
function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWithChildren<Props>): JSX.Element {
// const [grid, setGrid] = useState([]);
const calendarRef = useRef(null); // Create a ref for the calendar grid
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
@@ -76,39 +63,32 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
});
}; */
useEffect(() => {
const newGrid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
let styleProp = {
gridColumn: '1',
gridRow: `${2 * i + 2}`,
};
row.push(
<div key={hour} className={styles.timeBlock} style={styleProp}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
// TODO: Change to useMemo hook once we start calculating grid size based on if there's a Saturday class or not
const grid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
let styleProp = {
gridColumn: '1',
gridRow: `${2 * i + 2}`,
};
row.push(
<div key={hour} className={styles.timeBlock} style={styleProp}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
);
for (let k = 0; k < 5; k++) {
// let shouldRender = false;
styleProp = {
gridColumn: `${k + 2}`,
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
};
/* let shouldRenderChild = courseCells[iterator]?.calendarGridPoint &&
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);
</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} />);
}
setGrid(newGrid);
}, [hoursOfDay]);
grid.push(row);
}
return (
<div className={styles.calendarGrid}>
@@ -120,23 +100,7 @@ function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Pr
</div>
))}
{grid.map((row, rowIndex) => row)}
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} /> : 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>
)) */}
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse}/> : null}
</div>
);
}
@@ -145,9 +109,10 @@ export default CalendarGrid;
interface AccountForCourseConflictsProps {
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
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
const { dayIndex } = cell.calendarGridPoint;
@@ -173,11 +138,9 @@ function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsPro
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
if (isOverlapping) {
console.log('Found overlapping element');
// Adjust columnIndex to not overlap with the otherCell
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
columnIndex = otherCell.gridColumnStart + 1;
console.log(columnIndex);
}
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 => (
<div
key={`${block}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
gridColumn: `${block.calendarGridPoint.dayIndex + 2}`,
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
width: `calc(100% / ${block.totalColumns})`,
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
padding: '0px 10px 4px 0px',
@@ -203,7 +169,9 @@ function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsPro
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
timeAndLocation={block.componentProps.timeAndLocation}
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>
));

View File

@@ -4,6 +4,7 @@ import CourseStatus from '@views/components/common/CourseStatus/CourseStatus';
import Divider from '@views/components/common/Divider/Divider';
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
import Text from '@views/components/common/Text/Text';
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
import React from 'react';
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 UndoIcon from '~icons/material-symbols/undo';
/**
* Renders the header component for the calendar.
* @returns The CalendarHeader component.
*/
const CalendarHeader = () => (
<div className='min-h-79px min-w-672px flex px-0 py-15'>
const handleOpenOptions = async () => {
const url = chrome.runtime.getURL('/src/pages/options/index.html');
await openTabFromContentScript(url);
};
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 gap-10'>
<div className='flex gap-1'>
@@ -47,7 +49,7 @@ const CalendarHeader = () => (
<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' />
<Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions}/>
</div>
</div>
</div>

View File

@@ -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 List from '@views/components/common/List/List';
import ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem';
import Text from '@views/components/common/Text/Text';
import React, { useState } from 'react';
import useSchedules from '@views/hooks/useSchedules';
import React, { useEffect,useState } from 'react';
import AddSchedule from '~icons/material-symbols/add';
/**
* Props for the CalendarSchedules component.
*/
import List from '../../common/List/List';
import ScheduleListItem from '../../common/ScheduleListItem/ScheduleListItem';
import Text from '../../common/Text/Text';
export type Props = {
style?: React.CSSProperties;
dummySchedules?: UserSchedule[];
@@ -22,11 +23,39 @@ export type Props = {
* @returns The rendered component.
*/
export function CalendarSchedules(props: Props) {
const [activeScheduleIndex, setActiveScheduleIndex] = useState(props.dummyActiveIndex || 0);
const [schedules, setSchedules] = useState(props.dummySchedules || []);
const [activeScheduleIndex, setActiveScheduleIndex] = useState(0);
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) => (
<ScheduleListItem active={index === activeScheduleIndex} name={schedule.name} />
<ScheduleListItem
active={index === activeScheduleIndex}
name={schedule.name}
onClick={() => selectItem(index)}
/>
));
return (
@@ -39,8 +68,16 @@ export function CalendarSchedules(props: Props) {
</Text>
</div>
</div>
<List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={30} listWidth={240} />
<div className='flex flex-col space-y-2.5'>
<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>
);
}