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:
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -83,7 +83,6 @@ export default function Calendar(): JSX.Element {
|
||||
|
||||
<CourseCatalogInjectedPopup
|
||||
course={course}
|
||||
activeSchedule={activeSchedule}
|
||||
onClose={() => setShowPopup(false)}
|
||||
open={showPopup}
|
||||
afterLeave={() => setCourse(null)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user