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:
@@ -34,6 +34,7 @@
|
|||||||
"highcharts-react-official": "^3.2.1",
|
"highcharts-react-official": "^3.2.1",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
|
"nanoid": "^5.0.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-devtools-core": "^5.0.0",
|
"react-devtools-core": "^5.0.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -40,6 +40,9 @@ dependencies:
|
|||||||
husky:
|
husky:
|
||||||
specifier: ^9.0.11
|
specifier: ^9.0.11
|
||||||
version: 9.0.11
|
version: 9.0.11
|
||||||
|
nanoid:
|
||||||
|
specifier: ^5.0.6
|
||||||
|
version: 5.0.6
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
@@ -10054,6 +10057,12 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/nanoid@5.0.6:
|
||||||
|
resolution: {integrity: sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==}
|
||||||
|
engines: {node: ^18 || >=20}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/natural-compare@1.4.0:
|
/natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
@@ -11,25 +11,25 @@ import type { MessageHandler } from 'chrome-extension-toolkit';
|
|||||||
|
|
||||||
const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
|
const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
|
||||||
addCourse({ data, sendResponse }) {
|
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, sendResponse }) {
|
||||||
removeCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
|
removeCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
|
||||||
},
|
},
|
||||||
clearCourses({ data, sendResponse }) {
|
clearCourses({ data, sendResponse }) {
|
||||||
clearCourses(data.scheduleName).then(sendResponse);
|
clearCourses(data.scheduleId).then(sendResponse);
|
||||||
},
|
},
|
||||||
switchSchedule({ data, sendResponse }) {
|
switchSchedule({ data, sendResponse }) {
|
||||||
switchSchedule(data.scheduleName).then(sendResponse);
|
switchSchedule(data.scheduleId).then(sendResponse);
|
||||||
},
|
},
|
||||||
createSchedule({ data, sendResponse }) {
|
createSchedule({ data, sendResponse }) {
|
||||||
createSchedule(data.scheduleName).then(sendResponse);
|
createSchedule(data.scheduleName).then(sendResponse);
|
||||||
},
|
},
|
||||||
deleteSchedule({ data, sendResponse }) {
|
deleteSchedule({ data, sendResponse }) {
|
||||||
deleteSchedule(data.scheduleName).then(sendResponse);
|
deleteSchedule(data.scheduleId).then(sendResponse);
|
||||||
},
|
},
|
||||||
renameSchedule({ data, 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.
|
* 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.
|
* @param course - The course to add.
|
||||||
* @returns A promise that resolves to void.
|
* @returns A promise that resolves to void.
|
||||||
* @throws An error if the schedule is not found.
|
* @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 schedules = await UserScheduleStore.get('schedules');
|
||||||
const activeSchedule = schedules.find(s => s.name === scheduleName);
|
const activeSchedule = schedules.find(s => s.id === scheduleId);
|
||||||
if (!activeSchedule) {
|
if (!activeSchedule) {
|
||||||
throw new Error('Schedule not found');
|
throw new Error('Schedule not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clears the courses for a given schedule.
|
* 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.
|
* @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 schedules = await UserScheduleStore.get('schedules');
|
||||||
const schedule = schedules.find(schedule => schedule.name === scheduleName);
|
const schedule = schedules.find(schedule => schedule.id === scheduleId);
|
||||||
if (!schedule) {
|
if (!schedule) {
|
||||||
throw new Error(`Schedule ${scheduleName} does not exist`);
|
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||||
}
|
}
|
||||||
schedule.courses = [];
|
schedule.courses = [];
|
||||||
schedule.updatedAt = Date.now();
|
schedule.updatedAt = Date.now();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
|
import { generateRandomId } from '@shared/util/random';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new schedule with the given name
|
* 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> {
|
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
const schedules = await UserScheduleStore.get('schedules');
|
||||||
if (schedules.find(schedule => schedule.name === scheduleName)) {
|
// if (schedules.find(schedule => schedule.name === scheduleName)) {
|
||||||
return `Schedule ${scheduleName} already exists`;
|
// return `Schedule ${scheduleName} already exists`;
|
||||||
}
|
// }
|
||||||
|
|
||||||
schedules.push({
|
schedules.push({
|
||||||
|
id: generateRandomId(),
|
||||||
name: scheduleName,
|
name: scheduleName,
|
||||||
courses: [],
|
courses: [],
|
||||||
hours: 0,
|
hours: 0,
|
||||||
|
|||||||
@@ -3,18 +3,18 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
|||||||
/**
|
/**
|
||||||
* Deletes a schedule with the specified name.
|
* 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.
|
* @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([
|
const [schedules, activeIndex] = await Promise.all([
|
||||||
UserScheduleStore.get('schedules'),
|
UserScheduleStore.get('schedules'),
|
||||||
UserScheduleStore.get('activeIndex'),
|
UserScheduleStore.get('activeIndex'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
|
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||||
if (scheduleIndex === -1) {
|
if (scheduleIndex === -1) {
|
||||||
return `Schedule ${scheduleName} does not exist`;
|
return `Schedule ${scheduleId} does not exist`;
|
||||||
}
|
}
|
||||||
if (scheduleIndex === activeIndex) {
|
if (scheduleIndex === activeIndex) {
|
||||||
return 'Cannot delete active schedule';
|
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 schedules = await UserScheduleStore.get('schedules');
|
||||||
const activeSchedule = schedules.find(s => s.name === scheduleName);
|
const activeSchedule = schedules.find(s => s.id === scheduleId);
|
||||||
if (!activeSchedule) {
|
if (!activeSchedule) {
|
||||||
throw new Error('Schedule not found');
|
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.
|
* 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.
|
* @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.
|
* @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 schedules = await UserScheduleStore.get('schedules');
|
||||||
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
|
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||||
if (scheduleIndex === -1) {
|
if (scheduleIndex === -1) {
|
||||||
return `Schedule ${scheduleName} does not exist`;
|
return `Schedule ${scheduleId} does not exist`;
|
||||||
}
|
|
||||||
if (schedules.find(schedule => schedule.name === newName)) {
|
|
||||||
return `Schedule ${newName} already exists`;
|
|
||||||
}
|
}
|
||||||
|
// if (schedules.find(schedule => schedule.name === newName)) {
|
||||||
|
// return `Schedule ${newName} already exists`;
|
||||||
|
// }
|
||||||
|
|
||||||
schedules[scheduleIndex].name = newName;
|
schedules[scheduleIndex].name = newName;
|
||||||
schedules[scheduleIndex].updatedAt = Date.now();
|
schedules[scheduleIndex].updatedAt = Date.now();
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
|||||||
/**
|
/**
|
||||||
* Switches the active schedule to the specified schedule name.
|
* Switches the active schedule to the specified schedule name.
|
||||||
* Throws an error if the schedule does not exist.
|
* 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.
|
* @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 schedules = await UserScheduleStore.get('schedules');
|
||||||
|
|
||||||
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
|
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
|
||||||
if (scheduleIndex === -1) {
|
if (scheduleIndex === -1) {
|
||||||
throw new Error(`Schedule ${scheduleName} does not exist`);
|
throw new Error(`Schedule ${scheduleId} does not exist`);
|
||||||
}
|
}
|
||||||
schedules[scheduleIndex].updatedAt = Date.now();
|
schedules[scheduleIndex].updatedAt = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,25 @@ import type { Course } from '@shared/types/Course';
|
|||||||
export interface UserScheduleMessages {
|
export interface UserScheduleMessages {
|
||||||
/**
|
/**
|
||||||
* Add a course to a schedule
|
* 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
|
* 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
|
* 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
|
* 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
|
* Creates a new schedule with the specified name
|
||||||
@@ -34,14 +34,14 @@ export interface UserScheduleMessages {
|
|||||||
createSchedule: (data: { scheduleName: string }) => string | undefined;
|
createSchedule: (data: { scheduleName: string }) => string | undefined;
|
||||||
/**
|
/**
|
||||||
* Deletes a schedule with the specified name
|
* 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
|
* @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
|
* 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
|
* @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 { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
|
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
|
import { generateRandomId } from '../util/random';
|
||||||
|
|
||||||
interface IUserScheduleStore {
|
interface IUserScheduleStore {
|
||||||
schedules: UserSchedule[];
|
schedules: UserSchedule[];
|
||||||
activeIndex: number;
|
activeIndex: number;
|
||||||
@@ -13,6 +15,7 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
|
|||||||
schedules: [
|
schedules: [
|
||||||
new UserSchedule({
|
new UserSchedule({
|
||||||
courses: [],
|
courses: [],
|
||||||
|
id: generateRandomId(),
|
||||||
name: 'Schedule 1',
|
name: 'Schedule 1',
|
||||||
hours: 0,
|
hours: 0,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Serialized } from 'chrome-extension-toolkit';
|
import type { Serialized } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
|
import { generateRandomId } from '../util/random';
|
||||||
import { Course } from './Course';
|
import { Course } from './Course';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -7,6 +8,7 @@ import { Course } from './Course';
|
|||||||
*/
|
*/
|
||||||
export class UserSchedule {
|
export class UserSchedule {
|
||||||
courses: Course[];
|
courses: Course[];
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
hours: number;
|
hours: number;
|
||||||
/** Unix timestamp of when the schedule was last updated */
|
/** Unix timestamp of when the schedule was last updated */
|
||||||
@@ -14,12 +16,10 @@ export class UserSchedule {
|
|||||||
|
|
||||||
constructor(schedule: Serialized<UserSchedule>) {
|
constructor(schedule: Serialized<UserSchedule>) {
|
||||||
this.courses = schedule.courses.map(c => new Course(c));
|
this.courses = schedule.courses.map(c => new Course(c));
|
||||||
|
this.id = schedule.id ?? generateRandomId();
|
||||||
this.name = schedule.name;
|
this.name = schedule.name;
|
||||||
this.hours = 0;
|
this.hours = this.courses.reduce((acc, c) => acc + c.creditHours, 0);
|
||||||
for (const course of this.courses) {
|
this.updatedAt = schedule.updatedAt ?? 0;
|
||||||
this.hours += course.creditHours;
|
|
||||||
}
|
|
||||||
this.updatedAt = schedule.updatedAt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
containsCourse(course: Course): boolean {
|
containsCourse(course: Course): boolean {
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
/**
|
import { customAlphabet } from 'nanoid';
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random number between min and max
|
* Generate secure URL-friendly unique ID.
|
||||||
* @param min the minimum number
|
*
|
||||||
* @param max the maximum number
|
* @param size Size of the ID. The default size is 12.
|
||||||
* @returns a random number between min and max
|
* @returns A random string.
|
||||||
*/
|
*/
|
||||||
export function rangeRandom(min: number, max: number): number {
|
export const generateRandomId = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12);
|
||||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import { generateRandomId } from '@shared/util/random';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import List from '@views/components/common/List/List';
|
import List from '@views/components/common/List/List';
|
||||||
import ScheduleDropdown from '@views/components/common/ScheduleDropdown/ScheduleDropdown';
|
import ScheduleDropdown from '@views/components/common/ScheduleDropdown/ScheduleDropdown';
|
||||||
@@ -14,6 +15,7 @@ const schedules: UserSchedule[] = new Array(10).fill(exampleSchedule).map(
|
|||||||
(schedule: UserSchedule, index) =>
|
(schedule: UserSchedule, index) =>
|
||||||
new UserSchedule({
|
new UserSchedule({
|
||||||
...schedule,
|
...schedule,
|
||||||
|
id: generateRandomId(),
|
||||||
name: `Schedule ${index + 1}`,
|
name: `Schedule ${index + 1}`,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -61,10 +63,10 @@ const meta: Meta<typeof ScheduleDropdown> = {
|
|||||||
<ScheduleDropdown {...args}>
|
<ScheduleDropdown {...args}>
|
||||||
<List
|
<List
|
||||||
draggables={schedules}
|
draggables={schedules}
|
||||||
equalityCheck={(a, b) => a.name === b.name}
|
itemKey={s => s.id}
|
||||||
onReordered={reordered => {
|
onReordered={reordered => {
|
||||||
const activeSchedule = getActiveSchedule();
|
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
|
// don't care about the promise
|
||||||
UserScheduleStore.set('schedules', reordered);
|
UserScheduleStore.set('schedules', reordered);
|
||||||
@@ -74,9 +76,9 @@ const meta: Meta<typeof ScheduleDropdown> = {
|
|||||||
>
|
>
|
||||||
{(schedule, handleProps) => (
|
{(schedule, handleProps) => (
|
||||||
<ScheduleListItem
|
<ScheduleListItem
|
||||||
name={schedule.name}
|
schedule={schedule}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
switchSchedule(schedule.name);
|
switchSchedule(schedule.id);
|
||||||
}}
|
}}
|
||||||
dragHandleProps={handleProps}
|
dragHandleProps={handleProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const Default: Story = {
|
|||||||
args: {
|
args: {
|
||||||
draggables: exampleCourses.map((course, i) => ({ course, colors: tailwindColorways[i] })),
|
draggables: exampleCourses.map((course, i) => ({ course, colors: tailwindColorways[i] })),
|
||||||
children: generateCourseBlocks,
|
children: generateCourseBlocks,
|
||||||
|
itemKey: (item: { course: Course }) => item.course.uniqueId,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
render: args => (
|
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 ScheduleListItem from '@views/components/common/ScheduleListItem/ScheduleListItem';
|
||||||
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default {
|
import { exampleSchedule } from '../injected/mocked';
|
||||||
|
|
||||||
|
const meta = {
|
||||||
title: 'Components/Common/ScheduleListItem',
|
title: 'Components/Common/ScheduleListItem',
|
||||||
component: ScheduleListItem,
|
component: ScheduleListItem,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
tags: ['autodocs'],
|
|
||||||
},
|
},
|
||||||
argTypes: {
|
argTypes: {
|
||||||
active: { control: 'boolean' },
|
schedule: {
|
||||||
name: { control: 'text' },
|
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 Inactive: Story = {};
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
|
import { CalendarSchedules } from '@views/components/calendar/CalendarSchedules/CalendarSchedules';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -14,11 +9,7 @@ const meta = {
|
|||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
},
|
},
|
||||||
argTypes: {
|
render: args => (
|
||||||
// dummySchedules: { control: 'object' },
|
|
||||||
// dummyActiveIndex: { control: 'number' },
|
|
||||||
},
|
|
||||||
render: (args: any) => (
|
|
||||||
<div>
|
<div>
|
||||||
<CalendarSchedules {...args} />
|
<CalendarSchedules {...args} />
|
||||||
</div>
|
</div>
|
||||||
@@ -28,122 +19,4 @@ const meta = {
|
|||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
const schedules = [
|
export const Default: Story = {};
|
||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import { Status } from '@shared/types/Course';
|
import { Status } from '@shared/types/Course';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import { bevoCourse, bevoScheule, MikeScottCS314Course, MikeScottCS314Schedule } from './mocked';
|
import { bevoCourse, mikeScottCS314Course } from './mocked';
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
title: 'Components/Injected/CourseCatalogInjectedPopup',
|
||||||
@@ -14,23 +13,7 @@ const meta = {
|
|||||||
open: true,
|
open: true,
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
},
|
},
|
||||||
argTypes: {
|
tags: ['autodocs'],
|
||||||
course: {
|
|
||||||
control: {
|
|
||||||
type: 'object',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
activeSchedule: {
|
|
||||||
control: {
|
|
||||||
type: 'object',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onClose: {
|
|
||||||
control: {
|
|
||||||
type: 'function',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
render(args) {
|
render(args) {
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
const [isOpen, setIsOpen] = useState(args.open);
|
const [isOpen, setIsOpen] = useState(args.open);
|
||||||
@@ -44,24 +27,21 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const OpenCourse: Story = {
|
export const OpenCourse: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: MikeScottCS314Course,
|
course: mikeScottCS314Course,
|
||||||
activeSchedule: MikeScottCS314Schedule,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClosedCourse: Story = {
|
export const ClosedCourse: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: {
|
course: {
|
||||||
...MikeScottCS314Course,
|
...mikeScottCS314Course,
|
||||||
status: Status.CLOSED,
|
status: Status.CLOSED,
|
||||||
} as Course,
|
} as Course,
|
||||||
activeSchedule: new UserSchedule({ courses: [], name: '', hours: 0, updatedAt: Date.now() }),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CourseWithNoData: Story = {
|
export const CourseWithNoData: Story = {
|
||||||
args: {
|
args: {
|
||||||
course: bevoCourse,
|
course: bevoCourse,
|
||||||
activeSchedule: bevoScheule,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const exampleCourse: Course = new Course({
|
|||||||
|
|
||||||
export const exampleSchedule: UserSchedule = new UserSchedule({
|
export const exampleSchedule: UserSchedule = new UserSchedule({
|
||||||
courses: [exampleCourse],
|
courses: [exampleCourse],
|
||||||
|
id: 'az372389blep',
|
||||||
name: 'Example Schedule',
|
name: 'Example Schedule',
|
||||||
hours: 3,
|
hours: 3,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
@@ -105,14 +106,15 @@ export const bevoCourse: Course = new Course({
|
|||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const bevoScheule: UserSchedule = new UserSchedule({
|
export const bevoSchedule: UserSchedule = new UserSchedule({
|
||||||
courses: [bevoCourse],
|
courses: [bevoCourse],
|
||||||
|
id: 'bevoshenanigans52',
|
||||||
name: 'Bevo Schedule',
|
name: 'Bevo Schedule',
|
||||||
hours: 3,
|
hours: 3,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MikeScottCS314Course: Course = new Course({
|
export const mikeScottCS314Course: Course = new Course({
|
||||||
uniqueId: 50805,
|
uniqueId: 50805,
|
||||||
number: '314',
|
number: '314',
|
||||||
fullName: 'C S 314 DATA STRUCTURES',
|
fullName: 'C S 314 DATA STRUCTURES',
|
||||||
@@ -158,8 +160,9 @@ export const MikeScottCS314Course: Course = new Course({
|
|||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MikeScottCS314Schedule: UserSchedule = new UserSchedule({
|
export const mikeScottCS314Schedule: UserSchedule = new UserSchedule({
|
||||||
courses: [MikeScottCS314Course],
|
courses: [mikeScottCS314Course],
|
||||||
|
id: 'omgitsmikescott314',
|
||||||
name: 'Mike Scott CS314 Schedule',
|
name: 'Mike Scott CS314 Schedule',
|
||||||
hours: 3,
|
hours: 3,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
<CourseCatalogInjectedPopup
|
<CourseCatalogInjectedPopup
|
||||||
course={selectedCourse}
|
course={selectedCourse}
|
||||||
activeSchedule={activeSchedule}
|
|
||||||
show={showPopup}
|
show={showPopup}
|
||||||
onClose={() => setShowPopup(false)}
|
onClose={() => setShowPopup(false)}
|
||||||
afterLeave={() => setSelectedCourse(null)}
|
afterLeave={() => setSelectedCourse(null)}
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ export default function PopupMain(): JSX.Element {
|
|||||||
<ScheduleDropdown>
|
<ScheduleDropdown>
|
||||||
<List
|
<List
|
||||||
draggables={schedules}
|
draggables={schedules}
|
||||||
equalityCheck={(a, b) => a.name === b.name}
|
itemKey={schedule => schedule.id}
|
||||||
onReordered={reordered => {
|
onReordered={reordered => {
|
||||||
const activeSchedule = getActiveSchedule();
|
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
|
// don't care about the promise
|
||||||
UserScheduleStore.set('schedules', reordered);
|
UserScheduleStore.set('schedules', reordered);
|
||||||
@@ -77,9 +77,9 @@ export default function PopupMain(): JSX.Element {
|
|||||||
>
|
>
|
||||||
{(schedule, handleProps) => (
|
{(schedule, handleProps) => (
|
||||||
<ScheduleListItem
|
<ScheduleListItem
|
||||||
name={schedule.name}
|
schedule={schedule}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
switchSchedule(schedule.name);
|
switchSchedule(schedule.id);
|
||||||
}}
|
}}
|
||||||
dragHandleProps={handleProps}
|
dragHandleProps={handleProps}
|
||||||
/>
|
/>
|
||||||
@@ -98,7 +98,7 @@ export default function PopupMain(): JSX.Element {
|
|||||||
activeSchedule.courses = reordered.map(c => c.course);
|
activeSchedule.courses = reordered.map(c => c.course);
|
||||||
replaceSchedule(getActiveSchedule(), activeSchedule);
|
replaceSchedule(getActiveSchedule(), activeSchedule);
|
||||||
}}
|
}}
|
||||||
equalityCheck={(a, b) => a.course.uniqueId === b.course.uniqueId}
|
itemKey={e => e.course.uniqueId}
|
||||||
gap={10}
|
gap={10}
|
||||||
>
|
>
|
||||||
{({ course, colors }, handleProps) => (
|
{({ course, colors }, handleProps) => (
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export default function Calendar(): JSX.Element {
|
|||||||
|
|
||||||
<CourseCatalogInjectedPopup
|
<CourseCatalogInjectedPopup
|
||||||
course={course}
|
course={course}
|
||||||
activeSchedule={activeSchedule}
|
|
||||||
onClose={() => setShowPopup(false)}
|
onClose={() => setShowPopup(false)}
|
||||||
open={showPopup}
|
open={showPopup}
|
||||||
afterLeave={() => setCourse(null)}
|
afterLeave={() => setCourse(null)}
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ export default function CalendarHeader(): JSX.Element {
|
|||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
<LogoIcon />
|
<LogoIcon />
|
||||||
<div className='flex flex-col whitespace-nowrap'>
|
<div className='flex flex-col whitespace-nowrap'>
|
||||||
<Text className='text-lg! text-ut-burntorange font-medium!'>UT Registration</Text>
|
<Text className='text-ut-burntorange text-lg! font-medium!'>UT Registration</Text>
|
||||||
<Text className='text-lg! text-ut-orange font-medium!'>Plus</Text>
|
<Text className='text-ut-orange text-lg! font-medium!'>Plus</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +52,7 @@ export default function CalendarHeader(): JSX.Element {
|
|||||||
totalHours={activeSchedule.hours}
|
totalHours={activeSchedule.hours}
|
||||||
totalCourses={activeSchedule.courses.length}
|
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)}
|
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
|
|||||||
const [activeSchedule, schedules] = useSchedules();
|
const [activeSchedule, schedules] = useSchedules();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const index = schedules.findIndex(schedule => schedule.name === activeSchedule.name);
|
const index = schedules.findIndex(schedule => schedule.id === activeSchedule.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
setActiveScheduleIndex(index);
|
setActiveScheduleIndex(index);
|
||||||
}
|
}
|
||||||
@@ -68,10 +68,10 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
|
|||||||
<List
|
<List
|
||||||
gap={10}
|
gap={10}
|
||||||
draggables={schedules}
|
draggables={schedules}
|
||||||
equalityCheck={(a, b) => a.name === b.name}
|
itemKey={s => s.id}
|
||||||
onReordered={reordered => {
|
onReordered={reordered => {
|
||||||
const activeSchedule = getActiveSchedule();
|
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
|
// don't care about the promise
|
||||||
UserScheduleStore.set('schedules', reordered);
|
UserScheduleStore.set('schedules', reordered);
|
||||||
@@ -80,9 +80,9 @@ export function CalendarSchedules({ style, dummySchedules, dummyActiveIndex }: P
|
|||||||
>
|
>
|
||||||
{(schedule, handleProps) => (
|
{(schedule, handleProps) => (
|
||||||
<ScheduleListItem
|
<ScheduleListItem
|
||||||
name={schedule.name}
|
schedule={schedule}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
switchSchedule(schedule.name);
|
switchSchedule(schedule.id);
|
||||||
}}
|
}}
|
||||||
dragHandleProps={handleProps}
|
dragHandleProps={handleProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
|
|||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import React, { useCallback, useEffect, useState } 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)
|
* 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[];
|
draggables: T[];
|
||||||
children: (draggable: T, handleProps: DraggableProvidedDragHandleProps) => ReactElement;
|
children: (draggable: T, handleProps: DraggableProvidedDragHandleProps) => ReactElement;
|
||||||
onReordered: (elements: T[]) => void;
|
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
|
gap?: number; // Impacts the spacing between items in the list
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrap<T>(draggableElements: T[]) {
|
function wrap<T>(draggableElements: T[], keyTransform: ListProps<T>['itemKey']) {
|
||||||
return draggableElements.map((element, index) => ({
|
return draggableElements.map(element => ({
|
||||||
id: `id:${index}`,
|
id: keyTransform(element),
|
||||||
content: element,
|
content: element,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -68,20 +70,19 @@ function Item<T>(props: {
|
|||||||
* <List draggableElements={elements} />
|
* <List draggableElements={elements} />
|
||||||
*/
|
*/
|
||||||
function List<T>(props: ListProps<T>): JSX.Element {
|
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;
|
const transformFunction = props.children;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// check if the draggables content has *actually* changed
|
// check if the draggables content has *actually* changed
|
||||||
if (
|
if (
|
||||||
props.draggables.length === items.length &&
|
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;
|
return;
|
||||||
}
|
}
|
||||||
setItems(wrap(props.draggables));
|
setItems(wrap(props.draggables, props.itemKey));
|
||||||
}, [props.draggables]);
|
}, [props.draggables]);
|
||||||
|
|
||||||
const onDragEnd: OnDragEndResponder = useCallback(
|
const onDragEnd: OnDragEndResponder = useCallback(
|
||||||
@@ -123,7 +124,9 @@ function List<T>(props: ListProps<T>): JSX.Element {
|
|||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
|
<ExtensionRoot>
|
||||||
|
{transformFunction(items[rubric.source.index].content, provided.dragHandleProps)}
|
||||||
|
</ExtensionRoot>
|
||||||
</Item>
|
</Item>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -135,7 +138,7 @@ function List<T>(props: ListProps<T>): JSX.Element {
|
|||||||
style={{ marginBottom: `-${props.gap}px` }}
|
style={{ marginBottom: `-${props.gap}px` }}
|
||||||
>
|
>
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Draggable key={item.id} draggableId={item.id} index={index}>
|
<Draggable key={item.id} draggableId={item.id.toString()} index={index}>
|
||||||
{draggableProvided => (
|
{draggableProvided => (
|
||||||
<div
|
<div
|
||||||
ref={draggableProvided.innerRef}
|
ref={draggableProvided.innerRef}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import useSchedules from '@views/hooks/useSchedules';
|
import useSchedules from '@views/hooks/useSchedules';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -10,7 +11,7 @@ import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
|
|||||||
*/
|
*/
|
||||||
export type Props = {
|
export type Props = {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
name: string;
|
schedule: UserSchedule;
|
||||||
dragHandleProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className'>;
|
dragHandleProps?: Omit<React.HTMLAttributes<HTMLDivElement>, 'className'>;
|
||||||
onClick?: React.DOMAttributes<HTMLDivElement>['onClick'];
|
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
|
* 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 [activeSchedule] = useSchedules();
|
||||||
|
|
||||||
const isActive = useMemo(() => activeSchedule.name === name, [activeSchedule, name]);
|
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...style }} className='items-center rounded bg-white'>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import type { UserSchedule } from '@shared/types/UserSchedule';
|
|
||||||
import type { DialogProps } from '@views/components/common/Dialog/Dialog';
|
import type { DialogProps } from '@views/components/common/Dialog/Dialog';
|
||||||
import Dialog 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 React from 'react';
|
||||||
|
|
||||||
import Description from './Description';
|
import Description from './Description';
|
||||||
@@ -10,7 +10,6 @@ import HeadingAndActions from './HeadingAndActions';
|
|||||||
|
|
||||||
export type CourseCatalogInjectedPopupProps = DialogProps & {
|
export type CourseCatalogInjectedPopupProps = DialogProps & {
|
||||||
course: Course;
|
course: Course;
|
||||||
activeSchedule: UserSchedule;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,8 +22,9 @@ export type CourseCatalogInjectedPopupProps = DialogProps & {
|
|||||||
* @param {Function} props.onClose - The function to close the popup.
|
* @param {Function} props.onClose - The function to close the popup.
|
||||||
* @returns {JSX.Element} The CourseCatalogInjectedPopup component.
|
* @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 emptyRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
const [activeSchedule] = useSchedules();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog className='max-w-[780px] px-6' {...rest} initialFocus={emptyRef}>
|
<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 () => {
|
const handleAddOrRemoveCourse = async () => {
|
||||||
if (!activeSchedule) return;
|
if (!activeSchedule) return;
|
||||||
if (!courseAdded) {
|
if (!courseAdded) {
|
||||||
addCourse({ course, scheduleName: activeSchedule.name });
|
addCourse({ course, scheduleId: activeSchedule.id });
|
||||||
} else {
|
} else {
|
||||||
removeCourse({ course, scheduleName: activeSchedule.name });
|
removeCourse({ course, scheduleId: activeSchedule.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export function useFlattenedCourseSchedule(): FlattenedCourseSchedule {
|
|||||||
courseCells: [] as CalendarGridCourse[],
|
courseCells: [] as CalendarGridCourse[],
|
||||||
activeSchedule: new UserSchedule({
|
activeSchedule: new UserSchedule({
|
||||||
courses: [],
|
courses: [],
|
||||||
|
id: 'error',
|
||||||
name: 'Something may have went wrong',
|
name: 'Something may have went wrong',
|
||||||
hours: 0,
|
hours: 0,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
|
|||||||
@@ -3,9 +3,17 @@ import { UserSchedule } from '@shared/types/UserSchedule';
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
let schedulesCache = [];
|
let schedulesCache = [];
|
||||||
let activeIndexCache = 0;
|
let activeIndexCache = -1;
|
||||||
let initialLoad = true;
|
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.
|
* 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[]] {
|
export default function useSchedules(): [active: UserSchedule, schedules: UserSchedule[]] {
|
||||||
const [schedules, setSchedules] = useState<UserSchedule[]>(schedulesCache);
|
const [schedules, setSchedules] = useState<UserSchedule[]>(schedulesCache);
|
||||||
const [activeIndex, setActiveIndex] = useState<number>(activeIndexCache);
|
const [activeIndex, setActiveIndex] = useState<number>(activeIndexCache);
|
||||||
const [activeSchedule, setActiveSchedule] = useState<UserSchedule>(schedules[activeIndex]);
|
const [activeSchedule, setActiveSchedule] = useState<UserSchedule>(schedules[activeIndex] ?? errorSchedule);
|
||||||
|
|
||||||
if (initialLoad) {
|
if (initialLoad) {
|
||||||
initialLoad = false;
|
initialLoad = false;
|
||||||
@@ -56,22 +64,19 @@ export default function useSchedules(): [active: UserSchedule, schedules: UserSc
|
|||||||
|
|
||||||
// recompute active schedule on a schedule/index change
|
// recompute active schedule on a schedule/index change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveSchedule(schedules[activeIndex]);
|
setActiveSchedule(schedules[activeIndex] ?? errorSchedule);
|
||||||
}, [activeIndex, schedules]);
|
}, [activeIndex, schedules]);
|
||||||
|
|
||||||
return [activeSchedule, schedules];
|
return [activeSchedule, schedules];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getActiveSchedule(): UserSchedule {
|
export function getActiveSchedule(): UserSchedule {
|
||||||
return (
|
return schedulesCache[activeIndexCache] ?? errorSchedule;
|
||||||
schedulesCache[activeIndexCache] ||
|
|
||||||
new UserSchedule({ courses: [], name: 'An error has occurred', hours: 0, updatedAt: Date.now() })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: UserSchedule) {
|
export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: UserSchedule) {
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
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;
|
oldIndex = oldIndex !== -1 ? oldIndex : 0;
|
||||||
schedules[oldIndex] = newSchedule;
|
schedules[oldIndex] = newSchedule;
|
||||||
await UserScheduleStore.set('schedules', schedules);
|
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.
|
* 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.
|
* @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...');
|
console.log('Switching schedule...');
|
||||||
const schedules = await UserScheduleStore.get('schedules');
|
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);
|
await UserScheduleStore.set('activeIndex', activeIndex);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user