diff --git a/src/components/BadgeSection.tsx b/src/components/BadgeSection.tsx
index 5eaea5bb4..51d618a8d 100644
--- a/src/components/BadgeSection.tsx
+++ b/src/components/BadgeSection.tsx
@@ -2,6 +2,7 @@
import React, { useState, useEffect, useRef } from "react";
import Image from "next/image";
+import { CopyButton } from "@/components/ui/CopyButton";
interface BadgeSectionProps {
username: string;
@@ -137,30 +138,19 @@ function Toast({ visible }: { visible: boolean }) {
* Copyable code block component
*/
function CopyableCodeBlock({ code, onCopySuccess }: { code: string; onCopySuccess?: () => void }) {
- const [copied, setCopied] = useState(false);
-
- const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(code);
- setCopied(true);
- onCopySuccess?.();
- setTimeout(() => setCopied(false), 2000);
- } catch (err) {
- console.error("Failed to copy:", err);
- }
- };
-
return (
-
+
{code}
-
+
);
}
diff --git a/src/components/CopyLinkButton.tsx b/src/components/CopyLinkButton.tsx
index bf92a0e75..b400fb8ee 100644
--- a/src/components/CopyLinkButton.tsx
+++ b/src/components/CopyLinkButton.tsx
@@ -1,56 +1,20 @@
"use client";
-import { useState } from "react";
-import { toast } from "sonner";
+import { CopyButton } from "@/components/ui/CopyButton";
interface CopyLinkButtonProps {
url: string;
+ className?: string;
}
-export default function CopyLinkButton({ url }: CopyLinkButtonProps) {
- const [copied, setCopied] = useState(false);
-
- const handleCopy = async () => {
- // Fallback strategy for older browsers
- if (!navigator.clipboard) {
- const textArea = document.createElement("textarea");
- textArea.value = url;
- document.body.appendChild(textArea);
- textArea.select();
- try {
- document.execCommand("copy");
- triggerSuccess();
- } catch (err) {
- toast.error("Failed to copy link.");
- }
- document.body.removeChild(textArea);
- return;
- }
-
- // Modern browser copying execution
- try {
- await navigator.clipboard.writeText(url);
- triggerSuccess();
- } catch (err) {
- toast.error("Failed to copy link.");
- }
- };
-
- const triggerSuccess = () => {
- setCopied(true);
- toast.success("Link copied!", { duration: 2000 });
- setTimeout(() => setCopied(false), 2000);
- };
-
+export default function CopyLinkButton({ url, className }: CopyLinkButtonProps) {
return (
-
+
);
}
\ No newline at end of file
diff --git a/src/components/career-intelligence/ExportPanel.tsx b/src/components/career-intelligence/ExportPanel.tsx
index cd31d4bf7..7f1f2c30e 100644
--- a/src/components/career-intelligence/ExportPanel.tsx
+++ b/src/components/career-intelligence/ExportPanel.tsx
@@ -4,6 +4,7 @@ import React, { useState } from "react";
import { FileText, FileCode, Braces, Copy, Check, Download } from "lucide-react";
import { cn } from "@/lib/utils";
import type { ResumeContent, ExportFormat } from "@/types/cv-types";
+import { CopyButton } from "@/components/ui/CopyButton";
interface ExportPanelProps {
content: ResumeContent;
@@ -138,23 +139,28 @@ ${skillText}
-
+ />
);
diff --git a/src/components/ui/CopyButton.tsx b/src/components/ui/CopyButton.tsx
new file mode 100644
index 000000000..1d354a3c7
--- /dev/null
+++ b/src/components/ui/CopyButton.tsx
@@ -0,0 +1,122 @@
+"use client";
+
+import * as React from "react";
+import { useState } from "react";
+import { Copy, Check } from "lucide-react";
+import { toast } from "sonner";
+import { cn } from "@/lib/utils";
+
+export interface CopyButtonProps extends React.ButtonHTMLAttributes {
+ value: string;
+ copyLabel?: string;
+ copiedLabel?: string;
+ toastMessage?: string;
+ showToast?: boolean;
+ showText?: boolean;
+ iconOnly?: boolean;
+ duration?: number;
+ onCopySuccess?: () => void;
+}
+
+export const CopyButton = React.forwardRef(
+ (
+ {
+ value,
+ copyLabel = "Copy",
+ copiedLabel = "Copied!",
+ toastMessage,
+ showToast = true,
+ showText = true,
+ iconOnly = false,
+ duration = 2500,
+ onCopySuccess,
+ className,
+ children,
+ onClick,
+ ...props
+ },
+ ref
+ ) => {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async (e: React.MouseEvent) => {
+ if (onClick) onClick(e);
+ if (!value) return;
+
+ let success = false;
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ try {
+ await navigator.clipboard.writeText(value);
+ success = true;
+ } catch (err) {
+ console.error("Clipboard write error:", err);
+ }
+ }
+
+ if (!success) {
+ try {
+ const textArea = document.createElement("textarea");
+ textArea.value = value;
+ textArea.style.position = "fixed";
+ textArea.style.opacity = "0";
+ document.body.appendChild(textArea);
+ textArea.select();
+ success = document.execCommand("copy");
+ document.body.removeChild(textArea);
+ } catch (err) {
+ console.error("ExecCommand copy error:", err);
+ }
+ }
+
+ if (success) {
+ setCopied(true);
+ if (showToast) {
+ toast.success(toastMessage || "Copied to clipboard!", { duration });
+ }
+ if (onCopySuccess) {
+ onCopySuccess();
+ }
+ setTimeout(() => {
+ setCopied(false);
+ }, duration);
+ } else {
+ toast.error("Failed to copy to clipboard.");
+ }
+ };
+
+ return (
+
+ );
+ }
+);
+
+CopyButton.displayName = "CopyButton";