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;
|
style?: React.CSSProperties;
|
||||||
variant?: 'filled' | 'outline' | 'minimal';
|
variant?: 'filled' | 'outline' | 'minimal';
|
||||||
size?: 'regular' | 'small' | 'mini';
|
size?: 'regular' | 'small' | 'mini';
|
||||||
onClick?: () => void;
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
icon?: Icon;
|
icon?: Icon;
|
||||||
iconProps?: IconProps;
|
iconProps?: IconProps;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
|
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 { background } from '@shared/messages';
|
||||||
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
|
||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
@@ -11,6 +11,8 @@ 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, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Props for PopupCourseBlock
|
* Props for PopupCourseBlock
|
||||||
*/
|
*/
|
||||||
@@ -37,6 +39,8 @@ export default function PopupCourseBlock({
|
|||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
}: PopupCourseBlockProps): JSX.Element {
|
}: PopupCourseBlockProps): JSX.Element {
|
||||||
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
|
||||||
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
|
const lastCopyTime = useRef<number>(0);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -72,13 +76,26 @@ export default function PopupCourseBlock({
|
|||||||
window.close();
|
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 (
|
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-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
|
className
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
@@ -93,8 +110,8 @@ export default function PopupCourseBlock({
|
|||||||
>
|
>
|
||||||
<DotsSixVertical weight='bold' className='h-6 w-6 text-white' />
|
<DotsSixVertical weight='bold' className='h-6 w-6 text-white' />
|
||||||
</div>
|
</div>
|
||||||
<Text className={clsx('flex-1 py-3.5 truncate', fontColor)} variant='h1-course'>
|
<Text className={clsx('flex-1 py-spacing-5 truncate ml-spacing-3', fontColor)} variant='h1-course'>
|
||||||
<span className='px-0.5 font-450'>{formattedUniqueId}</span> {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>
|
||||||
@@ -103,11 +120,39 @@ export default function PopupCourseBlock({
|
|||||||
style={{
|
style={{
|
||||||
backgroundColor: colors.secondaryColor,
|
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>
|
</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>
|
</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 { background } from '@shared/messages';
|
||||||
import type { Course } from '@shared/types/Course';
|
import type { Course } from '@shared/types/Course';
|
||||||
import type Instructor from '@shared/types/Instructor';
|
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 Link from '@views/components/common/Link';
|
||||||
import Text from '@views/components/common/Text/Text';
|
import Text from '@views/components/common/Text/Text';
|
||||||
import { useCalendar } from '@views/contexts/CalendarContext';
|
import { useCalendar } from '@views/contexts/CalendarContext';
|
||||||
import React from 'react';
|
import clsx from 'clsx';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
|
||||||
import DisplayMeetingInfo from './DisplayMeetingInfo';
|
import DisplayMeetingInfo from './DisplayMeetingInfo';
|
||||||
|
|
||||||
@@ -46,10 +58,22 @@ export default function HeadingAndActions({ course, activeSchedule, onClose }: H
|
|||||||
const formattedUniqueId = uniqueId.toString().padStart(5, '0');
|
const formattedUniqueId = uniqueId.toString().padStart(5, '0');
|
||||||
const isInCalendar = useCalendar();
|
const isInCalendar = useCalendar();
|
||||||
|
|
||||||
|
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||||
|
const lastCopyTime = useRef<number>(0);
|
||||||
|
|
||||||
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
const getInstructorFullName = (instructor: Instructor) => instructor.toString({ format: 'first_last' });
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
navigator.clipboard.writeText(formattedUniqueId);
|
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 () => {
|
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'>
|
<Text variant='h1' className='flex-1 whitespace-nowrap text-theme-black'>
|
||||||
({department} {courseNumber})
|
({department} {courseNumber})
|
||||||
</Text>
|
</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}
|
{formattedUniqueId}
|
||||||
</Button>
|
</Button>
|
||||||
<button className='bg-transparent p-0 text-ut-black btn' onClick={onClose}>
|
<button className='bg-transparent p-0 text-ut-black btn' onClick={onClose}>
|
||||||
|
|||||||
Reference in New Issue
Block a user