feat: course colors (#175)
* feat: course colors * docs: fix typo in jsdoc
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { type CourseColors, getCourseColors } from '@shared/util/colors';
|
||||||
import type { Serialized } from 'chrome-extension-toolkit';
|
import type { Serialized } from 'chrome-extension-toolkit';
|
||||||
|
|
||||||
import type { CourseMeeting } from './CourseMeeting';
|
import type { CourseMeeting } from './CourseMeeting';
|
||||||
@@ -75,6 +76,8 @@ export class Course {
|
|||||||
semester: Semester;
|
semester: Semester;
|
||||||
/** Unix timestamp of when the course was last scraped */
|
/** Unix timestamp of when the course was last scraped */
|
||||||
scrapedAt: number;
|
scrapedAt: number;
|
||||||
|
/** The colors of the course when displayed */
|
||||||
|
colors: CourseColors;
|
||||||
|
|
||||||
constructor(course: Serialized<Course>) {
|
constructor(course: Serialized<Course>) {
|
||||||
Object.assign(this, course);
|
Object.assign(this, course);
|
||||||
@@ -83,6 +86,7 @@ export class Course {
|
|||||||
if (!course.scrapedAt) {
|
if (!course.scrapedAt) {
|
||||||
this.scrapedAt = Date.now();
|
this.scrapedAt = Date.now();
|
||||||
}
|
}
|
||||||
|
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Course, Status } from '@shared/types/Course';
|
import { Course, Status } from '@shared/types/Course';
|
||||||
import { CourseMeeting } from '@shared/types/CourseMeeting';
|
import { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||||
import Instructor from '@shared/types/Instructor';
|
import Instructor from '@shared/types/Instructor';
|
||||||
|
import { getCourseColors } from '@shared/util/colors';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
|
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ export const ExampleCourse: Course = new Course({
|
|||||||
uniqueId: 12345,
|
uniqueId: 12345,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
|
colors: getCourseColors('blue', 500),
|
||||||
});
|
});
|
||||||
export const ExampleCourse2: Course = new Course({
|
export const ExampleCourse2: Course = new Course({
|
||||||
courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
|
courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
|
||||||
@@ -92,6 +94,7 @@ export const ExampleCourse2: Course = new Course({
|
|||||||
uniqueId: 67890,
|
uniqueId: 67890,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
|
colors: getCourseColors('yellow', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
|
|||||||
import { Course, Status } from '@shared/types/Course';
|
import { Course, Status } from '@shared/types/Course';
|
||||||
import { CourseMeeting } from '@shared/types/CourseMeeting';
|
import { CourseMeeting } from '@shared/types/CourseMeeting';
|
||||||
import Instructor from '@shared/types/Instructor';
|
import Instructor from '@shared/types/Instructor';
|
||||||
import type { CourseColors } from '@shared/util/colors';
|
|
||||||
import { tailwindColorways } from '@shared/util/storybook';
|
import { tailwindColorways } from '@shared/util/storybook';
|
||||||
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';
|
||||||
@@ -63,6 +62,7 @@ const generateCourses = (count: number): Course[] => {
|
|||||||
status: Status.WAITLISTED,
|
status: Status.WAITLISTED,
|
||||||
uniqueId: 12345 + i, // Make uniqueId different for each course
|
uniqueId: 12345 + i, // Make uniqueId different for each course
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
colors: tailwindColorways[i],
|
||||||
});
|
});
|
||||||
|
|
||||||
courses.push(course);
|
courses.push(course);
|
||||||
@@ -72,10 +72,9 @@ const generateCourses = (count: number): Course[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const exampleCourses = generateCourses(numberOfCourses);
|
const exampleCourses = generateCourses(numberOfCourses);
|
||||||
const generateCourseBlocks = (
|
const generateCourseBlocks = (course: Course, dragHandleProps: DraggableProvidedDragHandleProps) => (
|
||||||
{ course, colors }: { course: Course; colors: CourseColors },
|
<PopupCourseBlock key={course.uniqueId} course={course} colors={course.colors} dragHandleProps={dragHandleProps} />
|
||||||
dragHandleProps: DraggableProvidedDragHandleProps
|
);
|
||||||
) => <PopupCourseBlock key={course.uniqueId} course={course} colors={colors} dragHandleProps={dragHandleProps} />;
|
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/List',
|
title: 'Components/Common/List',
|
||||||
@@ -94,9 +93,9 @@ type Story = StoryObj<typeof meta>;
|
|||||||
|
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
draggables: exampleCourses.map((course, i) => ({ course, colors: tailwindColorways[i] })),
|
draggables: exampleCourses,
|
||||||
children: generateCourseBlocks,
|
children: generateCourseBlocks,
|
||||||
itemKey: (item: { course: Course }) => item.course.uniqueId,
|
itemKey: (item: Course) => item.uniqueId,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
render: args => (
|
render: args => (
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export const ExampleCourse: Course = new Course({
|
|||||||
status: Status.WAITLISTED,
|
status: Status.WAITLISTED,
|
||||||
uniqueId: 12345,
|
uniqueId: 12345,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
colors: getCourseColors('cyan', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const exampleGovCourse: Course = new Course({
|
|||||||
status: Status.OPEN,
|
status: Status.OPEN,
|
||||||
uniqueId: 12345,
|
uniqueId: 12345,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
colors: getCourseColors('red', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
const examplePsyCourse: Course = new Course({
|
const examplePsyCourse: Course = new Course({
|
||||||
@@ -65,6 +66,7 @@ const examplePsyCourse: Course = new Course({
|
|||||||
status: Status.CLOSED,
|
status: Status.CLOSED,
|
||||||
uniqueId: 12346,
|
uniqueId: 12346,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
colors: getCourseColors('blue', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Course, Status } from '@shared/types/Course';
|
|||||||
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
|
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
|
||||||
import { CourseSchedule } from '@shared/types/CourseSchedule';
|
import { CourseSchedule } from '@shared/types/CourseSchedule';
|
||||||
import Instructor from '@shared/types/Instructor';
|
import Instructor from '@shared/types/Instructor';
|
||||||
|
import { getCourseColors } from '@shared/util/colors';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import CalendarCourse from '@views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting';
|
import CalendarCourse from '@views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting';
|
||||||
|
|
||||||
@@ -15,7 +16,6 @@ const meta = {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
course: { control: 'object' },
|
course: { control: 'object' },
|
||||||
meetingIdx: { control: 'number' },
|
meetingIdx: { control: 'number' },
|
||||||
color: { control: 'color' },
|
|
||||||
rightIcon: { control: 'object' },
|
rightIcon: { control: 'object' },
|
||||||
},
|
},
|
||||||
} satisfies Meta<typeof CalendarCourse>;
|
} satisfies Meta<typeof CalendarCourse>;
|
||||||
@@ -56,8 +56,8 @@ export const Default: Story = {
|
|||||||
season: 'Spring',
|
season: 'Spring',
|
||||||
},
|
},
|
||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
}),
|
}),
|
||||||
meetingIdx: 0,
|
meetingIdx: 0,
|
||||||
color: 'red',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Course, Status } from '@shared/types/Course';
|
|||||||
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
|
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
|
||||||
import Instructor from '@shared/types/Instructor';
|
import Instructor from '@shared/types/Instructor';
|
||||||
import { UserSchedule } from '@shared/types/UserSchedule';
|
import { UserSchedule } from '@shared/types/UserSchedule';
|
||||||
|
import { getCourseColors } from '@shared/util/colors';
|
||||||
|
|
||||||
export const exampleCourse: Course = new Course({
|
export const exampleCourse: Course = new Course({
|
||||||
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
|
||||||
@@ -51,6 +52,7 @@ export const exampleCourse: Course = new Course({
|
|||||||
status: Status.OPEN,
|
status: Status.OPEN,
|
||||||
uniqueId: 12345,
|
uniqueId: 12345,
|
||||||
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
|
||||||
|
colors: getCourseColors('blue', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exampleSchedule: UserSchedule = new UserSchedule({
|
export const exampleSchedule: UserSchedule = new UserSchedule({
|
||||||
@@ -104,6 +106,7 @@ export const bevoCourse: Course = new Course({
|
|||||||
season: 'Spring',
|
season: 'Spring',
|
||||||
},
|
},
|
||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
|
colors: getCourseColors('green', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const bevoSchedule: UserSchedule = new UserSchedule({
|
export const bevoSchedule: UserSchedule = new UserSchedule({
|
||||||
@@ -158,6 +161,7 @@ export const mikeScottCS314Course: Course = new Course({
|
|||||||
season: 'Spring',
|
season: 'Spring',
|
||||||
},
|
},
|
||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
|
colors: getCourseColors('orange', 500),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mikeScottCS314Schedule: UserSchedule = new UserSchedule({
|
export const mikeScottCS314Schedule: UserSchedule = new UserSchedule({
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { background } from '@shared/messages';
|
import { background } from '@shared/messages';
|
||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
import { tailwindColorways } from '@shared/util/storybook';
|
|
||||||
import Divider from '@views/components/common/Divider/Divider';
|
import Divider from '@views/components/common/Divider/Divider';
|
||||||
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
|
||||||
import List from '@views/components/common/List/List';
|
import List from '@views/components/common/List/List';
|
||||||
@@ -86,22 +85,19 @@ export default function PopupMain(): JSX.Element {
|
|||||||
<div className='flex-1 self-stretch overflow-y-auto px-5'>
|
<div className='flex-1 self-stretch overflow-y-auto px-5'>
|
||||||
{activeSchedule?.courses?.length > 0 && (
|
{activeSchedule?.courses?.length > 0 && (
|
||||||
<List
|
<List
|
||||||
draggables={activeSchedule.courses.map((course, i) => ({
|
draggables={activeSchedule.courses}
|
||||||
course,
|
|
||||||
colors: tailwindColorways[i],
|
|
||||||
}))}
|
|
||||||
onReordered={reordered => {
|
onReordered={reordered => {
|
||||||
activeSchedule.courses = reordered.map(c => c.course);
|
activeSchedule.courses = reordered;
|
||||||
replaceSchedule(getActiveSchedule(), activeSchedule);
|
replaceSchedule(getActiveSchedule(), activeSchedule);
|
||||||
}}
|
}}
|
||||||
itemKey={e => e.course.uniqueId}
|
itemKey={e => e.uniqueId}
|
||||||
gap={10}
|
gap={10}
|
||||||
>
|
>
|
||||||
{({ course, colors }, handleProps) => (
|
{(course, handleProps) => (
|
||||||
<PopupCourseBlock
|
<PopupCourseBlock
|
||||||
key={course.uniqueId}
|
key={course.uniqueId}
|
||||||
course={course}
|
course={course}
|
||||||
colors={colors}
|
colors={course.colors}
|
||||||
dragHandleProps={handleProps}
|
dragHandleProps={handleProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ export interface CalendarCourseMeetingProps {
|
|||||||
course: Course;
|
course: Course;
|
||||||
/* index into course meeting array to display */
|
/* index into course meeting array to display */
|
||||||
meetingIdx?: number;
|
meetingIdx?: number;
|
||||||
/** The background color for the course. */
|
|
||||||
color: string;
|
|
||||||
/** The icon to display on the right side of the course. This is optional. */
|
/** The icon to display on the right side of the course. This is optional. */
|
||||||
rightIcon?: React.ReactNode;
|
rightIcon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -22,12 +20,11 @@ export interface CalendarCourseMeetingProps {
|
|||||||
* `CalendarCourseMeeting` is a functional component that displays a course meeting.
|
* `CalendarCourseMeeting` is a functional component that displays a course meeting.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* <CalendarCourseMeeting course={course} meeting={meeting} color="red" rightIcon={<Icon />} />
|
* <CalendarCourseMeeting course={course} meeting={meeting} rightIcon={<Icon />} />
|
||||||
*/
|
*/
|
||||||
export default function CalendarCourseMeeting({
|
export default function CalendarCourseMeeting({
|
||||||
course,
|
course,
|
||||||
meetingIdx,
|
meetingIdx,
|
||||||
color,
|
|
||||||
rightIcon,
|
rightIcon,
|
||||||
}: CalendarCourseMeetingProps): JSX.Element {
|
}: CalendarCourseMeetingProps): JSX.Element {
|
||||||
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import { getCourseColors } from '@shared/util/colors';
|
|
||||||
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell/CalendarCourseCell';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
|
||||||
@@ -120,11 +119,8 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Part of TODO: block.course is definitely a course object
|
|
||||||
// console.log(courseCells);
|
|
||||||
|
|
||||||
return courseCells.map((block, i) => {
|
return courseCells.map((block, i) => {
|
||||||
const { courseDeptAndInstr, timeAndLocation, status, colors } = courseCells[i].componentProps;
|
const { courseDeptAndInstr, timeAndLocation, status } = courseCells[i].componentProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -141,8 +137,7 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
|
|||||||
courseDeptAndInstr={courseDeptAndInstr}
|
courseDeptAndInstr={courseDeptAndInstr}
|
||||||
timeAndLocation={timeAndLocation}
|
timeAndLocation={timeAndLocation}
|
||||||
status={status}
|
status={status}
|
||||||
// TODO: Change to block.componentProps.colors when colors are integrated to the rest of the project
|
colors={block.course.colors}
|
||||||
colors={getCourseColors('emerald', 500) /* block.componentProps.colors */}
|
|
||||||
onClick={() => setCourse(block.course)}
|
onClick={() => setCourse(block.course)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { InstructionMode, ScrapedRow, Semester, StatusType } from '@shared/
|
|||||||
import { Course, Status } from '@shared/types/Course';
|
import { Course, Status } from '@shared/types/Course';
|
||||||
import { CourseSchedule } from '@shared/types/CourseSchedule';
|
import { CourseSchedule } from '@shared/types/CourseSchedule';
|
||||||
import Instructor from '@shared/types/Instructor';
|
import Instructor from '@shared/types/Instructor';
|
||||||
|
import { getCourseColors } from '@shared/util/colors';
|
||||||
import type { SiteSupportType } from '@views/lib/getSiteSupport';
|
import type { SiteSupportType } from '@views/lib/getSiteSupport';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,6 +94,7 @@ export class CourseCatalogScraper {
|
|||||||
description: this.getDescription(document),
|
description: this.getDescription(document),
|
||||||
semester: this.getSemester(),
|
semester: this.getSemester(),
|
||||||
scrapedAt: Date.now(),
|
scrapedAt: Date.now(),
|
||||||
|
colors: getCourseColors('emerald', 500),
|
||||||
});
|
});
|
||||||
courses.push({
|
courses.push({
|
||||||
element: row,
|
element: row,
|
||||||
|
|||||||
Reference in New Issue
Block a user