diff --git a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/menu.tsx b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/menu.tsx index 8b14e5b9dd..0746b02ce5 100644 --- a/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/menu.tsx +++ b/apps/dashboard/app/(app)/ratelimits/[namespaceId]/logs/menu.tsx @@ -40,11 +40,17 @@ export const Menu: React.FC = ({ namespace, identifier }) => { { - copyToClipboard(identifier).then(() => - toast.success("Copied to clipboard", { - description: identifier, - }), - ); + copyToClipboard(identifier) + .then(() => + toast.success("Identifier copied to clipboard", { + description: identifier, + }), + ) + .catch((err) => + toast.error("Failed to copy to clipboard", { + description: (err as Error).message, + }), + ); }} > diff --git a/internal/ui/src/hooks/use-copy-to-clipboard.tsx b/internal/ui/src/hooks/use-copy-to-clipboard.tsx index 55c29d55b4..a033a5571f 100644 --- a/internal/ui/src/hooks/use-copy-to-clipboard.tsx +++ b/internal/ui/src/hooks/use-copy-to-clipboard.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useRef, useState } from "react"; +const DEFAULT_TIMEOUT = 3000; + export const useCopyToClipboard = ( - timeout = 3000, + timeout = DEFAULT_TIMEOUT, ): [boolean, (value: string | ClipboardItem) => Promise] => { const timer = useRef | null>(null); const [copied, setCopied] = useState(false); @@ -13,23 +15,40 @@ export const useCopyToClipboard = ( } }; + const writeToClipboard = async (value: string | ClipboardItem) => { + const isClipboardAvailable = + typeof navigator !== "undefined" && navigator.clipboard !== undefined; + + if (!isClipboardAvailable) { + throw new Error("Clipboard API is not supported in this browser"); + } + + if (typeof value === "string") { + await navigator.clipboard.writeText(value); + } else if (value instanceof ClipboardItem) { + await navigator.clipboard.write([value]); + } + }; + + const handleTimeout = () => { + if (Number.isFinite(timeout) && timeout >= 0) { + timer.current = setTimeout(() => setCopied(false), timeout); + } else { + console.warn(`Invalid timeout value; defaulting to ${DEFAULT_TIMEOUT}ms`); + timer.current = setTimeout(() => setCopied(false), DEFAULT_TIMEOUT); + } + }; + const copyToClipboard = useCallback( async (value: string | ClipboardItem) => { clearTimer(); try { - if (typeof value === "string") { - await navigator.clipboard.writeText(value); - } else if (value instanceof ClipboardItem) { - await navigator.clipboard.write([value]); - } + await writeToClipboard(value); setCopied(true); - - // Ensure timeout is a non-negative finite number - if (Number.isFinite(timeout) && timeout >= 0) { - timer.current = setTimeout(() => setCopied(false), timeout); - } + handleTimeout(); } catch (error) { - console.error("Failed to copy: ", error); + console.warn("Failed to copy to clipboard. ", error); + throw error; // Propagate error for higher-level handling } }, [timeout],