feat(ui): course unique number copy button (#490)

* feat: add copy icon for course unique ID

* feat: update Button component to support event parameter in onClick handler

* feat: add copy functionality for course unique ID

* refactor: use Text component instead of span

* fix: remove duplicate course number

* fix: remove unnecessary event forwarding

* fix: remove unnecessary boolean type

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>

* fix: remove double space

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>

* refactor: reduce clipboard copy delay and use formatted unique ID

* feat: add copy animation to dialog

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
Co-authored-by: nikshitak <nikkikurva@gmail.com>
This commit is contained in:
Ethan L
2025-01-21 00:02:00 -06:00
committed by GitHub
parent 009de62828
commit 501f506677
3 changed files with 95 additions and 12 deletions

View File

@@ -1,5 +1,5 @@
import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { DotsSixVertical } from '@phosphor-icons/react';
import { Check, Copy, DotsSixVertical } from '@phosphor-icons/react';
import { background } from '@shared/messages';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import type { Course } from '@shared/types/Course';
@@ -11,6 +11,8 @@ import Text from '@views/components/common/Text/Text';
import clsx from 'clsx';
import React, { useEffect, useRef, useState } from 'react';
import { Button } from './Button';
/**
* Props for PopupCourseBlock
*/
@@ -37,6 +39,8 @@ export default function PopupCourseBlock({
dragHandleProps,
}: 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(() => {
@@ -72,13 +76,26 @@ export default function PopupCourseBlock({
window.close();
};
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
const now = Date.now();
if (now - lastCopyTime.current < 500) {
return;
}
lastCopyTime.current = now;
await navigator.clipboard.writeText(formattedUniqueId);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 500);
};
return (
<div
style={{
backgroundColor: colors.primaryColor,
}}
className={clsx(
'h-full w-full inline-flex items-center justify-center gap-1 rounded pr-3 focusable cursor-pointer text-left hover:shadow-md ease-out group-[.is-dragging]:shadow-md',
'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',
className
)}
onClick={handleClick}
@@ -93,8 +110,8 @@ export default function PopupCourseBlock({
>
<DotsSixVertical weight='bold' className='h-6 w-6 text-white' />
</div>
<Text className={clsx('flex-1 py-3.5 truncate', fontColor)} variant='h1-course'>
<span className='px-0.5 font-450'>{formattedUniqueId}</span> {course.department} {course.number}
<Text className={clsx('flex-1 py-spacing-5 truncate ml-spacing-3', fontColor)} variant='h1-course'>
{course.department} {course.number}
{course.instructors.length > 0 ? <> &ndash; </> : ''}
{course.instructors.map(v => v.toString({ format: 'last' })).join('; ')}
</Text>
@@ -103,11 +120,39 @@ export default function PopupCourseBlock({
style={{
backgroundColor: colors.secondaryColor,
}}
className='ml-1 flex items-center justify-center justify-self-end rounded p-1px text-white'
className='ml-1 flex items-center justify-center justify-self-end rounded p-[3px] text-white'
>
<StatusIcon status={course.status} className='h-5 w-5' />
<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
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',
isCopied ? 'opacity-0 scale-75' : 'opacity-100 scale-100'
)}
/>
</div>
<Text variant='h2' className='text-base!'>
{formattedUniqueId}
</Text>
</Button>
</div>
);
}