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

@@ -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' />

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'
>

View File

@@ -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
)}
>

View File

@@ -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>
);

View File

@@ -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>
);
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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}>