fix: fixed bug with course cells after 12 PM extending past midnight (#122)

* 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

* fix: fixed bug with course cell times starting and after 12 PM. commented in CourseMeeting class

* Update src/views/hooks/useFlattenedCourseSchedule.ts

* fix: fixed build errors by removing old apis

* refactor: added type-safety and destructuring

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
This commit is contained in:
Som Gupta
2024-03-03 15:30:18 -06:00
committed by doprz
parent 5abb2a4d4f
commit f22a3cd7c0
10 changed files with 11003 additions and 344 deletions

11000
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,9 +31,13 @@ export type Location = {
export class CourseMeeting { export class CourseMeeting {
/** The day of the week that the course is taught */ /** The day of the week that the course is taught */
days: Day[]; days: Day[];
/** The start time of the course, in minutes since midnight */ /** NOTE: Times starting and after 12 PM have an additional 720 minutes (12 hrs) added to them
* The start time of the course, in minutes since midnight
* */
startTime: number; startTime: number;
/** The end time of the course, in minutes since midnight */ /** NOTE: Times starting and after 12 PM have an additional 720 minutes (12 hrs) added to them
* The end time of the course, in minutes since midnight
* */
endTime: number; endTime: number;
/** The location that the course is taught */ /** The location that the course is taught */
location?: Location; location?: Location;

View File

@@ -15,8 +15,8 @@ const meta = {
tags: ['autodocs'], tags: ['autodocs'],
}, },
argTypes: { argTypes: {
dummySchedules: { control: 'object' }, // dummySchedules: { control: 'object' },
dummyActiveIndex: { control: 'number' }, // dummyActiveIndex: { control: 'number' },
}, },
render: (args: any) => ( render: (args: any) => (
<div> <div>
@@ -138,7 +138,7 @@ const schedules = [
export const Default: Story = { export const Default: Story = {
args: { args: {
dummySchedules: schedules, // dummySchedules: schedules,
dummyActiveIndex: 0, // dummyActiveIndex: 0,
}, },
}; };

View File

@@ -11,99 +11,99 @@ const exampleSchedule: UserSchedule = new UserSchedule({
}); });
// TODO (achadaga): import this after // TODO (achadaga): import this after
// https://github.com/Longhorn-Developers/UT-Registration-Plus/pull/106 is merged // https://github.com/Longhorn-Developers/UT-Registration-Plus/pull/106 is merged
const bevoCourse: Course = new Course({ // const bevoCourse: Course = new Course({
uniqueId: 47280, // uniqueId: 47280,
number: '311C', // number: '311C',
fullName: "BVO 311C BEVO'S SEMINAR LONGHORN CARE", // fullName: "BVO 311C BEVO'S SEMINAR LONGHORN CARE",
courseName: "BEVO'S SEMINAR LONGHORN CARE", // courseName: "BEVO'S SEMINAR LONGHORN CARE",
department: 'BVO', // department: 'BVO',
creditHours: 3, // creditHours: 3,
status: Status.OPEN, // status: Status.OPEN,
instructors: [new Instructor({ fullName: 'BEVO', firstName: '', lastName: 'BEVO', middleInitial: '' })], // instructors: [new Instructor({ fullName: 'BEVO', firstName: '', lastName: 'BEVO', middleInitial: '' })],
isReserved: false, // isReserved: false,
description: [ // description: [
'Restricted to Students in the School of Longhorn Enthusiasts', // 'Restricted to Students in the School of Longhorn Enthusiasts',
'Immerse yourself in the daily routine of a longhorn—sunrise pasture walks and the best shady spots for a midday siesta. Understand the behavioral science behind our mascots stoic demeanor during games.', // 'Immerse yourself in the daily routine of a longhorn—sunrise pasture walks and the best shady spots for a midday siesta. Understand the behavioral science behind our mascots stoic demeanor during games.',
'BVO 311C and 312H may not both be counted.', // 'BVO 311C and 312H may not both be counted.',
'Prerequisite: Grazing 311 or 311H.', // 'Prerequisite: Grazing 311 or 311H.',
'May be counted toward the Independent Inquiry flag requirement. May be counted toward the Writing flag requirement', // 'May be counted toward the Independent Inquiry flag requirement. May be counted toward the Writing flag requirement',
'Offered on the letter-grade basis only.', // 'Offered on the letter-grade basis only.',
], // ],
schedule: new CourseSchedule({ // schedule: new CourseSchedule({
meetings: [ // meetings: [
new CourseMeeting({ // new CourseMeeting({
days: ['Tuesday', 'Thursday'], // days: ['Tuesday', 'Thursday'],
startTime: 480, // startTime: 480,
endTime: 570, // endTime: 570,
location: { building: 'UTC', room: '123' }, // location: { building: 'UTC', room: '123' },
}), // }),
new CourseMeeting({ // new CourseMeeting({
days: ['Thursday'], // days: ['Thursday'],
startTime: 570, // startTime: 570,
endTime: 630, // endTime: 630,
location: { building: 'JES', room: '123' }, // location: { building: 'JES', room: '123' },
}), // }),
], // ],
}), // }),
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/', // url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Independent Inquiry', 'Writing'], // flags: ['Independent Inquiry', 'Writing'],
instructionMode: 'In Person', // instructionMode: 'In Person',
semester: { // semester: {
code: '12345', // code: '12345',
year: 2024, // year: 2024,
season: 'Spring', // season: 'Spring',
}, // },
}); // });
const meta = { // const meta = {
title: 'Components/Injected/CourseCatalogInjectedPopup', // title: 'Components/Injected/CourseCatalogInjectedPopup',
component: CourseCatalogInjectedPopup, // component: CourseCatalogInjectedPopup,
args: { // args: {
course: exampleCourse, // course: exampleCourse,
activeSchedule: exampleSchedule, // activeSchedule: exampleSchedule,
onClose: () => {}, // onClose: () => {},
}, // },
argTypes: { // argTypes: {
course: { // course: {
control: { // control: {
type: 'object', // type: 'object',
}, // },
}, // },
activeSchedule: { // activeSchedule: {
control: { // control: {
type: 'object', // type: 'object',
}, // },
}, // },
onClose: { // onClose: {
control: { // control: {
type: 'function', // type: 'function',
}, // },
}, // },
}, // },
} satisfies Meta<typeof CourseCatalogInjectedPopup>; // } satisfies Meta<typeof CourseCatalogInjectedPopup>;
export default meta; // export default meta;
type Story = StoryObj<typeof meta>; // type Story = StoryObj<typeof meta>;
export const OpenCourse: Story = { // export const OpenCourse: Story = {
args: { // args: {
course: exampleCourse, // course: exampleCourse,
activeSchedule: exampleSchedule, // activeSchedule: exampleSchedule,
onClose: () => {}, // onClose: () => {},
}, // },
}; // };
export const ClosedCourse: Story = { // export const ClosedCourse: Story = {
args: { // args: {
course: { // course: {
...exampleCourse, // ...exampleCourse,
status: Status.CLOSED, // status: Status.CLOSED,
} satisfies Course, // } satisfies Course,
}, // },
}; // };
export const CourseWithNoData: Story = { // export const CourseWithNoData: Story = {
args: { // args: {
course: bevoCourse, // course: bevoCourse,
}, // },
}; // };

View File

@@ -22,8 +22,6 @@ interface Props {
* Grid of CalendarGridCell components forming the user's course schedule calendar view * Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props * @param props
*/ */
// function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
// const [grid, setGrid] = useState([]);
function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWithChildren<Props>): JSX.Element { function CalendarGrid({ courseCells, saturdayClass, setCourse }: React.PropsWithChildren<Props>): JSX.Element {
// const [grid, setGrid] = useState([]); // const [grid, setGrid] = useState([]);
const calendarRef = useRef(null); // Create a ref for the calendar grid const calendarRef = useRef(null); // Create a ref for the calendar grid
@@ -152,29 +150,33 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
}); });
// Part of TODO: block.course is definitely a course object // Part of TODO: block.course is definitely a course object
console.log(courseCells); // console.log(courseCells);
return courseCells.map(block => ( return courseCells.map((block, i) => {
<div const { courseDeptAndInstr, timeAndLocation, status, colors } = courseCells[i].componentProps;
key={`${block}`}
style={{ return (
gridColumn: `${block.calendarGridPoint.dayIndex + 2}`, <div
gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`, key={`${block}`}
width: `calc(100% / ${block.totalColumns})`, style={{
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`, gridColumn: `${block.calendarGridPoint.dayIndex + 2}`,
padding: '0px 10px 4px 0px', gridRow: `${block.calendarGridPoint.startIndex} / ${block.calendarGridPoint.endIndex}`,
}} width: `calc(100% / ${block.totalColumns})`,
> marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
<CalendarCourseCell padding: '0px 10px 4px 0px',
courseDeptAndInstr={block.componentProps.courseDeptAndInstr} }}
timeAndLocation={block.componentProps.timeAndLocation} >
status={block.componentProps.status} <CalendarCourseCell
// TODO: Change to block.componentProps.colors when colors are integrated to the rest of the project courseDeptAndInstr={courseDeptAndInstr}
colors={getCourseColors('emerald', 500) /* block.componentProps.colors */} timeAndLocation={timeAndLocation}
onClick={() => setCourse(block.course)} status={status}
/> // TODO: Change to block.componentProps.colors when colors are integrated to the rest of the project
</div> colors={getCourseColors('emerald', 500) /* block.componentProps.colors */}
)); onClick={() => setCourse(block.course)}
/>
</div>
)
});
} }
/* <div className={styles.buttonContainer}> /* <div className={styles.buttonContainer}>

View File

@@ -18,8 +18,8 @@ const handleOpenOptions = async () => {
await openTabFromContentScript(url); await openTabFromContentScript(url);
}; };
const CalendarHeader = ({ totalHours, totalCourses, scheduleName }) => ( const CalendarHeader = ( { totalHours, totalCourses, scheduleName } ) => (
<div className='min-h-79px min-w-672px w-full flex px-0'> <div className='min-h-79px min-w-672px w-full flex px-0 py-15'>
<div className='flex flex-row gap-20'> <div className='flex flex-row gap-20'>
<div className='flex gap-10'> <div className='flex gap-10'>
<div className='flex gap-1'> <div className='flex gap-1'>
@@ -49,7 +49,7 @@ const CalendarHeader = ({ totalHours, totalCourses, scheduleName }) => (
<div className='flex flex-row'> <div className='flex flex-row'>
<Button variant='single' icon={UndoIcon} color='ut-black' /> <Button variant='single' icon={UndoIcon} color='ut-black' />
<Button variant='single' icon={RedoIcon} color='ut-black' /> <Button variant='single' icon={RedoIcon} color='ut-black' />
<Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions} /> <Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions}/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -93,7 +93,6 @@ const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight,
return [...prevItems, ...newItems]; return [...prevItems, ...newItems];
}); });
}, [draggableElements]); }, [draggableElements]);
const onDragEnd = useCallback( const onDragEnd = useCallback(
result => { result => {
if (!result.destination) { if (!result.destination) {

View File

@@ -32,7 +32,7 @@ export default function CourseCatalogInjectedPopup({
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}> <Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
<div className='flex flex-col'> <div className='flex flex-col'>
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} /> <HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
<Description lines={course.description} /> <Description course={course} /* lines={course.description} Looks like this was replaced. Description now set internally*/ />
<GradeDistribution course={course} /> <GradeDistribution course={course} />
</div> </div>
</Popup> </Popup>

