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
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 400px;
|
width: 430px;
|
||||||
height: 540px;
|
height: 540px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,9 +7,6 @@ export interface IOptionsStore {
|
|||||||
/** whether we should enable course status chips (indicator for waitlisted, cancelled, and closed courses) */
|
/** whether we should enable course status chips (indicator for waitlisted, cancelled, and closed courses) */
|
||||||
enableCourseStatusChips: boolean;
|
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) */
|
/** whether we should automatically highlight conflicts on the course schedule page (adds a red strikethrough to courses that have conflicting times) */
|
||||||
enableHighlightConflicts: boolean;
|
enableHighlightConflicts: boolean;
|
||||||
|
|
||||||
@@ -25,10 +22,9 @@ export interface IOptionsStore {
|
|||||||
|
|
||||||
export const OptionsStore = createSyncStore<IOptionsStore>({
|
export const OptionsStore = createSyncStore<IOptionsStore>({
|
||||||
enableCourseStatusChips: false,
|
enableCourseStatusChips: false,
|
||||||
enableTimeAndLocationInPopup: false,
|
|
||||||
enableHighlightConflicts: true,
|
enableHighlightConflicts: true,
|
||||||
enableScrollToLoad: true,
|
enableScrollToLoad: true,
|
||||||
enableDataRefreshing: true,
|
enableDataRefreshing: false,
|
||||||
alwaysOpenCalendarInNewTab: false,
|
alwaysOpenCalendarInNewTab: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -40,7 +36,6 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
|
|||||||
export const initSettings = async () =>
|
export const initSettings = async () =>
|
||||||
({
|
({
|
||||||
enableCourseStatusChips: await OptionsStore.get('enableCourseStatusChips'),
|
enableCourseStatusChips: await OptionsStore.get('enableCourseStatusChips'),
|
||||||
enableTimeAndLocationInPopup: await OptionsStore.get('enableTimeAndLocationInPopup'),
|
|
||||||
enableHighlightConflicts: await OptionsStore.get('enableHighlightConflicts'),
|
enableHighlightConflicts: await OptionsStore.get('enableHighlightConflicts'),
|
||||||
enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'),
|
enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'),
|
||||||
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
|
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
|
||||||
|
|||||||
@@ -53,9 +53,8 @@ export class CourseMeeting {
|
|||||||
* @param meeting - The meeting to check for conflicts with
|
* @param meeting - The meeting to check for conflicts with
|
||||||
* @returns True if the given meeting conflicts with this meeting, false otherwise
|
* @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, startTime, endTime } = this;
|
||||||
const { days: otherDays, startTime: otherStartTime, endTime: otherEndTime } = meeting;
|
|
||||||
|
|
||||||
const hasDayConflict = days.some(day => otherDays.includes(day));
|
const hasDayConflict = days.some(day => otherDays.includes(day));
|
||||||
const hasTimeConflict = startTime < otherEndTime && endTime > otherStartTime;
|
const hasTimeConflict = startTime < otherEndTime && endTime > otherStartTime;
|
||||||
@@ -69,14 +68,13 @@ export class CourseMeeting {
|
|||||||
* @param options - Options for the string representation
|
* @param options - Options for the string representation
|
||||||
* @returns String representation of the days of the week that this meeting is taught
|
* @returns String representation of the days of the week that this meeting is taught
|
||||||
*/
|
*/
|
||||||
getDaysString(options: DaysStringOptions): string {
|
getDaysString({ format, separator }: DaysStringOptions): string {
|
||||||
let { format, separator } = options;
|
|
||||||
let { days } = this;
|
let { days } = this;
|
||||||
|
|
||||||
if (format === 'short') {
|
if (format === 'short') {
|
||||||
days = Object.keys(DAY_MAP).filter(day => days.includes(DAY_MAP[day as keyof typeof DAY_MAP])) as Day[];
|
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('');
|
return days.join('');
|
||||||
}
|
}
|
||||||
const listFormat = new Intl.ListFormat('en-US', {
|
const listFormat = new Intl.ListFormat('en-US', {
|
||||||
@@ -92,7 +90,7 @@ export class CourseMeeting {
|
|||||||
* @param options - Options for the string representation
|
* @param options - Options for the string representation
|
||||||
* @returns String representation of the time range for the course
|
* @returns String representation of the time range for the course
|
||||||
*/
|
*/
|
||||||
getTimeString(options: TimeStringOptions): string {
|
getTimeString({ separator = '-' }: TimeStringOptions): string {
|
||||||
const { startTime, endTime } = this;
|
const { startTime, endTime } = this;
|
||||||
const startHour = Math.floor(startTime / 60);
|
const startHour = Math.floor(startTime / 60);
|
||||||
const startMinute = startTime % 60;
|
const startMinute = startTime % 60;
|
||||||
@@ -124,7 +122,7 @@ export class CourseMeeting {
|
|||||||
endTimeString += endMinute === 0 ? ':00' : `:${endMinute}`;
|
endTimeString += endMinute === 0 ? ':00' : `:${endMinute}`;
|
||||||
endTimeString += endHour >= 12 ? 'pm' : 'am';
|
endTimeString += endHour >= 12 ? 'pm' : 'am';
|
||||||
|
|
||||||
return `${startTimeString} ${options.separator} ${endTimeString}`;
|
return `${startTimeString} ${separator} ${endTimeString}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,5 +151,5 @@ type DaysStringOptions = {
|
|||||||
*
|
*
|
||||||
* 'narrow' = `Monday Wednesday Friday`
|
* 'narrow' = `Monday Wednesday Friday`
|
||||||
*/
|
*/
|
||||||
separator: Intl.ListFormatStyle | 'none';
|
separator?: Intl.ListFormatStyle;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const exampleCourses = generateCourses(numberOfCourses);
|
|||||||
type CourseWithId = Course & BaseItem;
|
type CourseWithId = Course & BaseItem;
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Components/Common/List',
|
title: 'Components/Common/SortableList',
|
||||||
component: SortableList,
|
component: SortableList,
|
||||||
parameters: {
|
parameters: {
|
||||||
layout: 'centered',
|
layout: 'centered',
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
import splashText from '@assets/insideJokes';
|
import splashText from '@assets/insideJokes';
|
||||||
import createSchedule from '@pages/background/lib/createSchedule';
|
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 { background } from '@shared/messages';
|
||||||
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
|
||||||
import { openReportWindow } from '@shared/util/openReportWindow';
|
|
||||||
import Divider from '@views/components/common/Divider';
|
import Divider from '@views/components/common/Divider';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import { useEnforceScheduleLimit } from '@views/hooks/useEnforceScheduleLimit';
|
import { useEnforceScheduleLimit } from '@views/hooks/useEnforceScheduleLimit';
|
||||||
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
|
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
|
||||||
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString';
|
|
||||||
import useKC_DABR_WASM from 'kc-dabr-wasm';
|
import useKC_DABR_WASM from 'kc-dabr-wasm';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -27,14 +25,14 @@ import { SortableList } from './common/SortableList';
|
|||||||
*/
|
*/
|
||||||
export default function PopupMain(): JSX.Element {
|
export default function PopupMain(): JSX.Element {
|
||||||
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
||||||
const [enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
// const [enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
||||||
useKC_DABR_WASM();
|
useKC_DABR_WASM();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initAllSettings = async () => {
|
const initAllSettings = async () => {
|
||||||
const { enableCourseStatusChips, enableDataRefreshing } = await initSettings();
|
const { enableCourseStatusChips } = await initSettings();
|
||||||
setEnableCourseStatusChips(enableCourseStatusChips);
|
setEnableCourseStatusChips(enableCourseStatusChips);
|
||||||
setEnableDataRefreshing(enableDataRefreshing);
|
// setEnableDataRefreshing(enableDataRefreshing);
|
||||||
};
|
};
|
||||||
|
|
||||||
initAllSettings();
|
initAllSettings();
|
||||||
@@ -44,14 +42,14 @@ export default function PopupMain(): JSX.Element {
|
|||||||
// console.log('enableCourseStatusChips', newValue);
|
// console.log('enableCourseStatusChips', newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
|
// const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
|
||||||
setEnableDataRefreshing(newValue);
|
// setEnableDataRefreshing(newValue);
|
||||||
// console.log('enableDataRefreshing', newValue);
|
// // console.log('enableDataRefreshing', newValue);
|
||||||
});
|
// });
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
OptionsStore.removeListener(l1);
|
OptionsStore.removeListener(l1);
|
||||||
OptionsStore.removeListener(l2);
|
// OptionsStore.removeListener(l2);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -86,7 +84,7 @@ export default function PopupMain(): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-screen max-h-full flex flex-col bg-white'>
|
<div className='h-screen max-h-full flex flex-col bg-white'>
|
||||||
<div className='p-5 py-3.5'>
|
<div className='px-spacing-6 py-spacing-5'>
|
||||||
<div className='flex items-center justify-between bg-white'>
|
<div className='flex items-center justify-between bg-white'>
|
||||||
<SmallLogo />
|
<SmallLogo />
|
||||||
<div className='flex items-center gap-2.5'>
|
<div className='flex items-center gap-2.5'>
|
||||||
@@ -95,13 +93,15 @@ export default function PopupMain(): JSX.Element {
|
|||||||
color='ut-burntorange'
|
color='ut-burntorange'
|
||||||
onClick={handleCalendarOpenOnClick}
|
onClick={handleCalendarOpenOnClick}
|
||||||
icon={CalendarDots}
|
icon={CalendarDots}
|
||||||
/>
|
iconProps={{ weight: 'fill' }}
|
||||||
|
>
|
||||||
|
Calendar
|
||||||
|
</Button>
|
||||||
<Button variant='minimal' color='ut-black' onClick={handleOpenOptions} icon={GearSix} />
|
<Button variant='minimal' color='ut-black' onClick={handleOpenOptions} icon={GearSix} />
|
||||||
<Button variant='minimal' color='ut-black' onClick={openReportWindow} icon={Flag} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Divider orientation='horizontal' size='100%' />
|
<Divider className='bg-ut-offwhite/50' orientation='horizontal' size='100%' />
|
||||||
<div className='px-5 pb-2.5 pt-3.75'>
|
<div className='px-5 pb-2.5 pt-3.75'>
|
||||||
<ScheduleDropdown>
|
<ScheduleDropdown>
|
||||||
<SortableList
|
<SortableList
|
||||||
@@ -139,7 +139,10 @@ export default function PopupMain(): JSX.Element {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className='flex-1 self-stretch overflow-y-auto px-5'>
|
<div
|
||||||
|
style={{ scrollbarGutter: 'stable' }}
|
||||||
|
className='flex-1 self-stretch overflow-y-scroll pl-spacing-6 pr-[6px]'
|
||||||
|
>
|
||||||
{activeSchedule?.courses?.length > 0 && (
|
{activeSchedule?.courses?.length > 0 && (
|
||||||
<SortableList
|
<SortableList
|
||||||
draggables={activeSchedule.courses.map(course => ({
|
draggables={activeSchedule.courses.map(course => ({
|
||||||
@@ -157,21 +160,21 @@ export default function PopupMain(): JSX.Element {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full flex flex-col items-center gap-1.25 p-5 pt-3.75'>
|
<div className='w-full flex flex-col items-center gap-1.25 px-spacing-6 py-spacing-5'>
|
||||||
<div className='flex gap-2.5'>
|
<div className='flex gap-spacing-6'>
|
||||||
{enableCourseStatusChips && (
|
{enableCourseStatusChips && (
|
||||||
<>
|
<>
|
||||||
<CourseStatus status='WAITLISTED' size='mini' />
|
<CourseStatus status='WAITLISTED' size='small' />
|
||||||
<CourseStatus status='CLOSED' size='mini' />
|
<CourseStatus status='CLOSED' size='small' />
|
||||||
<CourseStatus status='CANCELLED' size='mini' />
|
<CourseStatus status='CANCELLED' size='small' />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{enableDataRefreshing && (
|
{/* {enableDataRefreshing && (
|
||||||
<div className='inline-flex items-center self-center gap-1'>
|
<div className='inline-flex items-center self-center gap-1'> */}
|
||||||
<Text variant='mini' className='text-ut-gray !font-normal'>
|
{/* <Text variant='mini' className='text-ut-gray !font-normal'>
|
||||||
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
|
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
|
||||||
</Text>
|
</Text> */}
|
||||||
{/* <button
|
{/* <button
|
||||||
className='h-4 w-4 bg-transparent p-0 btn'
|
className='h-4 w-4 bg-transparent p-0 btn'
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -184,8 +187,8 @@ export default function PopupMain(): JSX.Element {
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</button> */}
|
</button> */}
|
||||||
</div>
|
{/* </div>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default function Calendar(): JSX.Element {
|
|||||||
<div className='h-screen flex overflow-auto'>
|
<div className='h-screen flex overflow-auto'>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'py-spacing-6 relative h-full min-h-screen w-full flex flex-none flex-col justify-between overflow-clip whitespace-nowrap border-r border-theme-offwhite/50 shadow-[2px_0_10px,rgba(214_210_196_/_.1)] motion-safe:duration-300 motion-safe:ease-out-expo motion-safe:transition-[max-width] screenshot:hidden',
|
'py-spacing-6 relative h-full min-h-screen w-full flex flex-none flex-col justify-between overflow-clip whitespace-nowrap border-r border-ut-offwhite/50 shadow-[2px_0_10px,rgba(214_210_196_/_.1)] motion-safe:duration-300 motion-safe:ease-out-expo motion-safe:transition-[max-width] screenshot:hidden',
|
||||||
{
|
{
|
||||||
'max-w-[20.3125rem] ': showSidebar,
|
'max-w-[20.3125rem] ': showSidebar,
|
||||||
'max-w-0 pointer-events-none': !showSidebar,
|
'max-w-0 pointer-events-none': !showSidebar,
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ export default function CalendarBottomBar({ courseCells, setCourse }: CalendarBo
|
|||||||
const { courseDeptAndInstr, status, className } = block.componentProps;
|
const { courseDeptAndInstr, status, className } = block.componentProps;
|
||||||
return (
|
return (
|
||||||
<CalendarCourseBlock
|
<CalendarCourseBlock
|
||||||
|
key={block.course.uniqueId}
|
||||||
courseDeptAndInstr={courseDeptAndInstr}
|
courseDeptAndInstr={courseDeptAndInstr}
|
||||||
status={status}
|
status={status}
|
||||||
key={courseDeptAndInstr}
|
|
||||||
className={clsx(className, 'w-35! h-12.5! items-center')}
|
className={clsx(className, 'w-35! h-12.5! items-center')}
|
||||||
onClick={() => setCourse(block.course)}
|
onClick={() => setCourse(block.course)}
|
||||||
blockData={block}
|
blockData={block}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default function CourseCellColorPicker({ defaultColor }: CourseCellColorP
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='inline-flex flex-col border border-1 border-ut-offwhite rounded-1 bg-white p-1.25'>
|
<div className='inline-flex flex-col border border-ut-offwhite rounded-1 bg-white p-1.25'>
|
||||||
<div className='grid grid-cols-6 gap-1'>
|
<div className='grid grid-cols-6 gap-1'>
|
||||||
{Array.from(colorPatchColors.keys()).map(baseColor => (
|
{Array.from(colorPatchColors.keys()).map(baseColor => (
|
||||||
<ColorPatch
|
<ColorPatch
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
|
|||||||
className={clsx([
|
className={clsx([
|
||||||
styleResetClass,
|
styleResetClass,
|
||||||
'mt-spacing-3',
|
'mt-spacing-3',
|
||||||
'min-w-max cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-theme-offwhite1 focus:outline-none',
|
'min-w-max 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-[closed]:(opacity-0 scale-95)',
|
||||||
'data-[enter]:(ease-out-expo duration-150)',
|
'data-[enter]:(ease-out-expo duration-150)',
|
||||||
'data-[leave]:(ease-out duration-50)',
|
'data-[leave]:(ease-out duration-50)',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export default function Divider({ className, testId, size, orientation }: Divide
|
|||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
className={clsx('border-solid border-theme-offwhite/50 w-0 h-0', className)}
|
className={clsx('border-solid border-ut-offwhite/50 w-0 h-0', className)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { pickFontColor } from '@shared/util/colors';
|
|||||||
import { StatusIcon } from '@shared/util/icons';
|
import { StatusIcon } from '@shared/util/icons';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import clsx from 'clsx';
|
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 { Button } from './Button';
|
||||||
import { SortableListDragHandle } from './SortableListDragHandle';
|
import { SortableListDragHandle } from './SortableListDragHandle';
|
||||||
@@ -24,6 +24,19 @@ export interface PopupCourseBlockProps {
|
|||||||
|
|
||||||
const IS_STORYBOOK = import.meta.env.STORYBOOK;
|
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 (
|
||||||
|
<Text key={dateString} className={clsx('flex-1 truncate select-none', fontColor)} variant='h3-course'>
|
||||||
|
{`${dateString} ${meeting.getTimeString({ separator: '-' })}${
|
||||||
|
meeting.location ? `, ${meeting.location.building} ${meeting.location.room}` : ''
|
||||||
|
}`}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The "course block" to be used in the extension popup.
|
* 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 {
|
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element {
|
||||||
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
||||||
|
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
const lastCopyTime = useRef<number>(0);
|
const lastCopyTime = useRef<number>(0);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initSettings().then(({ enableCourseStatusChips }) => setEnableCourseStatusChips(enableCourseStatusChips));
|
const initAllSettings = async () => {
|
||||||
|
const { enableCourseStatusChips } = await initSettings();
|
||||||
|
setEnableCourseStatusChips(enableCourseStatusChips);
|
||||||
|
};
|
||||||
|
|
||||||
|
initAllSettings();
|
||||||
|
|
||||||
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
|
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
|
||||||
setEnableCourseStatusChips(newValue);
|
setEnableCourseStatusChips(newValue);
|
||||||
@@ -84,39 +103,64 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
|
|||||||
setTimeout(() => setIsCopied(false), 500);
|
setTimeout(() => setIsCopied(false), 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const meetings = useMemo(
|
||||||
|
() =>
|
||||||
|
course.schedule.meetings.map(meeting => (
|
||||||
|
<CourseMeeting
|
||||||
|
key={meeting.getDaysString({ format: 'short' })}
|
||||||
|
meeting={meeting}
|
||||||
|
fontColor={fontColor}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
[course.schedule.meetings, fontColor]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.primaryColor,
|
backgroundColor: colors.primaryColor,
|
||||||
}}
|
}}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'h-full w-full inline-flex items-center justify-center gap-1 rounded pr-2 focusable cursor-pointer text-left hover:shadow-md ease-out group-[.is-dragging]:shadow-md',
|
'w-full inline-flex items-center justify-center gap-1 rounded focusable cursor-pointer text-left hover:shadow-md ease-out group-[.is-dragging]:shadow-md min-h-[55px]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
{IS_STORYBOOK ? (
|
{IS_STORYBOOK ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.secondaryColor,
|
||||||
|
}}
|
||||||
|
className='flex cursor-move items-center self-stretch rounded rounded-r-0 px-spacing-2'
|
||||||
|
>
|
||||||
<DotsSixVertical weight='bold' className='h-6 w-6 cursor-move text-white' />
|
<DotsSixVertical weight='bold' className='h-6 w-6 cursor-move text-white' />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SortableListDragHandle
|
<SortableListDragHandle
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.secondaryColor,
|
backgroundColor: colors.secondaryColor,
|
||||||
}}
|
}}
|
||||||
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
|
className='flex cursor-move items-center self-stretch rounded rounded-r-0 px-spacing-2'
|
||||||
>
|
>
|
||||||
<DotsSixVertical weight='bold' className='h-6 w-6 cursor-move text-white' />
|
<DotsSixVertical weight='bold' className='h-6 w-6 cursor-move text-white' />
|
||||||
</SortableListDragHandle>
|
</SortableListDragHandle>
|
||||||
)}
|
)}
|
||||||
|
<div className='h-full flex flex-1 justify-center gap-spacing-3 p-spacing-3'>
|
||||||
|
<div className='flex flex-1 flex-col justify-center gap-spacing-1'>
|
||||||
<Text
|
<Text
|
||||||
className={clsx('flex-1 py-spacing-5 truncate ml-spacing-3 select-none', fontColor)}
|
className={clsx(
|
||||||
|
`truncate select-none justify-center ${meetings.length > 0 ? 'mb-auto' : 'my-auto'}`,
|
||||||
|
fontColor
|
||||||
|
)}
|
||||||
variant='h1-course'
|
variant='h1-course'
|
||||||
>
|
>
|
||||||
{course.department} {course.number}
|
{course.department} {course.number}
|
||||||
{course.instructors.length > 0 ? <> – </> : ''}
|
{course.instructors.length > 0 ? <> – </> : ''}
|
||||||
{course.instructors.map(v => v.toString({ format: 'last' })).join('; ')}
|
{course.instructors.map(v => v.toString({ format: 'last' })).join('; ')}
|
||||||
</Text>
|
</Text>
|
||||||
|
<div className='flex flex-col'>{meetings}</div>
|
||||||
|
</div>
|
||||||
{enableCourseStatusChips && course.status !== Status.OPEN && (
|
{enableCourseStatusChips && course.status !== Status.OPEN && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -127,16 +171,16 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
|
|||||||
<StatusIcon status={course.status} className='h-6 w-6' />
|
<StatusIcon status={course.status} className='h-6 w-6' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className='flex flex-col justify-center'>
|
||||||
<Button
|
<Button
|
||||||
color='ut-gray'
|
color='ut-gray'
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className='h-full max-h-[30px] w-fit gap-spacing-2 rounded py-spacing-2 text-white px-spacing-3!'
|
className='h-full max-h-[30px] max-w-fit gap-spacing-2 rounded text-white px-spacing-3! py-spacing-2!'
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.secondaryColor,
|
backgroundColor: colors.secondaryColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className='relative h-5.5 w-5.5'>
|
<div className='relative h-[21px] w-[21px]'>
|
||||||
<Check
|
<Check
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out',
|
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out',
|
||||||
@@ -146,15 +190,17 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
|
|||||||
<Copy
|
<Copy
|
||||||
weight='fill'
|
weight='fill'
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out',
|
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out select-none',
|
||||||
isCopied ? 'opacity-0 scale-75' : 'opacity-100 scale-100'
|
isCopied ? 'opacity-0 scale-75' : 'opacity-100 scale-100'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Text variant='h2' className='no-select text-base!'>
|
<Text variant='h2' className='select-none text-base!'>
|
||||||
{formattedUniqueId}
|
{formattedUniqueId}
|
||||||
</Text>
|
</Text>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
|
|||||||
const [activeSchedule] = useSchedules();
|
const [activeSchedule] = useSchedules();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='border border-ut-offwhite rounded border-solid bg-white'>
|
<div className='border border-ut-offwhite/50 rounded bg-white'>
|
||||||
<Disclosure defaultOpen={defaultOpen}>
|
<Disclosure defaultOpen={defaultOpen}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
|
|||||||
as={ExtensionRootWrapper}
|
as={ExtensionRootWrapper}
|
||||||
className={clsx([
|
className={clsx([
|
||||||
styleResetClass,
|
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-[closed]:(opacity-0 scale-95)',
|
||||||
'data-[enter]:(ease-out-expo duration-150)',
|
'data-[enter]:(ease-out-expo duration-150)',
|
||||||
'data-[leave]:(ease-out duration-50)',
|
'data-[leave]:(ease-out duration-50)',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const useDevMode = (targetCount: number): [boolean, () => void] => {
|
|||||||
*/
|
*/
|
||||||
export default function Settings(): JSX.Element {
|
export default function Settings(): JSX.Element {
|
||||||
const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
||||||
const [_showTimeLocation, setShowTimeLocation] = useState<boolean>(false);
|
// const [_showTimeLocation, setShowTimeLocation] = useState<boolean>(false);
|
||||||
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(false);
|
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(false);
|
||||||
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
|
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
|
||||||
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
const [_enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
|
||||||
@@ -119,14 +119,13 @@ export default function Settings(): JSX.Element {
|
|||||||
const initAndSetSettings = async () => {
|
const initAndSetSettings = async () => {
|
||||||
const {
|
const {
|
||||||
enableCourseStatusChips,
|
enableCourseStatusChips,
|
||||||
enableTimeAndLocationInPopup,
|
|
||||||
enableHighlightConflicts,
|
enableHighlightConflicts,
|
||||||
enableScrollToLoad,
|
enableScrollToLoad,
|
||||||
enableDataRefreshing,
|
enableDataRefreshing,
|
||||||
alwaysOpenCalendarInNewTab,
|
alwaysOpenCalendarInNewTab,
|
||||||
} = await initSettings();
|
} = await initSettings();
|
||||||
setEnableCourseStatusChips(enableCourseStatusChips);
|
setEnableCourseStatusChips(enableCourseStatusChips);
|
||||||
setShowTimeLocation(enableTimeAndLocationInPopup);
|
// setShowTimeLocation(enableTimeAndLocationInPopup);
|
||||||
setHighlightConflicts(enableHighlightConflicts);
|
setHighlightConflicts(enableHighlightConflicts);
|
||||||
setLoadAllCourses(enableScrollToLoad);
|
setLoadAllCourses(enableScrollToLoad);
|
||||||
setEnableDataRefreshing(enableDataRefreshing);
|
setEnableDataRefreshing(enableDataRefreshing);
|
||||||
@@ -150,27 +149,27 @@ export default function Settings(): JSX.Element {
|
|||||||
// console.log('enableCourseStatusChips', newValue);
|
// console.log('enableCourseStatusChips', newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => {
|
// const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => {
|
||||||
setShowTimeLocation(newValue);
|
// setShowTimeLocation(newValue);
|
||||||
// console.log('enableTimeAndLocationInPopup', newValue);
|
// // console.log('enableTimeAndLocationInPopup', newValue);
|
||||||
});
|
// });
|
||||||
|
|
||||||
const l3 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
|
const l2 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => {
|
||||||
setHighlightConflicts(newValue);
|
setHighlightConflicts(newValue);
|
||||||
// console.log('enableHighlightConflicts', newValue);
|
// console.log('enableHighlightConflicts', newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const l4 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
|
const l3 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => {
|
||||||
setLoadAllCourses(newValue);
|
setLoadAllCourses(newValue);
|
||||||
// console.log('enableScrollToLoad', newValue);
|
// console.log('enableScrollToLoad', newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const l5 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
|
const l4 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
|
||||||
setEnableDataRefreshing(newValue);
|
setEnableDataRefreshing(newValue);
|
||||||
// console.log('enableDataRefreshing', newValue);
|
// console.log('enableDataRefreshing', newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const l6 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
|
const l5 = OptionsStore.listen('alwaysOpenCalendarInNewTab', async ({ newValue }) => {
|
||||||
setCalendarNewTab(newValue);
|
setCalendarNewTab(newValue);
|
||||||
// console.log('alwaysOpenCalendarInNewTab', newValue);
|
// console.log('alwaysOpenCalendarInNewTab', newValue);
|
||||||
});
|
});
|
||||||
@@ -182,7 +181,6 @@ export default function Settings(): JSX.Element {
|
|||||||
OptionsStore.removeListener(l3);
|
OptionsStore.removeListener(l3);
|
||||||
OptionsStore.removeListener(l4);
|
OptionsStore.removeListener(l4);
|
||||||
OptionsStore.removeListener(l5);
|
OptionsStore.removeListener(l5);
|
||||||
OptionsStore.removeListener(l6);
|
|
||||||
|
|
||||||
window.removeEventListener('keydown', handleKeyPress);
|
window.removeEventListener('keydown', handleKeyPress);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user