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:
Preston Cook
2025-02-13 18:07:05 -06:00
committed by GitHub
parent b171f01d01
commit ee4c6ce699
14 changed files with 163 additions and 123 deletions

View File

@@ -41,7 +41,7 @@ export default function Divider({ className, testId, size, orientation }: Divide
<div
style={style}
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)}
/>
);
}

View File

@@ -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 (
<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.
*
@@ -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<boolean>(false);
const [isCopied, setIsCopied] = useState<boolean>(false);
const lastCopyTime = useRef<number>(0);
const ref = useRef<HTMLDivElement>(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 => (
<CourseMeeting
key={meeting.getDaysString({ format: 'short' })}
meeting={meeting}
fontColor={fontColor}
/>
)),
[course.schedule.meetings, fontColor]
);
return (
<div
style={{
backgroundColor: colors.primaryColor,
}}
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
)}
onClick={handleClick}
ref={ref}
>
{IS_STORYBOOK ? (
<DotsSixVertical weight='bold' className='h-6 w-6 cursor-move text-white' />
<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' />
</div>
) : (
<SortableListDragHandle
style={{
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' />
</SortableListDragHandle>
)}
<Text
className={clsx('flex-1 py-spacing-5 truncate ml-spacing-3 select-none', fontColor)}
variant='h1-course'
>
{course.department} {course.number}
{course.instructors.length > 0 ? <> &ndash; </> : ''}
{course.instructors.map(v => v.toString({ format: 'last' })).join('; ')}
</Text>
{enableCourseStatusChips && course.status !== Status.OPEN && (
<div
style={{
backgroundColor: colors.secondaryColor,
}}
className='ml-1 flex items-center justify-center justify-self-end rounded p-[3px] text-white'
>
<StatusIcon status={course.status} className='h-6 w-6' />
</div>
)}
<Button
color='ut-gray'
onClick={handleCopy}
className='h-full max-h-[30px] w-fit gap-spacing-2 rounded py-spacing-2 text-white px-spacing-3!'
style={{
backgroundColor: colors.secondaryColor,
}}
>
<div className='relative h-5.5 w-5.5'>
<Check
<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
className={clsx(
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out',
isCopied ? 'opacity-100 scale-100' : 'opacity-0 scale-75'
`truncate select-none justify-center ${meetings.length > 0 ? 'mb-auto' : 'my-auto'}`,
fontColor
)}
/>
<Copy
weight='fill'
className={clsx(
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out',
isCopied ? 'opacity-0 scale-75' : 'opacity-100 scale-100'
)}
/>
variant='h1-course'
>
{course.department} {course.number}
{course.instructors.length > 0 ? <> &ndash; </> : ''}
{course.instructors.map(v => v.toString({ format: 'last' })).join('; ')}
</Text>
<div className='flex flex-col'>{meetings}</div>
</div>
<Text variant='h2' className='no-select text-base!'>
{formattedUniqueId}
</Text>
</Button>
{enableCourseStatusChips && course.status !== Status.OPEN && (
<div
style={{
backgroundColor: colors.secondaryColor,
}}
className='ml-1 flex items-center justify-center justify-self-end rounded p-[3px] text-white'
>
<StatusIcon status={course.status} className='h-6 w-6' />
</div>
)}
<div className='flex flex-col justify-center'>
<Button
color='ut-gray'
onClick={handleCopy}
className='h-full max-h-[30px] max-w-fit gap-spacing-2 rounded text-white px-spacing-3! py-spacing-2!'
style={{
backgroundColor: colors.secondaryColor,
}}
>
<div className='relative h-[21px] w-[21px]'>
<Check
className={clsx(
'absolute size-full inset-0 text-white transition-all duration-250 ease-in-out',
isCopied ? 'opacity-100 scale-100' : 'opacity-0 scale-75'
)}
/>
<Copy
weight='fill'
className={clsx(
'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'
)}
/>
</div>
<Text variant='h2' className='select-none text-base!'>
{formattedUniqueId}
</Text>
</Button>
</div>
</div>
</div>
);
}

View File

@@ -19,7 +19,7 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
const [activeSchedule] = useSchedules();
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}>
{({ open }) => (
<>

View File

@@ -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)',