feat: calendar matchings (#173)

* feat: calendar matchings

* fix: build

* refactor: resolve pr comments

* fix: destrucure editorRef

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
Razboy20
2024-03-17 00:32:50 -05:00
committed by GitHub
parent df1849180d
commit 791a42bcd4
23 changed files with 243 additions and 410 deletions

View File

@@ -1,8 +0,0 @@
.scrollableSchedules {
overflow-y: auto; /* Enables vertical scrolling */
}
.scrollableLimit {
overflow-y: auto; /* Enables vertical scrolling */
max-height: 35vh; /* Adjusts based on your needs and testing */
}

View File

@@ -11,7 +11,6 @@ import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSched
import { MessageListener } from 'chrome-extension-toolkit';
import React, { useEffect, useRef, useState } from 'react';
import styles from './Calendar.module.scss';
/**
* A reusable chip component that follows the design system of the extension.
* @returns
@@ -33,8 +32,6 @@ export default function Calendar(): JSX.Element {
});
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
const [sidebarWidth, setSidebarWidth] = useState('20%');
const [scale, setScale] = useState(1);
useEffect(() => {
const listener = new MessageListener<CalendarTabMessages>({
@@ -52,59 +49,21 @@ export default function Calendar(): JSX.Element {
return () => listener.unlisten();
}, [activeSchedule.courses]);
useEffect(() => {
const adjustLayout = () => {
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
const desiredCalendarHeight = 760;
const minSidebarWidthPixels = 230;
const scale = Math.min(1, windowHeight / desiredCalendarHeight);
const sidebarWidthPixels = Math.max(
windowWidth * scale - windowWidth + minSidebarWidthPixels,
minSidebarWidthPixels
);
const newSidebarWidth = `${(sidebarWidthPixels / windowWidth) * 100}%`;
setScale(scale);
setSidebarWidth(newSidebarWidth);
};
adjustLayout();
window.addEventListener('resize', adjustLayout);
return () => window.removeEventListener('resize', adjustLayout);
}, []);
useEffect(() => {
if (course) setShowPopup(true);
}, [course]);
const calendarContainerStyle = {
transform: `scale(${scale})`,
transformOrigin: 'top left',
marginTop: '-20px',
};
return (
<div className='h-screen flex flex-col' style={{ width: 'calc(100% - 1rem)' }}>
<div className='pl-5'>
<CalendarHeader />
</div>
<div className={`flex flex-grow flex-row overflow-hidden pl-4 ${styles.scrollableSchedules}`}>
<div className='sidebar-style' style={{ width: sidebarWidth, padding: '10px 15px 5px 5px' }}>
<div className={`mb-4 ${styles.scrollableLimit}`}>
<CalendarSchedules />
</div>
<Divider orientation='horizontal' size='100%' />
<div className='mt-4'>
<ImportantLinks />
</div>
<div className='h-full w-full flex flex-col'>
<CalendarHeader />
<div className='h-full flex flex-grow overflow-hidden pl-7.5'>
<div className='overflow-auto py-5'>
<CalendarSchedules />
<Divider orientation='horizontal' size='100%' className='my-5' />
<ImportantLinks />
</div>
<div className='flex flex-grow flex-col' style={calendarContainerStyle} ref={calendarRef}>
<div className='flex-grow overflow-auto'>
<div className='flex flex-grow flex-col' ref={calendarRef}>
<div className='flex-grow overflow-auto px-4 pt-6.5'>
<CalendarGrid courseCells={courseCells} setCourse={setCourse} />
</div>
<CalendarBottomBar calendarRef={calendarRef} />

View File

@@ -56,7 +56,11 @@ export default function CalendarCourseCell({
return (
<div
className={clsx('h-full w-full flex justify-center rounded p-2 cursor-pointer', fontColor, className)}
className={clsx(
'h-full min-w-full w-0 flex justify-center rounded p-2 cursor-pointer',
fontColor,
className
)}
style={{
backgroundColor: colors.primaryColor,
}}

View File

@@ -1,139 +0,0 @@
.dayLabelContainer {
display: flex;
flex-direction: row;
height: 13px;
min-width: 40px;
min-height: 13px;
padding-bottom: 15px;
justify-content: center;
align-items: center;
gap: 10px;
flex: 1 0 0;
}
.calendarGrid {
display: grid;
grid-template-columns: auto repeat(5, 6fr);
grid-template-rows: repeat(26, 1fr);
width: 100%;
height: 100%;
}
.calendarRow {
display: flex;
}
.calendar {
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
width: 100%;
height: 100%;
}
.day {
gap: 5px;
color: #bf5700;
text-align: center;
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
margin-top: 20px;
border-right: 1px solid #dadce0;
border-bottom: 1px solid #dadce0;
border-left: 1px solid #dadce0;
}
.timeAndGrid {
display: flex;
}
.timeColumn {
display: flex;
min-height: 573px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
flex: 1 0 0;
border-radius: 0px;
}
.timeBlock {
display: flex;
width: auto;
height: auto;
flex-direction: column;
align-items: flex-end;
.timeLabelContainer {
display: flex;
max-height: 20px;
flex-direction: column;
align-items: flex-end;
gap: 17px;
flex: 1 0 0;
border-radius: var(--border-radius-none, 0px);
}
p {
color: #1a2024;
text-align: left;
height: 6.6px;
align-self: stretch;
margin-top: -10px;
margin-right: 10px;
margin-bottom: 0;
/* Type scale/small */
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
}
.buttonContainer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 10px;
margin-top: auto;
}
.calendarButton {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 6px;
border: none;
background-color: transparent;
color: #333;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
.buttonIcon {
height: 24px;
fill: currentColor;
}
.divider {
height: 30px;
width: 1px;
background-color: grey;
}
.dot {
height: 75%; /* 75% of the container's height */
width: 75%; /* 75% of the container's width */
background-color: #000000; /* Color of the dot */
border-radius: 50%; /* Rounds the corners into a circle */
top: 12.5%; /* Centers the dot vertically */
left: 12.5%; /* Centers the dot horizontally */
}

View File

@@ -1,14 +1,13 @@
import type { Course } from '@shared/types/Course';
import { DAY_MAP } from '@shared/types/CourseMeeting';
import { getCourseColors } from '@shared/util/colors';
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import CalendarCell from '@views/components/calendar/CalendarGridCell/CalendarGridCell';
import Text from '@views/components/common/Text/Text';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import React, { useEffect, useState } from 'react';
import React from 'react';
import styles from './CalendarGrid.module.scss';
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const daysOfWeek = ['MON', 'TUE', 'WED', 'THU', 'FRI'];
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
interface Props {
@@ -17,55 +16,60 @@ interface Props {
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
}
function CalendarHour(hour: number) {
return (
<div className='grid-row-span-2 pr-2'>
<Text variant='small' className='inline-block w-full text-right -translate-y-2.25'>
{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}
</Text>
</div>
);
}
function makeGridRow(row: number, cols: number): JSX.Element {
const hour = hoursOfDay[row];
return (
<>
{CalendarHour(hour)}
<div className='grid-row-span-2 w-4 border-b border-r border-gray-300' />
{[...Array(cols).keys()].map(col => (
<CalendarCell key={`${row}${col}`} row={row} col={col} />
))}
</>
);
}
// TODO: add Saturday class support
/**
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
export default function CalendarGrid({
courseCells,
saturdayClass,
saturdayClass, // TODO: implement/move away from props
setCourse,
}: React.PropsWithChildren<Props>): JSX.Element {
const [grid, setGrid] = useState([]);
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>
</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} />);
}
newGrid.push(row);
}
setGrid(newGrid);
}, []);
return (
<div className={styles.calendarGrid}>
<div className='grid grid-cols-[auto_auto_repeat(5,1fr)] grid-rows-[auto_repeat(26,1fr)] h-full'>
{/* Displaying day labels */}
<div className={styles.timeBlock} />
<div />
<div className='w-4 border-b border-r border-gray-300' />
{daysOfWeek.map(day => (
<div key={day} className={styles.day}>
{day}
<div className='h-4 flex items-end justify-center border-b border-r border-gray-300 pb-1.5'>
<Text key={day} variant='small' className='text text-center text-ut-burntorange' as='div'>
{day}
</Text>
</div>
))}
{grid.map(row => row)}
{[...Array(13).keys()].map(i => makeGridRow(i, 5))}
{CalendarHour(21)}
{Array(6)
.fill(1)
.map(() => (
<div className='h-4 flex items-end justify-center border-r border-gray-300' />
))}
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} /> : null}
</div>
);
@@ -76,6 +80,7 @@ interface AccountForCourseConflictsProps {
setCourse: React.Dispatch<React.SetStateAction<Course | null>>;
}
// TODO: Possibly refactor to be more concise
function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseConflictsProps): JSX.Element[] {
// Groups by dayIndex to identify overlaps
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
@@ -123,7 +128,7 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
return (
<div
key={`${block}`}
key={`${JSON.stringify(block)}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 2}`,
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,

View File

@@ -1,18 +0,0 @@
.calendarCell {
display: flex;
width: 100%;
height: 100%;
min-width: 45px;
min-height: 40px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
border: 1px solid #dadce0;
}
.hourLine {
width: 100%;
height: 1px;
border-radius: 0px;
background: rgba(218, 220, 224, 0.25);
}

View File

@@ -1,19 +1,24 @@
import React from 'react';
import styles from './CalendarGridCell.module.scss';
interface Props {
styleProp: any;
row: number;
col: number;
}
/**
* Component representing each 1 hour time block of a calendar
* @param props
*/
function CalendarCell({ styleProp }: Props): JSX.Element {
function CalendarCell(props: Props): JSX.Element {
return (
<div className={styles.calendarCell} style={styleProp}>
<div className={styles.hourLine} />
<div
className='h-full w-full flex items-center border-b border-r border-gray-300'
style={{
gridColumn: props.col + 3,
gridRow: `${2 * props.row + 2} / ${2 * props.row + 4}`,
}}
>
<div className='h-0 w-full border-t border-gray-300/25' />
</div>
);
}

View File

@@ -2,7 +2,7 @@ import { Status } from '@shared/types/Course';
import { Button } from '@views/components/common/Button/Button';
import CourseStatus from '@views/components/common/CourseStatus/CourseStatus';
import Divider from '@views/components/common/Divider/Divider';
import { LogoIcon } from '@views/components/common/LogoIcon';
import { LargeLogo } from '@views/components/common/LogoIcon';
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
import Text from '@views/components/common/Text/Text';
import useSchedules from '@views/hooks/useSchedules';
@@ -11,9 +11,7 @@ import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript
import React from 'react';
import MenuIcon from '~icons/material-symbols/menu';
import RedoIcon from '~icons/material-symbols/redo';
import SettingsIcon from '~icons/material-symbols/settings';
import UndoIcon from '~icons/material-symbols/undo';
import RefreshIcon from '~icons/material-symbols/refresh';
/**
* Opens the options page in a new tab.
@@ -32,44 +30,34 @@ export default function CalendarHeader(): JSX.Element {
const [activeSchedule] = useSchedules();
return (
<div className='min-h-79px min-w-672px w-full flex px-0 py-5'>
<div className='w-full flex flex-row gap-20'>
<div className='flex gap-10'>
<div className='flex gap-1'>
<Button className='self-center' variant='single' icon={MenuIcon} color='ut-gray' />
<div className='flex items-center gap-2'>
<LogoIcon />
<div className='flex flex-col whitespace-nowrap'>
<Text className='text-ut-burntorange text-lg! font-medium!'>UT Registration</Text>
<Text className='text-ut-orange text-lg! font-medium!'>Plus</Text>
</div>
</div>
</div>
<Divider className='self-center' size='2.5rem' orientation='vertical' />
<div className='flex flex-col self-center'>
<ScheduleTotalHoursAndCourses
scheduleName={activeSchedule.name}
totalHours={activeSchedule.hours}
totalCourses={activeSchedule.courses.length}
/>
<Text variant='h4' className='text-gray text-xs! font-medium! leading-normal!'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
</div>
</div>
<div className='ml-auto flex flex-row items-center justify-end space-x-8'>
<div className='flex flex-row space-x-4'>
<CourseStatus size='small' status={Status.WAITLISTED} />
<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 className='flex items-center gap-5 border-b px-7 py-4'>
<Button variant='single' icon={MenuIcon} color='ut-gray' />
<LargeLogo />
<Divider className='mx-4 self-center' size='2.5rem' orientation='vertical' />
<div className='flex-grow'>
<ScheduleTotalHoursAndCourses
scheduleName={activeSchedule.name}
totalHours={activeSchedule.hours}
totalCourses={activeSchedule.courses.length}
/>
<div className='flex items-center gap-1'>
<Text variant='mini' className='text-ut-gray'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
<button className='inline-block h-4 w-4 bg-transparent p-0 btn'>
<RefreshIcon className='h-4 w-4 animate-duration-800 text-ut-black' />
</button>
</div>
</div>
<div className='flex flex-row items-center justify-end gap-6'>
<CourseStatus size='small' status={Status.WAITLISTED} />
<CourseStatus size='small' status={Status.CLOSED} />
<CourseStatus size='small' status={Status.CANCELLED} />
{/* <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>
);
}

View File

@@ -1,68 +1,35 @@
import { background } from '@shared/messages';
import createSchedule from '@pages/background/lib/createSchedule';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { Button } from '@views/components/common/Button/Button';
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 useSchedules, { getActiveSchedule, switchSchedule } from '@views/hooks/useSchedules';
import React, { useEffect, useState } from 'react';
import React from 'react';
import AddSchedule from '~icons/material-symbols/add';
/**
* Props for the CalendarSchedules component.
*/
export type Props = {
style?: React.CSSProperties;
dummySchedules?: UserSchedule[];
dummyActiveIndex?: number;
};
/**
* Renders a component that displays a list of schedules.
*
* @param props - The component props.
* @returns The rendered component.
*/
export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: Props) {
const [activeScheduleIndex, setActiveScheduleIndex] = useState(0);
const [newSchedule, setNewSchedule] = useState('');
const [activeSchedule, schedules] = useSchedules();
useEffect(() => {
const index = schedules.findIndex(schedule => schedule.id === activeSchedule.id);
if (index !== -1) {
setActiveScheduleIndex(index);
}
}, [activeSchedule, schedules]);
const handleKeyDown = event => {
if (event.code === 'Enter') {
background.createSchedule({ scheduleName: newSchedule }).then(() => {
setNewSchedule('');
});
}
};
const handleScheduleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNewSchedule(e.target.value);
};
const fixBuildError = {
dummySchedules,
dummyActiveIndex,
};
console.log(fixBuildError);
export function CalendarSchedules() {
const [, schedules] = useSchedules();
return (
<div style={{ ...style }} className='items-center'>
<div className='min-w-full w-0 items-center'>
<div className='m0 m-b-2 w-full flex justify-between'>
<Text variant='h3'>MY SCHEDULES</Text>
<div className='cursor-pointer items-center justify-center btn-transition -ml-1.5 hover:text-zinc-400'>
<Text variant='h3'>
<AddSchedule />
</Text>
</div>
<Button
variant='single'
color='theme-black'
className='h-fit p-0 btn'
onClick={() => createSchedule('New Schedule')}
>
<AddSchedule className='h-6 w-6' />
</Button>
</div>
<div className='flex flex-col space-y-2.5'>
<List
@@ -88,13 +55,6 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
/>
)}
</List>
<input
type='text'
placeholder='Enter new schedule'
value={newSchedule}
onChange={handleScheduleInputChange}
onKeyDown={handleKeyDown}
/>
</div>
</div>
);

View File

@@ -52,7 +52,7 @@ export default function ImportantLinks({ className }: Props): JSX.Element {
<a
key={link.text}
href={link.url}
className='flex items-center gap-0.5 text-ut-burntorange'
className='flex items-center gap-0.5 text-ut-burntorange underline-offset-2 hover:underline'
target='_blank'
rel='noreferrer'
>