From 5e06625966e61312ff416a909c2d003cb416bfd4 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Mon, 13 Oct 2025 17:00:43 +0200 Subject: [PATCH] feat: add copy to clipboard functionality for MAC address in network settings --- ui/package-lock.json | 4 +- ui/src/components/useCopyToClipBoard.tsx | 49 +++++++++++++++++++ .../routes/devices.$id.settings.network.tsx | 36 +++++++++----- 3 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 ui/src/components/useCopyToClipBoard.tsx diff --git a/ui/package-lock.json b/ui/package-lock.json index 9f948f2c..fb6903d6 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -28,6 +28,7 @@ "react": "^19.1.1", "react-animate-height": "^3.2.3", "react-dom": "^19.1.1", + "react-hook-form": "^7.62.0", "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-router": "^7.9.3", @@ -5856,8 +5857,6 @@ "react": "^19.1.1" } }, -<<<<<<< Updated upstream -======= "node_modules/react-hook-form": { "version": "7.62.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", @@ -5874,7 +5873,6 @@ "react": "^16.8.0 || ^17 || ^18 || ^19" } }, ->>>>>>> Stashed changes "node_modules/react-hot-toast": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", diff --git a/ui/src/components/useCopyToClipBoard.tsx b/ui/src/components/useCopyToClipBoard.tsx new file mode 100644 index 00000000..39041bb9 --- /dev/null +++ b/ui/src/components/useCopyToClipBoard.tsx @@ -0,0 +1,49 @@ +import { useCallback, useState } from "react"; + +export function useCopyToClipboard(resetInterval = 2000) { + const [isCopied, setIsCopied] = useState(false); + + const copy = useCallback(async (text: string) => { + if (!text) return false; + + let success = false; + + if (navigator.clipboard && window.isSecureContext) { + try { + await navigator.clipboard.writeText(text); + success = true; + } catch (err) { + console.warn("Clipboard API failed:", err); + } + } + + // Fallback for insecure contexts + if (!success) { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + + try { + success = document.execCommand("copy"); + } catch (err) { + console.error("Fallback copy failed:", err); + success = false; + } finally { + document.body.removeChild(textarea); + } + } + + setIsCopied(success); + if (success && resetInterval > 0) { + setTimeout(() => setIsCopied(false), resetInterval); + } + + return success; + }, [resetInterval]); + + return { copy, isCopied }; +} diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 17fe79ac..d36bd9dd 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -2,7 +2,7 @@ import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import { useCallback, useEffect, useRef, useState } from "react"; import { FieldValues, FormProvider, useForm } from "react-hook-form"; -import { LuEthernetPort } from "react-icons/lu"; +import { LuCopy, LuEthernetPort } from "react-icons/lu"; import validator from "validator"; import { ConfirmDialog } from "@/components/ConfirmDialog"; @@ -24,6 +24,7 @@ import StaticIpv4Card from "../components/StaticIpv4Card"; import StaticIpv6Card from "../components/StaticIpv6Card"; import { useJsonRpc } from "../hooks/useJsonRpc"; import { SettingsItem } from "../components/SettingsItem"; +import { useCopyToClipboard } from "../components/useCopyToClipBoard"; dayjs.extend(relativeTime); @@ -225,6 +226,8 @@ export default function SettingsNetworkRoute() { }); }; + const { copy } = useCopyToClipboard(); + return ( <> @@ -248,19 +251,26 @@ export default function SettingsNetworkRoute() { } />
- - + - +
+ +
+ {networkState?.mac_address} {" "} +
+
+
+