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:
@@ -16,7 +16,7 @@ import RefreshIcon from '~icons/material-symbols/refresh';
|
||||
import SettingsIcon from '~icons/material-symbols/settings';
|
||||
|
||||
import CourseStatus from './common/CourseStatus/CourseStatus';
|
||||
import { LogoIcon } from './common/LogoIcon';
|
||||
import { SmallLogo } from './common/LogoIcon';
|
||||
import PopupCourseBlock from './common/PopupCourseBlock/PopupCourseBlock';
|
||||
import ScheduleDropdown from './common/ScheduleDropdown/ScheduleDropdown';
|
||||
import ScheduleListItem from './common/ScheduleListItem/ScheduleListItem';
|
||||
@@ -44,16 +44,7 @@ export default function PopupMain(): JSX.Element {
|
||||
<div className='h-screen max-h-full flex flex-col bg-white'>
|
||||
<div className='p-5 py-3.5'>
|
||||
<div className='flex items-center justify-between bg-white'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<LogoIcon />
|
||||
<div className='flex flex-col'>
|
||||
<span className='text-lg text-ut-burntorange font-medium leading-[18px]'>
|
||||
UT Registration
|
||||
<br />
|
||||
</span>
|
||||
<span className='text-lg text-ut-orange font-medium leading-[18px]'>Plus</span>
|
||||
</div>
|
||||
</div>
|
||||
<SmallLogo />
|
||||
<div className='flex items-center gap-2.5'>
|
||||
<button className='bg-ut-burntorange px-2 py-1.25 btn' onClick={handleCalendarOpenOnClick}>
|
||||
<CalendarIcon className='size-6 text-white' />
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center'>
|
||||
<HDialog.Panel
|
||||
className={clsx(
|
||||
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-ut-offwhite rounded-lg bg-white shadow-xl ml-[calc(100vw-100%)] mt-[calc(100vw-100%)]',
|
||||
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%)] mt-[calc(100vw-100%)]',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,18 +3,21 @@ import 'uno.css';
|
||||
|
||||
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
|
||||
import { MessageListener } from 'chrome-extension-toolkit';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import styles from './ExtensionRoot.module.scss';
|
||||
|
||||
interface Props {
|
||||
testId?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper component for the extension elements that adds some basic styling to them
|
||||
*/
|
||||
export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JSX.Element {
|
||||
// TODO: move out of ExtensionRoot
|
||||
useEffect(() => {
|
||||
const tabInfoListener = new MessageListener<TabInfoMessages>({
|
||||
getTabInfo: ({ sendResponse }) => {
|
||||
@@ -31,7 +34,7 @@ export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JS
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.extensionRoot} data-testid={props.testId}>
|
||||
<div className={clsx(styles.extensionRoot, props.className)} data-testid={props.testId}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { SVGProps } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function LogoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
export function LogoIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<svg width='40' height='40' viewBox='0 0 40 40' fill='none' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<circle cx='20' cy='20' r='20' fill='#BF5700' />
|
||||
<circle cx='20' cy='20' r='15.5' stroke='white' strokeWidth='3' />
|
||||
<rect x='18' y='10' width='4' height='19.5489' fill='white' />
|
||||
@@ -11,3 +11,27 @@ export function LogoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function SmallLogo(): JSX.Element {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<LogoIcon />
|
||||
<div className='flex flex-col text-lg font-medium leading-[1em]'>
|
||||
<p className='text-ut-burntorange'>UT Registration</p>
|
||||
<p className='text-ut-orange'>Plus</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LargeLogo(): JSX.Element {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<LogoIcon className='h-12 w-12' />
|
||||
<div className='flex flex-col text-[1.35rem] font-medium leading-[1em]'>
|
||||
<p className='text-ut-burntorange'>UT Registration</p>
|
||||
<p className='text-ut-orange'>Plus</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import deleteSchedule from '@pages/background/lib/deleteSchedule';
|
||||
import renameSchedule from '@pages/background/lib/renameSchedule';
|
||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import useSchedules from '@views/hooks/useSchedules';
|
||||
import clsx from 'clsx';
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import XIcon from '~icons/material-symbols/close';
|
||||
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
||||
|
||||
/**
|
||||
@@ -19,31 +22,80 @@ export type Props = {
|
||||
/**
|
||||
* This is a reusable dropdown component that can be used to toggle the visiblity of information
|
||||
*/
|
||||
export default function ScheduleListItem({ style, schedule, dragHandleProps, onClick }: Props): JSX.Element {
|
||||
export default function ScheduleListItem({ schedule, dragHandleProps, onClick }: Props): JSX.Element {
|
||||
const [activeSchedule] = useSchedules();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editorValue, setEditorValue] = useState(schedule.name);
|
||||
|
||||
const editorRef = React.useRef<HTMLInputElement>(null);
|
||||
const { current: editor } = editorRef;
|
||||
useEffect(() => {
|
||||
setEditorValue(schedule.name);
|
||||
|
||||
if (isEditing && editor) {
|
||||
editor.focus();
|
||||
editor.setSelectionRange(0, editor.value.length);
|
||||
}
|
||||
}, [isEditing, schedule.name, editor]);
|
||||
|
||||
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
|
||||
|
||||
const handleBlur = () => {
|
||||
if (editorValue.trim() !== '') {
|
||||
schedule.name = editorValue.trim();
|
||||
renameSchedule(schedule.id, schedule.name);
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ ...style }} className='items-center rounded bg-white'>
|
||||
<li className='w-100% flex cursor-pointer items-center self-stretch justify-left text-ut-burntorange'>
|
||||
<div className='flex justify-center'>
|
||||
<div className='rounded bg-white'>
|
||||
<li className='w-full flex cursor-pointer items-center text-ut-burntorange'>
|
||||
<div className='h-full cursor-move focusable' {...dragHandleProps}>
|
||||
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
||||
</div>
|
||||
<div className='group flex flex-1 items-center overflow-x-hidden'>
|
||||
<div
|
||||
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
||||
{...dragHandleProps}
|
||||
className='flex flex-grow items-center gap-1.5 overflow-x-hidden'
|
||||
onClick={(...e) => !isEditing && onClick(...e)}
|
||||
>
|
||||
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
|
||||
</div>
|
||||
<div className='group inline-flex items-center justify-center gap-1.5' onClick={onClick}>
|
||||
<div
|
||||
className={clsx(
|
||||
'h-5.5 w-5.5 relative border-2px border-current rounded-full btn-transition group-active:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition tansform scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
|
||||
'h-5.5 w-5.5 relative border-2px border-current rounded-full btn-transition group-active:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition tansform-gpu scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
|
||||
{
|
||||
'after:(scale-0! opacity-0 ease-in-out! duration-200!)': !isActive,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<Text variant='p'>{schedule.name}</Text>
|
||||
{isEditing && (
|
||||
<Text
|
||||
variant='p'
|
||||
as='input'
|
||||
className='mr-1 flex-1 px-0.5 outline-blue-500 -ml-0.5'
|
||||
value={editorValue}
|
||||
onChange={e => setEditorValue(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') handleBlur();
|
||||
if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
}
|
||||
}}
|
||||
onBlur={handleBlur}
|
||||
ref={editorRef}
|
||||
/>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<Text variant='p' className='flex-1 truncate' onDoubleClick={() => setIsEditing(true)}>
|
||||
{schedule.name}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<XIcon
|
||||
className='invisible h-5 w-5 text-ut-red group-hover:visible'
|
||||
onClick={() => deleteSchedule(schedule.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -21,14 +21,14 @@ export default function ScheduleTotalHoursAndCourses({
|
||||
totalCourses,
|
||||
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
|
||||
return (
|
||||
<div className='min-w-64 flex content-center items-baseline gap-2 whitespace-nowrap uppercase'>
|
||||
<Text className='text-ut-burntorange' variant='h1' as='span'>
|
||||
<div className='min-w-64 flex items-center gap-2.5 whitespace-nowrap'>
|
||||
<Text className='text-ut-burntorange uppercase' variant='h1' as='span'>
|
||||
{`${scheduleName}: `}
|
||||
</Text>
|
||||
<Text variant='h3' as='div' className='flex flex-row items-center gap-2 text-theme-black'>
|
||||
{totalHours} {totalHours === 1 ? 'HOUR' : 'HOURS'}
|
||||
<Text variant='h4' as='span' className='text-ut-black'>
|
||||
{totalCourses} {totalCourses === 1 ? 'COURSE' : 'COURSES'}
|
||||
{totalHours} {totalHours === 1 ? 'Hour' : 'Hours'}
|
||||
<Text variant='h4' as='span' className='text-ut-black capitalize'>
|
||||
{totalCourses} {totalCourses === 1 ? 'Course' : 'Courses'}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,13 @@
|
||||
.mini {
|
||||
font-size: 0.79rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.88875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.p {
|
||||
@@ -25,11 +27,13 @@
|
||||
.h4 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h3-course {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
line-height: 100%; /* 0.6875rem */
|
||||
}
|
||||
|
||||
@@ -37,6 +41,7 @@
|
||||
font-size: 1.26563rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h2-course {
|
||||
@@ -49,16 +54,19 @@
|
||||
.h2 {
|
||||
font-size: 1.42375rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h1-course {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.h1 {
|
||||
font-size: 1.60188rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropsOf, ReactTag } from '@headlessui/react/dist/types';
|
||||
import clsx from 'clsx';
|
||||
import type { ElementType, ReactNode } from 'react';
|
||||
import type { ElementType, ReactNode, Ref } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Text.module.scss';
|
||||
@@ -13,6 +13,7 @@ type CleanProps<TTag extends ReactTag, TOmitableProps extends PropertyKey = neve
|
||||
type OurProps<TTag extends ReactTag> = {
|
||||
as?: TTag;
|
||||
children?: ReactNode;
|
||||
ref?: React.ForwardedRef<React.ElementRef<TTag>>;
|
||||
};
|
||||
|
||||
type AsProps<TTag extends ReactTag, TOverrides = {}> = CleanProps<TTag, keyof TOverrides> & OurProps<TTag> & TOverrides;
|
||||
@@ -36,14 +37,14 @@ export type TextProps<TTag extends ElementType = 'span'> = PropsOf<TTag>['classN
|
||||
/**
|
||||
* A reusable Text component with props that build on top of the design system for the extension
|
||||
*/
|
||||
export default function Text<TTag extends ElementType = 'span'>({
|
||||
as,
|
||||
className,
|
||||
variant,
|
||||
...rest
|
||||
}: TextProps<TTag>): JSX.Element {
|
||||
function Text<TTag extends ElementType = 'span'>(
|
||||
{ as, className, variant, ...rest }: TextProps<TTag>,
|
||||
ref: Ref<HTMLElement>
|
||||
): JSX.Element {
|
||||
const Comp = as || 'span';
|
||||
const mergedClassName = clsx(styles.text, styles[variant || 'p'], className);
|
||||
|
||||
return <Comp className={mergedClassName} {...rest} />;
|
||||
return <Comp className={mergedClassName} {...rest} ref={ref} />;
|
||||
}
|
||||
|
||||
export default React.forwardRef(Text) as typeof Text;
|
||||
|
||||
@@ -103,10 +103,10 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
<div className='w-full px-2 pb-3 pt-6 text-ut-black'>
|
||||
<div className='flex flex-col'>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Text variant='h1' as='h1' className='truncate text-theme-black'>
|
||||
<Text variant='h1' className='truncate text-theme-black'>
|
||||
{courseName}
|
||||
</Text>
|
||||
<Text variant='h1' as='h2' className='flex-1 whitespace-nowrap'>
|
||||
<Text variant='h1' className='flex-1 whitespace-nowrap text-theme-black'>
|
||||
({department} {courseNumber})
|
||||
</Text>
|
||||
<Button color='ut-burntorange' variant='single' icon={Copy} onClick={handleCopy}>
|
||||
|
||||
Reference in New Issue
Block a user