Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 10 additions & 20 deletions src/components/BadgeSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<div className="flex items-center justify-between rounded-lg bg-[var(--control)] p-3 border border-[var(--border)]">
<div className="flex items-center justify-between rounded-lg bg-[var(--control)] p-3 border border-[var(--border)] gap-2">
<code className="flex-1 text-xs text-[var(--card-foreground)] overflow-auto scrollbar-thin">
{code}
</code>
<button
onClick={handleCopy}
className="ml-2 shrink-0 px-2 py-1 text-xs font-medium rounded bg-[var(--accent)] text-[var(--accent-foreground)] hover:opacity-90 transition-opacity"
>
{copied ? "✓ Copied!" : "Copy"}
</button>
<CopyButton
value={code}
copyLabel="Copy"
copiedLabel="Copied!"
showToast={false}
onCopySuccess={onCopySuccess}
className="shrink-0 bg-[var(--accent)] text-[var(--accent-foreground)] border-none hover:opacity-90 transition-opacity"
/>
</div>
);
}
56 changes: 10 additions & 46 deletions src/components/CopyLinkButton.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<button
onClick={handleCopy}
type="button"
aria-label="Copy profile link"
title="Copy profile link"
className="inline-flex items-center gap-1 px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 focus-visible:ring-2 focus-visible:ring-indigo-500 transition-colors dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-700"
>
<span>{copied ? "Copied!" : "Copy link"}</span>
</button>
<CopyButton
value={url}
copyLabel="Copy link"
copiedLabel="Link Copied!"
toastMessage="Profile link copied to clipboard!"
className={className}
/>
);
}
38 changes: 22 additions & 16 deletions src/components/career-intelligence/ExportPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -138,23 +139,28 @@ ${skillText}
</div>

<div className="flex justify-center">
<button
type="button"
onClick={copyToClipboard}
<CopyButton
value={`
# Resume: ${content.role}

## Professional Summary
${content.professionalSummary}

## Experience Highlights
${content.bulletPoints.map((bp) => `- ${bp.text}`).join("\n")}

## Projects
${content.projectDescriptions.map((p) => `### ${p.name}\n${p.description}\n${p.highlights.map((h) => `- ${h}`).join("\n")}`).join("\n\n")}

## Skills Summary
${content.skillSummary}
${content.skills.map((c) => `**${c.category}**: ${c.skills.join(", ")}`).join("\n")}
`.trim()}
copyLabel="Copy Full Resume Text"
copiedLabel="Copied to Clipboard!"
toastMessage="Resume text copied to clipboard!"
className="inline-flex items-center gap-2 whitespace-nowrap rounded-md text-sm font-semibold transition-colors border border-[var(--border)] bg-[var(--card)] hover:bg-[var(--card-muted)] text-[var(--foreground)] h-9 px-5 py-2"
>
{copied ? (
<>
<Check className="h-4 w-4 text-emerald-500" />
Copied to Clipboard!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy Full Resume Text
</>
)}
</button>
/>
</div>
</div>
);
Expand Down
122 changes: 122 additions & 0 deletions src/components/ui/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement> {
value: string;
copyLabel?: string;
copiedLabel?: string;
toastMessage?: string;
showToast?: boolean;
showText?: boolean;
iconOnly?: boolean;
duration?: number;
onCopySuccess?: () => void;
}

export const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
(
{
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<HTMLButtonElement>) => {
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 (
<button
ref={ref}
type="button"
onClick={handleCopy}
className={cn(
"inline-flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 active:scale-95 disabled:opacity-50 disabled:pointer-events-none select-none",
copied
? "bg-emerald-500/10 text-emerald-600 border border-emerald-500/30 dark:bg-emerald-500/20 dark:text-emerald-400 dark:border-emerald-500/40 shadow-sm"
: "bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-200 dark:border-gray-600 dark:hover:bg-gray-700 shadow-sm",
className
)}
title={copied ? copiedLabel : copyLabel}
aria-label={copied ? copiedLabel : copyLabel}
{...props}
>
<span className="relative flex items-center justify-center w-3.5 h-3.5 flex-shrink-0">
{copied ? (
<Check className="w-3.5 h-3.5 text-emerald-600 dark:text-emerald-400 animate-in zoom-in-50 duration-200" />
) : (
<Copy className="w-3.5 h-3.5 text-current transition-transform duration-150 group-hover:scale-110" />
)}
</span>
{children ? (
children
) : !iconOnly && showText ? (
<span className={cn("transition-opacity duration-150", copied && "font-semibold")}>
{copied ? copiedLabel : copyLabel}
</span>
) : null}
</button>
);
}
);

CopyButton.displayName = "CopyButton";
Loading