refactor(UserSchedule): index by a unique id rather than name (#166)

* refactor(UserSchedule): index by a unique id rather than name

* refactor: Update parameter names in schedule function jsdocs

* refactor: change more instances of .name to .id

* refactor: Fix typo in variable name and update references

* refactor: Remove console.log statement

* fix(chromatic): Update ScheduleListItem story

* refactor: remove unused eslint rule
This commit is contained in:
Razboy20
2024-03-14 23:09:04 -05:00
committed by GitHub
parent 5714ed16d7
commit 85769e9d2c
31 changed files with 182 additions and 304 deletions

View File

@@ -11,25 +11,25 @@ import type { MessageHandler } from 'chrome-extension-toolkit';
const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
addCourse({ data, sendResponse }) {
addCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
addCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
},
removeCourse({ data, sendResponse }) {
removeCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
removeCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
},
clearCourses({ data, sendResponse }) {
clearCourses(data.scheduleName).then(sendResponse);
clearCourses(data.scheduleId).then(sendResponse);
},
switchSchedule({ data, sendResponse }) {
switchSchedule(data.scheduleName).then(sendResponse);
switchSchedule(data.scheduleId).then(sendResponse);
},
createSchedule({ data, sendResponse }) {
createSchedule(data.scheduleName).then(sendResponse);
},
deleteSchedule({ data, sendResponse }) {
deleteSchedule(data.scheduleName).then(sendResponse);
deleteSchedule(data.scheduleId).then(sendResponse);
},
renameSchedule({ data, sendResponse }) {
renameSchedule(data.scheduleName, data.newName).then(sendResponse);
renameSchedule(data.scheduleId, data.newName).then(sendResponse);
},
};

View File

