feat: implement clipboard utility for copying text and update components to use it

This commit is contained in:
Anders Böttcher
2026-05-13 17:19:43 +02:00
parent f7c52d0b1c
commit ffbbf1b6eb
3 changed files with 41 additions and 16 deletions
+5 -4
View File
@@ -2,6 +2,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { LINKS } from "@/lib/links"; import { LINKS } from "@/lib/links";
import { copyText } from "@/lib/clipboard";
export interface TerminalLinks { export interface TerminalLinks {
dockerImage?: string; dockerImage?: string;
@@ -254,10 +255,10 @@ export default function TerminalTyper({ mode = "docker-run", links = {}, casaosM
.join("\n"); .join("\n");
} }
navigator.clipboard.writeText(textToCopy).then(() => { copyText(textToCopy).then(() => {
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
}); }).catch(() => {});
}; };
const copyCasaOs = () => { const copyCasaOs = () => {
@@ -265,10 +266,10 @@ export default function TerminalTyper({ mode = "docker-run", links = {}, casaosM
const text = CASAOS_LINES.filter(line => line.type === "output") const text = CASAOS_LINES.filter(line => line.type === "output")
.map(line => line.text) .map(line => line.text)
.join("\n"); .join("\n");
navigator.clipboard.writeText(text).then(() => { copyText(text).then(() => {
setCopiedCasaOs(true); setCopiedCasaOs(true);
setTimeout(() => setCopiedCasaOs(false), 2000); setTimeout(() => setCopiedCasaOs(false), 2000);
}); }).catch(() => {});
}; };
useEffect(() => { useEffect(() => {
+3 -12
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { copyText } from "@/lib/clipboard";
export default function CopyLinkButton({ token }: { token: string }) { export default function CopyLinkButton({ token }: { token: string }) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -8,21 +9,11 @@ export default function CopyLinkButton({ token }: { token: string }) {
async function copy() { async function copy() {
const url = `${window.location.origin}/files/${token}`; const url = `${window.location.origin}/files/${token}`;
try { try {
await navigator.clipboard.writeText(url); await copyText(url);
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2500); setTimeout(() => setCopied(false), 2500);
} catch { } catch {
// Fallback for browsers that deny clipboard access // copy completely unavailable (e.g. sandboxed iframe) — do nothing
const el = document.createElement("textarea");
el.value = url;
el.style.position = "fixed";
el.style.opacity = "0";
document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
setCopied(true);
setTimeout(() => setCopied(false), 2500);
} }
} }
+33
View File
@@ -0,0 +1,33 @@
/**
* Copies text to the clipboard.
*
* Tries the modern Clipboard API first (requires secure context / HTTPS).
* Falls back to the legacy textarea + execCommand technique, which works on
* plain HTTP and private-IP origins where navigator.clipboard is unavailable.
*/
export async function copyText(text: string): Promise<void> {
// Modern path — secure context (HTTPS or localhost)
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text)
return
} catch {
// Fall through to legacy method
}
}
// Legacy fallback — works on HTTP / private IPs
const el = document.createElement('textarea')
el.value = text
// Keep it out of the viewport so it doesn't flash
el.style.cssText = 'position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;pointer-events:none;'
document.body.appendChild(el)
el.focus()
el.select()
try {
// eslint-disable-next-line @typescript-eslint/no-deprecated
document.execCommand('copy')
} finally {
document.body.removeChild(el)
}
}