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:
@@ -10,7 +10,7 @@ interface Props {
|
||||
style?: React.CSSProperties;
|
||||
variant?: 'filled' | 'outline' | 'minimal';
|
||||
size?: 'regular' | 'small' | 'mini';
|
||||
onClick?: () => void;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
icon?: Icon;
|
||||
iconProps?: IconProps;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -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 ? <> – </> : ''}
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { ArrowUpRight, CalendarDots, ChatText, Copy, FileText, Minus, Plus, Smiley, X } from '@phosphor-icons/react';
|
||||
import {
|
||||
ArrowUpRight,
|
||||
CalendarDots,
|
||||
ChatText,
|
||||
Check,
|
||||
Copy,
|
||||
FileText,
|
||||
Minus,
|
||||
Plus,
|
||||
Smiley,
|
||||
X,
|
||||
} from '@phosphor-icons/react';
|
||||
import { background } from '@shared/messages';
|
||||
import type { Course } from '@shared/types/Course';
|
||||
import type Instructor from '@shared/types/Instructor';
|
||||
@@ -9,7 +20,8 @@ import Divider from '@views/components/common/Divider';
|
||||
import Link from '@views/components/common/Link';
|
||||
import Text from '@views/components/common/Text/Text';
|
||||
import { useCalendar } from '@views/contexts/CalendarContext';
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef, useState } from 'react';
|
||||
|
||||
import DisplayMeetingInfo from './DisplayMeetingInfo';
|
||||
|
||||
@@ -46,10 +58,22 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
const formattedUniqueId = uniqueId.toString().padStart(5, '0');
|
||||
const isInCalendar = useCalendar();
|
||||
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
const lastCopyTime = useRef<number>(0);
|
||||
|
||||
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(formattedUniqueId);
|
||||
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);
|
||||
};
|
||||
|
||||
const handleOpenRateMyProf = async () => {
|
||||
@@ -107,7 +131,21 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
||||
<Text variant='h1' className='flex-1 whitespace-nowrap text-theme-black'>
|
||||
({department} {courseNumber})
|
||||
</Text>
|
||||
<Button color='ut-burntorange' variant='minimal' icon={Copy} onClick={handleCopy}>
|
||||
<Button color='ut-burntorange' variant='minimal' onClick={handleCopy}>
|
||||
<div className='relative h-5.5 w-5.5'>
|
||||
<Check
|
||||
className={clsx(
|
||||
'absolute size-full inset-0 text-ut-burntorange transition-all duration-250 ease-in-out',
|
||||
isCopied ? 'opacity-100 scale-100' : 'opacity-0 scale-75'
|
||||
)}
|
||||
/>
|
||||
<Copy
|
||||
className={clsx(
|
||||
'absolute size-full inset-0 text-ut-burntorange transition-all duration-250 ease-in-out',
|
||||
isCopied ? 'opacity-0 scale-75' : 'opacity-100 scale-100'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{formattedUniqueId}
|
||||
</Button>
|
||||
<button className='bg-transparent p-0 text-ut-black btn' onClick={onClose}>
|
||||
|
||||
Reference in New Issue
Block a user