@@ -3,14 +3,14 @@ import type { Course } from '@shared/types/Course';
/**
* Adds a course to a user's schedule.
* @param scheduleName - The name of the schedule to add the course to.
* @param scheduleId - The id of the schedule to add the course to.
* @param course - The course to add.
* @returns A promise that resolves to void.
* @throws An error if the schedule is not found.
*/
export default async function addCourse(scheduleName: string, course: Course): Promise<void> {
export default async function addCourse(scheduleId: string, course: Course): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const activeSchedule = schedules.find(s => s.name === scheduleName);
const activeSchedule = schedules.find(s => s.id === scheduleId);
if (!activeSchedule) {
throw new Error('Schedule not found');
}

View File

@@ -2,14 +2,14 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Clears the courses for a given schedule.
* @param scheduleName - The name of the schedule.
* @param scheduleId - The id of the schedule.
* @throws Error if the schedule does not exist.
*/
export default async function clearCourses(scheduleName: string): Promise<void> {
export default async function clearCourses(scheduleId: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(schedule => schedule.name === scheduleName);
const schedule = schedules.find(schedule => schedule.id === scheduleId);
if (!schedule) {
throw new Error(`Schedule ${scheduleName} does not exist`);
throw new Error(`Schedule ${scheduleId} does not exist`);
}
schedule.courses = [];
schedule.updatedAt = Date.now();

View File

@@ -1,4 +1,5 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { generateRandomId } from '@shared/util/random';
/**
* Creates a new schedule with the given name
@@ -7,11 +8,12 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
*/
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
if (schedules.find(schedule => schedule.name === scheduleName)) {
return `Schedule ${scheduleName} already exists`;
}
// if (schedules.find(schedule => schedule.name === scheduleName)) {
// return `Schedule ${scheduleName} already exists`;
// }
schedules.push({
id: generateRandomId(),
name: scheduleName,
courses: [],
hours: 0,

View File

@@ -3,18 +3,18 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Deletes a schedule with the specified name.
*
* @param scheduleName - The name of the schedule to delete.
* @param scheduleId - The id of the schedule to delete.
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is deleted successfully.
*/
export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> {
export default async function deleteSchedule(scheduleId: string): Promise<string | undefined> {
const [schedules, activeIndex] = await Promise.all([
UserScheduleStore.get('schedules'),
UserScheduleStore.get('activeIndex'),
]);
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
if (scheduleIndex === -1) {
return `Schedule ${scheduleName} does not exist`;
return `Schedule ${scheduleId} does not exist`;
}
if (scheduleIndex === activeIndex) {
return 'Cannot delete active schedule';

View File

@@ -4,9 +4,9 @@ import type { Course } from '@shared/types/Course';
/**
*
*/
export default async function removeCourse(scheduleName: string, course: Course): Promise<void> {
export default async function removeCourse(scheduleId: string, course: Course): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const activeSchedule = schedules.find(s => s.name === scheduleName);
const activeSchedule = schedules.find(s => s.id === scheduleId);
if (!activeSchedule) {
throw new Error('Schedule not found');
}

View File

@@ -2,19 +2,19 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Renames a schedule with the specified name to a new name.
* @param scheduleName - The name of the schedule to be renamed.
* @param scheduleId - The id of the schedule to be renamed.
* @param newName - The new name for the schedule.
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is renamed successfully.
*/
export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> {
export default async function renameSchedule(scheduleId: string, newName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
if (scheduleIndex === -1) {
return `Schedule ${scheduleName} does not exist`;
}
if (schedules.find(schedule => schedule.name === newName)) {
return `Schedule ${newName} already exists`;
return `Schedule ${scheduleId} does not exist`;
}
// if (schedules.find(schedule => schedule.name === newName)) {
// return `Schedule ${newName} already exists`;
// }
schedules[scheduleIndex].name = newName;
schedules[scheduleIndex].updatedAt = Date.now();

View File

@@ -3,15 +3,15 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Switches the active schedule to the specified schedule name.
* Throws an error if the schedule does not exist.
* @param scheduleName - The name of the schedule to switch to.
* @param scheduleId - The id of the schedule to switch to.
* @returns A Promise that resolves when the active schedule is successfully switched.
*/
export default async function switchSchedule(scheduleName: string): Promise<void> {
export default async function switchSchedule(scheduleId: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
if (scheduleIndex === -1) {
throw new Error(`Schedule ${scheduleName} does not exist`);
throw new Error(`Schedule ${scheduleId} does not exist`);
}
schedules[scheduleIndex].updatedAt = Date.now();

View File

@@ -6,25 +6,25 @@ import type { Course } from '@shared/types/Course';
export interface UserScheduleMessages {
/**
* Add a course to a schedule
* @param data the schedule name and course to add
* @param data the schedule id and course to add
*/
addCourse: (data: { scheduleName: string; course: Course }) => void;
addCourse: (data: { scheduleId: string; course: Course }) => void;
/**
* Remove a course from a schedule
* @param data the schedule name and course to remove
* @param data the schedule id and course to remove
*/
removeCourse: (data: { scheduleName: string; course: Course }) => void;
removeCourse: (data: { scheduleId: string; course: Course }) => void;
/**
* Clears all courses from a schedule
* @param data the name of the schedule to clear
* @param data the id of the schedule to clear
*/
clearCourses: (data: { scheduleName: string }) => void;
clearCourses: (data: { scheduleId: string }) => void;
/**
* Switches the active schedule to the one specified
* @param data the name of the schedule to switch to
* @param data the id of the schedule to switch to
*/
switchSchedule: (data: { scheduleName: string }) => void;
switchSchedule: (data: { scheduleId: string }) => void;
/**
* Creates a new schedule with the specified name
@@ -34,14 +34,14 @@ export interface UserScheduleMessages {
createSchedule: (data: { scheduleName: string }) => string | undefined;
/**
* Deletes a schedule with the specified name
* @param data the name of the schedule to delete
* @param data the id of the schedule to delete
* @returns undefined if successful, otherwise an error message
*/
deleteSchedule: (data: { scheduleName: string }) => string | undefined;
deleteSchedule: (data: { scheduleId: string }) => string | undefined;
/**
* Renames a schedule with the specified name
* @param data the name of the schedule to rename and the new name
* @param data the id of the schedule to rename and the new name
* @returns undefined if successful, otherwise an error message
*/
renameSchedule: (data: { scheduleName: string; newName: string }) => string | undefined;
renameSchedule: (data: { scheduleId: string; newName: string }) => string | undefined;
}

View File

@@ -1,6 +1,8 @@
import { UserSchedule } from '@shared/types/UserSchedule';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
import { generateRandomId } from '../util/random';
interface IUserScheduleStore {
schedules: UserSchedule[];
activeIndex: number;
@@ -13,6 +15,7 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
schedules: [
new UserSchedule({
courses: [],
id: generateRandomId(),
name: 'Schedule 1',
hours: 0,
updatedAt: Date.now(),

View File

@@ -1,5 +1,6 @@
import type { Serialized } from 'chrome-extension-toolkit';
import { generateRandomId } from '../util/random';
import { Course } from './Course';
/**
@@ -7,6 +8,7 @@ import { Course } from './Course';
*/
export class UserSchedule {
courses: Course[];
id: string;
name: string;
hours: number;
/** Unix timestamp of when the schedule was last updated */
@@ -14,12 +16,10 @@ export class UserSchedule {
constructor(schedule: Serialized<UserSchedule>) {
this.courses = schedule.courses.map(c => new Course(c));
this.id = schedule.id ?? generateRandomId();
this.name = schedule.name;
this.hours = 0;
for (const course of this.courses) {
this.hours += course.creditHours;
}
this.updatedAt = schedule.updatedAt;
this.hours = this.courses.reduce((acc, c) => acc + c.creditHours, 0);
this.updatedAt = schedule.updatedAt ?? 0;
}
containsCourse(course: Course): boolean {

View File

@@ -1,25 +1,9 @@
/**
* Generate a random ID
*
* @returns string of size 10 made up of random numbers and letters
* @param length the length of the ID to generate
* @example "cdtl9l88pj"
*/
export function generateRandomId(length: number = 10): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i += 1) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
import { customAlphabet } from 'nanoid';
/**
* Generate a random number between min and max
* @param min the minimum number
* @param max the maximum number
* @returns a random number between min and max
* Generate secure URL-friendly unique ID.
*
* @param size Size of the ID. The default size is 12.
* @returns A random string.
*/
export function rangeRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export const generateRandomId = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12);

View File

@@ -1,5 +1,6 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { UserSchedule } from '@shared/types/UserSchedule';
import { generateRandomId } from '@shared/util/random';
import type { Meta, StoryObj } from '@storybook/react';
import List from '@views/components/common/List/List';
import ScheduleDropdown from '@views/components/common/ScheduleDropdown/ScheduleDropdown';
@@ -14,6 +15,7 @@ const schedules: UserSchedule[] = new Array(10).fill(exampleSchedule).map(
(schedule: UserSchedule, index) =>
new UserSchedule({
...schedule,
id: generateRandomId(),
name: `Schedule ${index + 1}`,
})
);
@@ -61,10 +63,10 @@ const meta: Meta<typeof ScheduleDropdown> = {
<ScheduleDropdown {...args}>
<List
draggables={schedules}
equalityCheck={(a, b) => a.name === b.name}
itemKey={s => s.id}
onReordered={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.name === activeSchedule.name);
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);
// don't care about the promise
UserScheduleStore.set('schedules', reordered);
@@ -74,9 +76,9 @@ const meta: Meta<typeof ScheduleDropdown> = {
>
{(schedule, handleProps) => (
<ScheduleListItem
name={schedule.name}
schedule={schedule}
onClick={() => {
switchSchedule(schedule.name);
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>

View File

@@ -96,6 +96,7 @@ export const Default: Story = {
args: {
draggables: exampleCourses.map((course, i) => ({ course, colors: tailwindColorways[i] })),
children: generateCourseBlocks,
itemKey: (item: { course: Course }) => item.course.uniqueId,
gap: 12,
},
render: args => (

View File

@@ -1,37 +1,49 @@
/* eslint-disable jsdoc/require-jsdoc */
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem';
import useSchedules from '@views/hooks/useSchedules';
import React from 'react';
export default {
import { exampleSchedule } from '../injected/mocked';
const meta = {
title: 'Components/Common/ScheduleListItem',
component: ScheduleListItem,
parameters: {
layout: 'centered',
tags: ['autodocs'],
},
argTypes: {
active: { control: 'boolean' },
name: { control: 'text' },
schedule: {
control: {
type: 'UserSchedule',
},
},
},
args: {
schedule: exampleSchedule,
},
tags: ['autodocs'],
} satisfies Meta<typeof ScheduleListItem>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Active: Story = {
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [activeSchedule] = useSchedules();
return (
<ScheduleListItem
{...args}
schedule={
new UserSchedule({
...exampleSchedule,
id: activeSchedule.id,
})
}
/>
);
},
};
export const Default = args => <ScheduleListItem {...args} />;
Default.args = {
name: 'My Schedule',
active: true,
};
export const Active = args => <ScheduleListItem {...args} />;
Active.args = {
name: 'My Schedule',
active: true,
};
export const Inactive = args => <ScheduleListItem {...args} />;
Inactive.args = {
name: 'My Schedule',
active: false,
};
export const Inactive: Story = {};

View File

@@ -1,8 +1,3 @@
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
import React from 'react';
@@ -14,11 +9,7 @@ const meta = {
layout: 'centered',
tags: ['autodocs'],
},
argTypes: {
// dummySchedules: { control: 'object' },
// dummyActiveIndex: { control: 'number' },
},
render: (args: any) => (
render: args => (
<div>
<CalendarSchedules {...args} />
</div>
@@ -28,122 +19,4 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
const schedules = [
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
scrapedAt: Date.now(),
}),
],
name: 'Main Schedule',
hours: 0,
updatedAt: Date.now(),
}),
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Spring',
},
scrapedAt: Date.now(),
}),
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
scrapedAt: Date.now(),
}),
],
name: 'Backup #3',
hours: 0,
updatedAt: Date.now(),
}),
];
export const Default: Story = {
args: {
// dummySchedules: schedules,
// dummyActiveIndex: 0,
},
};
export const Default: Story = {};

View File

@@ -1,11 +1,10 @@
import type { Course } from '@shared/types/Course';
import { Status } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import React, { useState } from 'react';
import { bevoCourse, bevoScheule, MikeScottCS314Course, MikeScottCS314Schedule } from './mocked';
import { bevoCourse, mikeScottCS314Course } from './mocked';
const meta = {
title: 'Components/Injected/CourseCatalogInjectedPopup',
@@ -14,23 +13,7 @@ const meta = {
open: true,
onClose: () => {},
},
argTypes: {
course: {
control: {
type: 'object',
},
},
activeSchedule: {
control: {
type: 'object',
},
},
onClose: {
control: {
type: 'function',
},
},
},
tags: ['autodocs'],
render(args) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const [isOpen, setIsOpen] = useState(args.open);
@@ -44,24 +27,21 @@ type Story = StoryObj<typeof meta>;
export const OpenCourse: Story = {
args: {
course: MikeScottCS314Course,
activeSchedule: MikeScottCS314Schedule,
course: mikeScottCS314Course,
},
};
export const ClosedCourse: Story = {
args: {
course: {
...MikeScottCS314Course,
...mikeScottCS314Course,
status: Status.CLOSED,
} as Course,
activeSchedule: new UserSchedule({ courses: [], name: '', hours: 0, updatedAt: Date.now() }),
},
};
export const CourseWithNoData: Story = {
args: {
course: bevoCourse,
activeSchedule: bevoScheule,
},
};

View File

@@ -55,6 +55,7 @@ export const exampleCourse: Course = new Course({
export const exampleSchedule: UserSchedule = new UserSchedule({
courses: [exampleCourse],
id: 'az372389blep',
name: 'Example Schedule',
hours: 3,
updatedAt: Date.now(),
@@ -105,14 +106,15 @@ export const bevoCourse: Course = new Course({
scrapedAt: Date.now(),
});
export const bevoScheule: UserSchedule = new UserSchedule({
export const bevoSchedule: UserSchedule = new UserSchedule({
courses: [bevoCourse],
id: 'bevoshenanigans52',
name: 'Bevo Schedule',
hours: 3,
updatedAt: Date.now(),
});
export const MikeScottCS314Course: Course = new Course({
export const mikeScottCS314Course: Course = new Course({
uniqueId: 50805,
number: '314',
fullName: 'C S 314 DATA STRUCTURES',
@@ -158,8 +160,9 @@ export const MikeScottCS314Course: Course = new Course({
scrapedAt: Date.now(),
});
export const MikeScottCS314Schedule: UserSchedule = new UserSchedule({
courses: [MikeScottCS314Course],
export const mikeScottCS314Schedule: UserSchedule = new UserSchedule({
courses: [mikeScottCS314Course],
id: 'omgitsmikescott314',
name: 'Mike Scott CS314 Schedule',
hours: 3,
updatedAt: Date.now(),

View File

@@ -79,7 +79,6 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element {
)}
<CourseCatalogInjectedPopup
course={selectedCourse}
activeSchedule={activeSchedule}
show={showPopup}
onClose={() => setShowPopup(false)}
afterLeave={() => setSelectedCourse(null)}

View File

@@ -64,10 +64,10 @@ export default function PopupMain(): JSX.Element {
<ScheduleDropdown>
<List
draggables={schedules}
equalityCheck={(a, b) => a.name === b.name}
itemKey={schedule => schedule.id}
onReordered={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.name === activeSchedule.name);
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);
// don't care about the promise
UserScheduleStore.set('schedules', reordered);
@@ -77,9 +77,9 @@ export default function PopupMain(): JSX.Element {
>
{(schedule, handleProps) => (
<ScheduleListItem
name={schedule.name}
schedule={schedule}
onClick={() => {
switchSchedule(schedule.name);
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>
@@ -98,7 +98,7 @@ export default function PopupMain(): JSX.Element {
activeSchedule.courses = reordered.map(c => c.course);
replaceSchedule(getActiveSchedule(), activeSchedule);
}}
equalityCheck={(a, b) => a.course.uniqueId === b.course.uniqueId}
itemKey={e => e.course.uniqueId}
gap={10}
>
{({ course, colors }, handleProps) => (

View File

@@ -83,7 +83,6 @@ export default function Calendar(): JSX.Element {
<CourseCatalogInjectedPopup
course={course}
activeSchedule={activeSchedule}
onClose={() => setShowPopup(false)}
open={showPopup}
afterLeave={() => setCourse(null)}

View File

@@ -40,8 +40,8 @@ export default function CalendarHeader(): JSX.Element {
<div className='flex items-center gap-2'>
<LogoIcon />
<div className='flex flex-col whitespace-nowrap'>
<Text className='text-lg! text-ut-burntorange font-medium!'>UT Registration</Text>
<Text className='text-lg! text-ut-orange font-medium!'>Plus</Text>
<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>
@@ -52,7 +52,7 @@ export default function CalendarHeader(): JSX.Element {
totalHours={activeSchedule.hours}
totalCourses={activeSchedule.courses.length}
/>
<Text variant='h4' className='text-xs! text-gray font-medium! leading-normal!'>
<Text variant='h4' className='text-gray text-xs! font-medium! leading-normal!'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
</div>

View File

@@ -30,7 +30,7 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
const [activeSchedule, schedules] = useSchedules();
useEffect(() => {
const index = schedules.findIndex(schedule => schedule.name === activeSchedule.name);
const index = schedules.findIndex(schedule => schedule.id === activeSchedule.id);
if (index !== -1) {
setActiveScheduleIndex(index);
}
@@ -68,10 +68,10 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
<List
gap={10}
draggables={schedules}
equalityCheck={(a, b) => a.name === b.name}
itemKey={s => s.id}
onReordered={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.name === activeSchedule.name);
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);
// don't care about the promise
UserScheduleStore.set('schedules', reordered);
@@ -80,9 +80,9 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
>
{(schedule, handleProps) => (
<ScheduleListItem
name={schedule.name}
schedule={schedule}
onClick={() => {
switchSchedule(schedule.name);
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>

View File

@@ -3,6 +3,8 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import ExtensionRoot from '../ExtensionRoot/ExtensionRoot';
/*
* Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
*/
@@ -14,13 +16,13 @@ export interface ListProps<T> {
draggables: T[];
children: (draggable: T, handleProps: DraggableProvidedDragHandleProps) => ReactElement;
onReordered: (elements: T[]) => void;
equalityCheck?: (a: T, b: T) => boolean;
itemKey: (item: T) => React.Key;
gap?: number; // Impacts the spacing between items in the list
}
function wrap<T>(draggableElements: T[]) {
return draggableElements.map((element, index) => ({
id: `id:${index}`,
function wrap<T>(draggableElements: T[], keyTransform: ListProps<T>['itemKey']) {
return draggableElements.map(element => ({
id: keyTransform(element),
content: element,
}));
}
@@ -68,20 +70,19 @@ function Item<T>(props: {
* <List draggableElements={elements} />
*/
function List<T>(props: ListProps<T>): JSX.Element {
const [items, setItems] = useState(wrap(props.draggables));
const [items, setItems] = useState(wrap(props.draggables, props.itemKey));
const equalityCheck = props.equalityCheck || ((a, b) => a === b);
const transformFunction = props.children;
useEffect(() => {
// check if the draggables content has *actually* changed
if (
props.draggables.length === items.length &&
props.draggables.every((element, index) => equalityCheck(element, items[index].content))
props.draggables.every((element, index) => props.itemKey(element) === items[index].id)
) {
return;
}
setItems(wrap(props.draggables));
setItems(wrap(props.draggables, props.itemKey));
}, [props.draggables]);
const onDragEnd: OnDragEndResponder = useCallback(
@@ -123,7 +124,9 @@ function List<T>(props: ListProps<T>): JSX.Element {
...style,
}}
>
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
<ExtensionRoot>
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
</ExtensionRoot>
</Item>
);
}}
@@ -135,7 +138,7 @@ function List<T>(props: ListProps<T>): JSX.Element {
style={{ marginBottom: `-${props.gap}px` }}
>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
<Draggable key={item.id} draggableId={item.id.toString()} index={index}>
{draggableProvided => (
<div
ref={draggableProvided.innerRef}

View File

@@ -1,3 +1,4 @@
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';
@@ -10,7 +11,7 @@ import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
*/
export type Props = {
style?: React.CSSProperties;
name: string;
schedule: UserSchedule;
dragHandleProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className'>;
onClick?: React.DOMAttributes<HTMLDivElement>['onClick'];
};
@@ -18,10 +19,10 @@ export type Props = {
/**
* This is a reusable dropdown component that can be used to toggle the visiblity of information
*/
export default function ScheduleListItem({ style, name, dragHandleProps, onClick }: Props): JSX.Element {
export default function ScheduleListItem({ style, schedule, dragHandleProps, onClick }: Props): JSX.Element {
const [activeSchedule] = useSchedules();
const isActive = useMemo(() => activeSchedule.name === name, [activeSchedule, name]);
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
return (
<div style={{ ...style }} className='items-center rounded bg-white'>
@@ -42,7 +43,7 @@ export default function ScheduleListItem({ style, name, dragHandleProps, onClick
}
)}
/>
<Text variant='p'>{name}</Text>
<Text variant='p'>{schedule.name}</Text>
</div>
</div>
</li>

View File

@@ -1,7 +1,7 @@
import type { Course } from '@shared/types/Course';
import type { UserSchedule } from '@shared/types/UserSchedule';
import type { DialogProps } from '@views/components/common/Dialog/Dialog';
import Dialog from '@views/components/common/Dialog/Dialog';
import useSchedules from '@views/hooks/useSchedules';
import React from 'react';
import Description from './Description';
@@ -10,7 +10,6 @@ import HeadingAndActions from './HeadingAndActions';
export type CourseCatalogInjectedPopupProps = DialogProps & {
course: Course;
activeSchedule: UserSchedule;
};
/**
@@ -23,8 +22,9 @@ export type CourseCatalogInjectedPopupProps = DialogProps & {
* @param {Function} props.onClose - The function to close the popup.
* @returns {JSX.Element} The CourseCatalogInjectedPopup component.
*/
function CourseCatalogInjectedPopup({ course, activeSchedule, ...rest }: CourseCatalogInjectedPopupProps): JSX.Element {
function CourseCatalogInjectedPopup({ course, ...rest }: CourseCatalogInjectedPopupProps): JSX.Element {
const emptyRef = React.useRef<HTMLDivElement>(null);
const [activeSchedule] = useSchedules();
return (
<Dialog className='max-w-[780px] px-6' {...rest} initialFocus={emptyRef}>

View File

@@ -100,9 +100,9 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
const handleAddOrRemoveCourse = async () => {
if (!activeSchedule) return;
if (!courseAdded) {
addCourse({ course, scheduleName: activeSchedule.name });
addCourse({ course, scheduleId: activeSchedule.id });
} else {
removeCourse({ course, scheduleName: activeSchedule.name });
removeCourse({ course, scheduleId: activeSchedule.id });
}
};

View File

@@ -59,6 +59,7 @@ export function useFlattenedCourseSchedule(): FlattenedCourseSchedule {
courseCells: [] as CalendarGridCourse[],
activeSchedule: new UserSchedule({
courses: [],
id: 'error',
name: 'Something may have went wrong',
hours: 0,
updatedAt: Date.now(),

View File

@@ -3,9 +3,17 @@ import { UserSchedule } from '@shared/types/UserSchedule';
import { useEffect, useState } from 'react';
let schedulesCache = [];
let activeIndexCache = 0;
let activeIndexCache = -1;
let initialLoad = true;
const errorSchedule = new UserSchedule({
courses: [],
id: 'error',
name: 'An error has occurred',
hours: 0,
updatedAt: Date.now(),
});
/**
* Fetches the user schedules from storage and sets the cached state.
*/
@@ -25,7 +33,7 @@ async function fetchData() {
export default function useSchedules(): [active: UserSchedule, schedules: UserSchedule[]] {
const [schedules, setSchedules] = useState<UserSchedule[]>(schedulesCache);
const [activeIndex, setActiveIndex] = useState<number>(activeIndexCache);
const [activeSchedule, setActiveSchedule] = useState<UserSchedule>(schedules[activeIndex]);
const [activeSchedule, setActiveSchedule] = useState<UserSchedule>(schedules[activeIndex] ?? errorSchedule);
if (initialLoad) {
initialLoad = false;
@@ -56,22 +64,19 @@ export default function useSchedules(): [active: UserSchedule, schedules: UserSc
// recompute active schedule on a schedule/index change
useEffect(() => {
setActiveSchedule(schedules[activeIndex]);
setActiveSchedule(schedules[activeIndex] ?? errorSchedule);
}, [activeIndex, schedules]);
return [activeSchedule, schedules];
}
export function getActiveSchedule(): UserSchedule {
return (
schedulesCache[activeIndexCache] ||
new UserSchedule({ courses: [], name: 'An error has occurred', hours: 0, updatedAt: Date.now() })
);
return schedulesCache[activeIndexCache] ?? errorSchedule;
}
export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: UserSchedule) {
const schedules = await UserScheduleStore.get('schedules');
let oldIndex = schedules.findIndex(s => s.name === oldSchedule.name);
let oldIndex = schedules.findIndex(s => s.id === oldSchedule.id);
oldIndex = oldIndex !== -1 ? oldIndex : 0;
schedules[oldIndex] = newSchedule;
await UserScheduleStore.set('schedules', schedules);
@@ -80,12 +85,12 @@ export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: Us
/**
* Switches the active schedule to the one with the specified name.
* @param name - The name of the schedule to switch to.
* @param id - The id of the schedule to switch to.
* @returns A promise that resolves when the active schedule has been switched.
*/
export async function switchSchedule(name: string): Promise<void> {
export async function switchSchedule(id: string): Promise<void> {
console.log('Switching schedule...');
const schedules = await UserScheduleStore.get('schedules');
const activeIndex = schedules.findIndex(s => s.name === name);
const activeIndex = schedules.findIndex(s => s.id === id);
await UserScheduleStore.set('activeIndex', activeIndex);
}