View File

@@ -56,9 +56,7 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({
onClose, onClose,
}: HeadingAndActionProps): JSX.Element => { }: HeadingAndActionProps): JSX.Element => {
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course; const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
const [courseAdded, setCourseAdded] = useState<boolean>( const courseAdded = activeSchedule.courses.some(ourCourse => ourCourse.uniqueId === uniqueId);
activeSchedule !== undefined ? activeSchedule.courses.some(course => course.uniqueId === uniqueId) : false
);
const getInstructorFullName = (instructor: Instructor) => { const getInstructorFullName = (instructor: Instructor) => {
const { firstName, lastName } = instructor; const { firstName, lastName } = instructor;
@@ -105,7 +103,6 @@ const HeadingAndActions: React.FC<HeadingAndActionProps> = ({
} else { } else {
removeCourse({ course, scheduleName: activeSchedule.name }); removeCourse({ course, scheduleName: activeSchedule.name });
} }
setCourseAdded(prev => !prev);
}; };
return ( return (

View File

@@ -5,6 +5,8 @@ import type { CalendarCourseCellProps } from '@views/components/calendar/Calenda
import useSchedules from './useSchedules'; import useSchedules from './useSchedules';
const dayToNumber: { [day: string]: number } = { const dayToNumber: { [day: string]: number } = {
Monday: 0, Monday: 0,
Tuesday: 1, Tuesday: 1,
@@ -58,31 +60,30 @@ export function useFlattenedCourseSchedule(): FlattenedCourseSchedule {
return { return {
courseCells: [] as CalendarGridCourse[], courseCells: [] as CalendarGridCourse[],
activeSchedule: new UserSchedule([], 'Something may have went wrong', 0), activeSchedule: new UserSchedule([], 'Something may have went wrong', 0),
} as FlattenedCourseSchedule; } satisfies FlattenedCourseSchedule;
} }
if (activeSchedule.courses.length === 0) { if (activeSchedule.courses.length === 0) {
return { return {
courseCells: [] as CalendarGridCourse[], courseCells: [] as CalendarGridCourse[],
activeSchedule, activeSchedule
} satisfies FlattenedCourseSchedule; } satisfies FlattenedCourseSchedule;
} }
const { courses, name, hours } = activeSchedule; const { courses, name, hours } = activeSchedule;
const processedCourses = courses const processedCourses = courses.flatMap((course: Course) => {
.flatMap((course: Course) => { const { status, courseDeptAndInstr, meetings } = extractCourseInfo(course);
const { status, courseDeptAndInstr, meetings } = extractCourseInfo(course);
if (meetings.length === 0) { if (meetings.length === 0) {
return processAsyncCourses({ courseDeptAndInstr, status, course }); return processAsyncCourses({ courseDeptAndInstr, status, course });
} }
return meetings.flatMap((meeting: CourseMeeting) => return meetings.flatMap((meeting: CourseMeeting) =>
processInPersonMeetings(meeting, { courseDeptAndInstr, status, course }) processInPersonMeetings(meeting, { courseDeptAndInstr, status, course })
); );
}) }).sort(sortCourses);
.sort(sortCourses);
return { return {
courseCells: processedCourses as CalendarGridCourse[], courseCells: processedCourses as CalendarGridCourse[],
@@ -105,33 +106,23 @@ function extractCourseInfo(course: Course) {
/** /**
* Function to process each in-person class into its distinct meeting objects for calendar grid * Function to process each in-person class into its distinct meeting objects for calendar grid
*/ */
function processAsyncCourses({ function processAsyncCourses({ courseDeptAndInstr, status, course }: { courseDeptAndInstr: string, status: StatusType, course: Course }) {
courseDeptAndInstr, return [{
status, calendarGridPoint: {
course, dayIndex: 0,
}: { startIndex: 0,
courseDeptAndInstr: string; endIndex: 0,
status: StatusType;
course: Course;
}) {
return [
{
calendarGridPoint: {
dayIndex: 0,
startIndex: 0,
endIndex: 0,
},
componentProps: {
courseDeptAndInstr,
status,
colors: {
primaryColor: 'ut-gray',
secondaryColor: 'ut-gray',
},
},
course,
}, },
] satisfies CalendarGridCourse[]; componentProps: {
courseDeptAndInstr,
status,
colors: {
primaryColor: 'ut-gray',
secondaryColor: 'ut-gray',
},
},
course,
}] satisfies CalendarGridCourse[];
} }
/** /**
@@ -141,13 +132,17 @@ function processInPersonMeetings(
{ days, startTime, endTime, location }: CourseMeeting, { days, startTime, endTime, location }: CourseMeeting,
{ courseDeptAndInstr, status, course } { courseDeptAndInstr, status, course }
) { ) {
const midnightIndex = 1440;
const normalizingTimeFactor = 720;
const time = getTimeString({ separator: '-', capitalize: true }, startTime, endTime); const time = getTimeString({ separator: '-', capitalize: true }, startTime, endTime);
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`; const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
let normalizedStartTime = startTime >= midnightIndex ? startTime - normalizingTimeFactor : startTime;
let normalizedEndTime = endTime >= midnightIndex ? endTime - normalizingTimeFactor : endTime;
return days.map(day => ({ return days.map(day => ({
calendarGridPoint: { calendarGridPoint: {
dayIndex: dayToNumber[day], dayIndex: dayToNumber[day],
startIndex: convertMinutesToIndex(startTime), startIndex: convertMinutesToIndex(normalizedStartTime),
endIndex: convertMinutesToIndex(endTime), endIndex: convertMinutesToIndex(normalizedEndTime),
}, },
componentProps: { componentProps: {
courseDeptAndInstr, courseDeptAndInstr,
@@ -165,7 +160,7 @@ function processInPersonMeetings(
/** /**
* Utility function to sort courses for the calendar grid * Utility function to sort courses for the calendar grid
*/ */
function sortCourses(a, b) { function sortCourses(a: CalendarGridCourse, b: CalendarGridCourse): number {
const { dayIndex: dayIndexA, startIndex: startIndexA, endIndex: endIndexA } = a.calendarGridPoint; const { dayIndex: dayIndexA, startIndex: startIndexA, endIndex: endIndexA } = a.calendarGridPoint;
const { dayIndex: dayIndexB, startIndex: startIndexB, endIndex: endIndexB } = b.calendarGridPoint; const { dayIndex: dayIndexB, startIndex: startIndexB, endIndex: endIndexB } = b.calendarGridPoint;