From ee4c6ce6999ad35821b9be5d657790f2dee017b3 Mon Sep 17 00:00:00 2001 From: Preston Cook Date: Thu, 13 Feb 2025 18:07:05 -0600 Subject: [PATCH] feat(ui): update popup and course blocks (#506) * feat(ui): add time and location to popup * feat(ui): memoize meeting times * feat(ui): remove resizing * feat(ui): add no select to copy course id button * feat(ui): complete update to popup and course blocks * chore: update settings page * chore: fix types * fix(ui): update spacing, padding, and remove last updated section * chore: fix type issues * fix(ui): update borders to offwhite/50 * fix(ui): apply proper offwhite styling * fix(ui): add unique key to async courses in bottom bar --- src/pages/popup/index.html | 2 +- src/shared/storage/OptionsStore.ts | 7 +- src/shared/types/CourseMeeting.ts | 14 +- ...t.stories.tsx => SortableList.stories.tsx} | 2 +- src/views/components/PopupMain.tsx | 81 +++++----- src/views/components/calendar/Calendar.tsx | 2 +- .../components/calendar/CalendarBottomBar.tsx | 2 +- .../CourseCellColorPicker.tsx | 2 +- .../CalendarHeader/CalendarHeader.tsx | 2 +- src/views/components/common/Divider.tsx | 2 +- .../components/common/PopupCourseBlock.tsx | 144 ++++++++++++------ .../components/common/ScheduleDropdown.tsx | 2 +- .../components/common/ScheduleListItem.tsx | 2 +- src/views/components/settings/Settings.tsx | 22 ++- 14 files changed, 163 insertions(+), 123 deletions(-) rename src/stories/components/{List.stories.tsx => SortableList.stories.tsx} (98%) diff --git a/src/pages/popup/index.html b/src/pages/popup/index.html index 8a62efef..7d5049b1 100644 --- a/src/pages/popup/index.html +++ b/src/pages/popup/index.html @@ -9,7 +9,7 @@ body { margin: 0; padding: 0; - width: 400px; + width: 430px; height: 540px; } diff --git a/src/shared/storage/OptionsStore.ts b/src/shared/storage/OptionsStore.ts index 9e51bdd5..ce45c934 100644 --- a/src/shared/storage/OptionsStore.ts +++ b/src/shared/storage/OptionsStore.ts @@ -7,9 +7,6 @@ export interface IOptionsStore { /** whether we should enable course status chips (indicator for waitlisted, cancelled, and closed courses) */ enableCourseStatusChips: boolean; - /** whether we should enable course's time and location in the extension's popup */ - enableTimeAndLocationInPopup: boolean; - /** whether we should automatically highlight conflicts on the course schedule page (adds a red strikethrough to courses that have conflicting times) */ enableHighlightConflicts: boolean; @@ -25,10 +22,9 @@ export interface IOptionsStore { export const OptionsStore = createSyncStore({ enableCourseStatusChips: false, - enableTimeAndLocationInPopup: false, enableHighlightConflicts: true, enableScrollToLoad: true, - enableDataRefreshing: true, + enableDataRefreshing: false, alwaysOpenCalendarInNewTab: false, }); @@ -40,7 +36,6 @@ export const OptionsStore = createSyncStore({ export const initSettings = async () => ({ enableCourseStatusChips: await OptionsStore.get('enableCourseStatusChips'), - enableTimeAndLocationInPopup: await OptionsStore.get('enableTimeAndLocationInPopup'), enableHighlightConflicts: await OptionsStore.get('enableHighlightConflicts'), enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'), enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'), diff --git a/src/shared/types/CourseMeeting.ts b/src/shared/types/CourseMeeting.ts index 2119f5b6..b14c0a22 100644 --- a/src/shared/types/CourseMeeting.ts +++ b/src/shared/types/CourseMeeting.ts @@ -53,9 +53,8 @@ export class CourseMeeting { * @param meeting - The meeting to check for conflicts with * @returns True if the given meeting conflicts with this meeting, false otherwise */ - isConflicting(meeting: CourseMeeting): boolean { + isConflicting({ days: otherDays, startTime: otherStartTime, endTime: otherEndTime }: CourseMeeting): boolean { const { days, startTime, endTime } = this; - const { days: otherDays, startTime: otherStartTime, endTime: otherEndTime } = meeting; const hasDayConflict = days.some(day => otherDays.includes(day)); const hasTimeConflict = startTime < otherEndTime && endTime > otherStartTime; @@ -69,14 +68,13 @@ export class CourseMeeting { * @param options - Options for the string representation * @returns String representation of the days of the week that this meeting is taught */ - getDaysString(options: DaysStringOptions): string { - let { format, separator } = options; + getDaysString({ format, separator }: DaysStringOptions): string { let { days } = this; if (format === 'short') { days = Object.keys(DAY_MAP).filter(day => days.includes(DAY_MAP[day as keyof typeof DAY_MAP])) as Day[]; } - if (separator === 'none') { + if (!separator) { return days.join(''); } const listFormat = new Intl.ListFormat('en-US', { @@ -92,7 +90,7 @@ export class CourseMeeting { * @param options - Options for the string representation * @returns String representation of the time range for the course */ - getTimeString(options: TimeStringOptions): string { + getTimeString({ separator = '-' }: TimeStringOptions): string { const { startTime, endTime } = this; const startHour = Math.floor(startTime / 60); const startMinute = startTime % 60; @@ -124,7 +122,7 @@ export class CourseMeeting { endTimeString += endMinute === 0 ? ':00' : `:${endMinute}`; endTimeString += endHour >= 12 ? 'pm' : 'am'; - return `${startTimeString} ${options.separator} ${endTimeString}`; + return `${startTimeString} ${separator} ${endTimeString}`; } } @@ -153,5 +151,5 @@ type DaysStringOptions = { * * 'narrow' = `Monday Wednesday Friday` */ - separator: Intl.ListFormatStyle | 'none'; + separator?: Intl.ListFormatStyle; }; diff --git a/src/stories/components/List.stories.tsx b/src/stories/components/SortableList.stories.tsx similarity index 98% rename from src/stories/components/List.stories.tsx rename to src/stories/components/SortableList.stories.tsx index c557ce5c..9d96b618 100644 --- a/src/stories/components/List.stories.tsx +++ b/src/stories/components/SortableList.stories.tsx @@ -76,7 +76,7 @@ const exampleCourses = generateCourses(numberOfCourses); type CourseWithId = Course & BaseItem; const meta = { - title: 'Components/Common/List', + title: 'Components/Common/SortableList', component: SortableList, parameters: { layout: 'centered', diff --git a/src/views/components/PopupMain.tsx b/src/views/components/PopupMain.tsx index f7cf14e2..808f9475 100644 --- a/src/views/components/PopupMain.tsx +++ b/src/views/components/PopupMain.tsx @@ -1,15 +1,13 @@ import splashText from '@assets/insideJokes'; import createSchedule from '@pages/background/lib/createSchedule'; -import { CalendarDots, Flag, GearSix, Plus } from '@phosphor-icons/react'; +import { CalendarDots, GearSix, Plus } from '@phosphor-icons/react'; import { background } from '@shared/messages'; import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; -import { openReportWindow } from '@shared/util/openReportWindow'; import Divider from '@views/components/common/Divider'; import Text from '@views/components/common/Text/Text'; import { useEnforceScheduleLimit } from '@views/hooks/useEnforceScheduleLimit'; import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules'; -import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; import useKC_DABR_WASM from 'kc-dabr-wasm'; import React, { useEffect, useState } from 'react'; @@ -27,14 +25,14 @@ import { SortableList } from './common/SortableList'; */ export default function PopupMain(): JSX.Element { const [enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); - const [enableDataRefreshing, setEnableDataRefreshing] = useState(false); + // const [enableDataRefreshing, setEnableDataRefreshing] = useState(false); useKC_DABR_WASM(); useEffect(() => { const initAllSettings = async () => { - const { enableCourseStatusChips, enableDataRefreshing } = await initSettings(); + const { enableCourseStatusChips } = await initSettings(); setEnableCourseStatusChips(enableCourseStatusChips); - setEnableDataRefreshing(enableDataRefreshing); + // setEnableDataRefreshing(enableDataRefreshing); }; initAllSettings(); @@ -44,14 +42,14 @@ export default function PopupMain(): JSX.Element { // console.log('enableCourseStatusChips', newValue); }); - const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { - setEnableDataRefreshing(newValue); - // console.log('enableDataRefreshing', newValue); - }); + // const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { + // setEnableDataRefreshing(newValue); + // // console.log('enableDataRefreshing', newValue); + // }); return () => { OptionsStore.removeListener(l1); - OptionsStore.removeListener(l2); + // OptionsStore.removeListener(l2); }; }, []); @@ -86,7 +84,7 @@ export default function PopupMain(): JSX.Element { return (
-
+
@@ -95,13 +93,15 @@ export default function PopupMain(): JSX.Element { color='ut-burntorange' onClick={handleCalendarOpenOnClick} icon={CalendarDots} - /> + iconProps={{ weight: 'fill' }} + > + Calendar +
- +
)} -
+
{activeSchedule?.courses?.length > 0 && ( ({ @@ -157,35 +160,35 @@ export default function PopupMain(): JSX.Element { /> )}
-
-
+
+
{enableCourseStatusChips && ( <> - - - + + + )}
- {enableDataRefreshing && ( -
- + {/* {enableDataRefreshing && ( +
*/} + {/* LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} - - {/* */} -
- )} +
*/} + {/* */} + {/*
+ )} */}
); diff --git a/src/views/components/calendar/Calendar.tsx b/src/views/components/calendar/Calendar.tsx index 341d9a41..c106918b 100644 --- a/src/views/components/calendar/Calendar.tsx +++ b/src/views/components/calendar/Calendar.tsx @@ -65,7 +65,7 @@ export default function Calendar(): JSX.Element {
setCourse(block.course)} blockData={block} diff --git a/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx b/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx index 4d697d60..76d77cdd 100644 --- a/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx +++ b/src/views/components/calendar/CalendarCourseCellColorPicker/CourseCellColorPicker.tsx @@ -105,7 +105,7 @@ export default function CourseCellColorPicker({ defaultColor }: CourseCellColorP }; return ( -
+
{Array.from(colorPatchColors.keys()).map(baseColor => ( ); } diff --git a/src/views/components/common/PopupCourseBlock.tsx b/src/views/components/common/PopupCourseBlock.tsx index 803c3acc..573b5401 100644 --- a/src/views/components/common/PopupCourseBlock.tsx +++ b/src/views/components/common/PopupCourseBlock.tsx @@ -8,7 +8,7 @@ import { pickFontColor } from '@shared/util/colors'; import { StatusIcon } from '@shared/util/icons'; import Text from '@views/components/common/Text/Text'; import clsx from 'clsx'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import { Button } from './Button'; import { SortableListDragHandle } from './SortableListDragHandle'; @@ -24,6 +24,19 @@ export interface PopupCourseBlockProps { const IS_STORYBOOK = import.meta.env.STORYBOOK; +const CourseMeeting = memo( + ({ meeting, fontColor }: { meeting: Course['schedule']['meetings'][0]; fontColor: string }) => { + const dateString = meeting.getDaysString({ format: 'short' }); + return ( + + {`${dateString} ${meeting.getTimeString({ separator: '-' })}${ + meeting.location ? `, ${meeting.location.building} ${meeting.location.room}` : '' + }`} + + ); + } +); + /** * The "course block" to be used in the extension popup. * @@ -35,12 +48,18 @@ const IS_STORYBOOK = import.meta.env.STORYBOOK; */ export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element { const [enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); + const [isCopied, setIsCopied] = useState(false); const lastCopyTime = useRef(0); const ref = useRef(null); useEffect(() => { - initSettings().then(({ enableCourseStatusChips }) => setEnableCourseStatusChips(enableCourseStatusChips)); + const initAllSettings = async () => { + const { enableCourseStatusChips } = await initSettings(); + setEnableCourseStatusChips(enableCourseStatusChips); + }; + + initAllSettings(); const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => { setEnableCourseStatusChips(newValue); @@ -84,77 +103,104 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou setTimeout(() => setIsCopied(false), 500); }; + const meetings = useMemo( + () => + course.schedule.meetings.map(meeting => ( + + )), + [course.schedule.meetings, fontColor] + ); + return (
{IS_STORYBOOK ? ( - +
+ +
) : ( )} - - - {course.department} {course.number} - {course.instructors.length > 0 ? <> – : ''} - {course.instructors.map(v => v.toString({ format: 'last' })).join('; ')} - - {enableCourseStatusChips && course.status !== Status.OPEN && ( -
- -
- )} - - + {enableCourseStatusChips && course.status !== Status.OPEN && ( +
+ +
+ )} +
+ +
+
); } diff --git a/src/views/components/common/ScheduleDropdown.tsx b/src/views/components/common/ScheduleDropdown.tsx index 00273166..e4690281 100644 --- a/src/views/components/common/ScheduleDropdown.tsx +++ b/src/views/components/common/ScheduleDropdown.tsx @@ -19,7 +19,7 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop const [activeSchedule] = useSchedules(); return ( -
+
{({ open }) => ( <> diff --git a/src/views/components/common/ScheduleListItem.tsx b/src/views/components/common/ScheduleListItem.tsx index 0a2a84fb..8d9ca413 100644 --- a/src/views/components/common/ScheduleListItem.tsx +++ b/src/views/components/common/ScheduleListItem.tsx @@ -166,7 +166,7 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem as={ExtensionRootWrapper} className={clsx([ styleResetClass, - 'w-fit cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-theme-offwhite/50 focus:outline-none', + 'w-fit cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite/50 focus:outline-none', 'data-[closed]:(opacity-0 scale-95)', 'data-[enter]:(ease-out-expo duration-150)', 'data-[leave]:(ease-out duration-50)', diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index ac446efc..d045ba71 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -86,7 +86,7 @@ const useDevMode = (targetCount: number): [boolean, () => void] => { */ export default function Settings(): JSX.Element { const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); - const [_showTimeLocation, setShowTimeLocation] = useState(false); + // const [_showTimeLocation, setShowTimeLocation] = useState(false); const [highlightConflicts, setHighlightConflicts] = useState(false); const [loadAllCourses, setLoadAllCourses] = useState(false); const [_enableDataRefreshing, setEnableDataRefreshing] = useState(false); @@ -119,14 +119,13 @@ export default function Settings(): JSX.Element { const initAndSetSettings = async () => { const { enableCourseStatusChips, - enableTimeAndLocationInPopup, enableHighlightConflicts, enableScrollToLoad, enableDataRefreshing, alwaysOpenCalendarInNewTab, } = await initSettings(); setEnableCourseStatusChips(enableCourseStatusChips); - setShowTimeLocation(enableTimeAndLocationInPopup); + // setShowTimeLocation(enableTimeAndLocationInPopup); setHighlightConflicts(enableHighlightConflicts); setLoadAllCourses(enableScrollToLoad); setEnableDataRefreshing(enableDataRefreshing); @@ -150,27 +149,27 @@ export default function Settings(): JSX.Element { // console.log('enableCourseStatusChips', newValue); }); - const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => { - setShowTimeLocation(newValue); - // console.log('enableTimeAndLocationInPopup', newValue); - }); + // const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => { + // setShowTimeLocation(newValue); + // // console.log('enableTimeAndLocationInPopup', newValue); + // }); - const l3 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => { + const l2 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => { setHighlightConflicts(newValue); // console.log('enableHighlightConflicts', newValue); }); - const l4 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => { + const l3 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => { setLoadAllCourses(newValue); // console.log('enableScrollToLoad', newValue); }); - const l5 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { + const l4 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { setEnableDataRefreshing(newValue); // console.log('enableDataRefreshing', newValue); }); - const l6 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => { + const l5 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => { setCalendarNewTab(newValue); // console.log('alwaysOpenCalendarInNewTab', newValue); }); @@ -182,7 +181,6 @@ export default function Settings(): JSX.Element { OptionsStore.removeListener(l3); OptionsStore.removeListener(l4); OptionsStore.removeListener(l5); - OptionsStore.removeListener(l6); window.removeEventListener('keydown', handleKeyPress); };