From 2baa8420af8bc9e2429e7a214a417c940c3df94b Mon Sep 17 00:00:00 2001 From: javatcoding1 Date: Thu, 16 Oct 2025 23:40:51 +0530 Subject: [PATCH 1/2] fix(copy-code): improve error handling and button feedback --- src/utils/MarkdownRenderer.tsx | 51 ++++++-------------- src/utils/copy-code.ts | 86 ++++++++++++++++++++-------------- 2 files changed, 64 insertions(+), 73 deletions(-) diff --git a/src/utils/MarkdownRenderer.tsx b/src/utils/MarkdownRenderer.tsx index 7c267019..8d38a9e4 100644 --- a/src/utils/MarkdownRenderer.tsx +++ b/src/utils/MarkdownRenderer.tsx @@ -9,6 +9,7 @@ import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; import { h } from 'hastscript'; +import { initCodeCopy } from '@/utils/copy-code'; // Type definitions interface MarkdownRendererProps { @@ -226,35 +227,7 @@ const MarkdownRenderer: React.FC = ({ } }, []); - const handleCopyClick = useCallback(async (e: MouseEvent) => { - const button = (e.target as HTMLElement).closest( - '.copy-code-btn', - ) as HTMLButtonElement | null; - if (!button) return; - - const code = button.getAttribute('data-code') || ''; - - try { - await navigator.clipboard.writeText(code); - const successMessage = button.nextElementSibling as HTMLElement; - if (successMessage) { - successMessage.classList.remove('hidden'); - successMessage.classList.add('flex'); - setTimeout(() => { - successMessage.classList.add('hidden'); - successMessage.classList.remove('flex'); - }, 2000); - } - } catch { - const textArea = document.createElement('textarea'); - textArea.value = code; - textArea.style.cssText = 'position:fixed;opacity:0;'; - document.body.appendChild(textArea); - textArea.select(); - document.execCommand('copy'); - document.body.removeChild(textArea); - } - }, []); + const handleAnchorClick = useCallback((e: MouseEvent) => { const target = e.target as HTMLElement; @@ -281,13 +254,11 @@ const MarkdownRenderer: React.FC = ({ }, []); useEffect(() => { - document.addEventListener('click', handleCopyClick); document.addEventListener('click', handleAnchorClick); return () => { - document.removeEventListener('click', handleCopyClick); document.removeEventListener('click', handleAnchorClick); }; - }, [handleCopyClick, handleAnchorClick]); + }, [handleAnchorClick]); useEffect(() => { if (setZoomableImages) { @@ -296,6 +267,15 @@ const MarkdownRenderer: React.FC = ({ } }, [processedContent, setZoomableImages]); + // Initialize copy functionality when content changes + useEffect(() => { + const timeoutId = setTimeout(() => { + initCodeCopy(); + }, 100); // Small delay to ensure DOM is updated + + return () => clearTimeout(timeoutId); + }, [processedContent]); + useEffect(() => { scrollToAnchor(); const handlePopState = () => { @@ -403,9 +383,9 @@ const MarkdownRenderer: React.FC = ({ {language} -
+
-
- Copied! -
diff --git a/src/utils/copy-code.ts b/src/utils/copy-code.ts index a2733e87..b30e0c22 100644 --- a/src/utils/copy-code.ts +++ b/src/utils/copy-code.ts @@ -1,10 +1,10 @@ /** - * Copy code functionality for code blocks - * Simplified and optimized version + * Copy code functionality for code blocks with user-friendly feedback */ /** * Handle click on copy button with unified clipboard handling + * @param event - The click event from the copy button */ function handleCopyClick(event: Event): void { event.preventDefault(); @@ -13,21 +13,15 @@ function handleCopyClick(event: Event): void { const button = event.currentTarget as HTMLElement; const codeContent = button.getAttribute('data-code'); - if (!codeContent) { - console.error('No code content found to copy'); + if (!codeContent || codeContent.trim() === '') { + showErrorMessage(button); return; } copyToClipboard(codeContent, button); } -/** - * Unified clipboard copy with fallback - */ -async function copyToClipboard( - text: string, - button: HTMLElement, -): Promise { +async function copyToClipboard(text: string, button: HTMLElement): Promise { try { await navigator.clipboard.writeText(text); showSuccessMessage(button); @@ -35,33 +29,60 @@ async function copyToClipboard( // Fallback for older browsers const textarea = document.createElement('textarea'); textarea.value = text; - textarea.style.cssText = - 'position:fixed;left:-999999px;top:-999999px;opacity:0;'; - + textarea.style.cssText = 'position:fixed;left:-999999px;top:-999999px;opacity:0;'; document.body.appendChild(textarea); textarea.select(); - const success = document.execCommand('copy'); document.body.removeChild(textarea); - - if (success) showSuccessMessage(button); + + if (success) { + showSuccessMessage(button); + } else { + showErrorMessage(button); + } } } -/** - * Show success message - */ function showSuccessMessage(button: HTMLElement): void { - const successMessage = button.nextElementSibling as HTMLElement; - if (successMessage?.classList.contains('copy-success-message')) { - successMessage.classList.remove('hidden'); - setTimeout(() => successMessage.classList.add('hidden'), 2000); - } + const originalContent = button.innerHTML; + const originalClasses = button.className; + + button.className = 'copy-code-btn bg-green-100 text-green-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-green-200'; + button.innerHTML = ` + + + + Copied! + `; + button.setAttribute('disabled', 'true'); + + setTimeout(() => { + button.className = originalClasses; + button.innerHTML = originalContent; + button.removeAttribute('disabled'); + }, 2000); +} + +function showErrorMessage(button: HTMLElement): void { + const originalContent = button.innerHTML; + const originalClasses = button.className; + + button.className = 'copy-code-btn bg-red-100 text-red-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-red-200'; + button.innerHTML = ` + + + + Failed! + `; + button.setAttribute('disabled', 'true'); + + setTimeout(() => { + button.className = originalClasses; + button.innerHTML = originalContent; + button.removeAttribute('disabled'); + }, 2500); } -/** - * Initialize copy code functionality - */ export function initCodeCopy(): void { document.querySelectorAll('.copy-code-btn').forEach((button) => { if (button instanceof HTMLElement) { @@ -71,18 +92,11 @@ export function initCodeCopy(): void { }); } -// Auto-initialize when available +// Auto-initialize if (typeof window !== 'undefined') { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCodeCopy); } else { initCodeCopy(); } - - // Re-initialize for dynamic content - let timeoutId: number; - document.addEventListener('click', () => { - clearTimeout(timeoutId); - timeoutId = window.setTimeout(initCodeCopy, 100); - }); } From 09750e6267ed0588484d4c227d8b374f0ea5f933 Mon Sep 17 00:00:00 2001 From: javatcoding1 Date: Thu, 16 Oct 2025 23:56:54 +0530 Subject: [PATCH 2/2] fix(copy-code): replace console errors with visual feedback --- src/utils/MarkdownRenderer.tsx | 2 -- src/utils/copy-code.ts | 41 ++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/utils/MarkdownRenderer.tsx b/src/utils/MarkdownRenderer.tsx index 8d38a9e4..b62e29cd 100644 --- a/src/utils/MarkdownRenderer.tsx +++ b/src/utils/MarkdownRenderer.tsx @@ -227,8 +227,6 @@ const MarkdownRenderer: React.FC = ({ } }, []); - - const handleAnchorClick = useCallback((e: MouseEvent) => { const target = e.target as HTMLElement; diff --git a/src/utils/copy-code.ts b/src/utils/copy-code.ts index b30e0c22..d97b8f18 100644 --- a/src/utils/copy-code.ts +++ b/src/utils/copy-code.ts @@ -21,7 +21,15 @@ function handleCopyClick(event: Event): void { copyToClipboard(codeContent, button); } -async function copyToClipboard(text: string, button: HTMLElement): Promise { +/** + * Copy text to clipboard with fallback for older browsers + * @param text - The text content to copy to clipboard + * @param button - The copy button element for showing feedback + */ +async function copyToClipboard( + text: string, + button: HTMLElement, +): Promise { try { await navigator.clipboard.writeText(text); showSuccessMessage(button); @@ -29,12 +37,13 @@ async function copyToClipboard(text: string, button: HTMLElement): Promise // Fallback for older browsers const textarea = document.createElement('textarea'); textarea.value = text; - textarea.style.cssText = 'position:fixed;left:-999999px;top:-999999px;opacity:0;'; + textarea.style.cssText = + 'position:fixed;left:-999999px;top:-999999px;opacity:0;'; document.body.appendChild(textarea); textarea.select(); const success = document.execCommand('copy'); document.body.removeChild(textarea); - + if (success) { showSuccessMessage(button); } else { @@ -43,11 +52,16 @@ async function copyToClipboard(text: string, button: HTMLElement): Promise } } +/** + * Show success feedback by transforming button to green "Copied!" state + * @param button - The copy button element to show success feedback in + */ function showSuccessMessage(button: HTMLElement): void { const originalContent = button.innerHTML; const originalClasses = button.className; - - button.className = 'copy-code-btn bg-green-100 text-green-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-green-200'; + + button.className = + 'copy-code-btn bg-green-100 text-green-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-green-200'; button.innerHTML = ` @@ -55,7 +69,7 @@ function showSuccessMessage(button: HTMLElement): void { Copied! `; button.setAttribute('disabled', 'true'); - + setTimeout(() => { button.className = originalClasses; button.innerHTML = originalContent; @@ -63,11 +77,16 @@ function showSuccessMessage(button: HTMLElement): void { }, 2000); } +/** + * Show error feedback by transforming button to red "Failed!" state + * @param button - The copy button element to show error feedback in + */ function showErrorMessage(button: HTMLElement): void { const originalContent = button.innerHTML; const originalClasses = button.className; - - button.className = 'copy-code-btn bg-red-100 text-red-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-red-200'; + + button.className = + 'copy-code-btn bg-red-100 text-red-800 text-xs px-4 py-2 rounded-lg transition-all duration-200 flex items-center space-x-2 shadow-sm border border-red-200'; button.innerHTML = ` @@ -75,7 +94,7 @@ function showErrorMessage(button: HTMLElement): void { Failed! `; button.setAttribute('disabled', 'true'); - + setTimeout(() => { button.className = originalClasses; button.innerHTML = originalContent; @@ -83,6 +102,10 @@ function showErrorMessage(button: HTMLElement): void { }, 2500); } +/** + * Initialize copy functionality for all copy buttons on the page + * Attaches click event handlers to elements with 'copy-code-btn' class + */ export function initCodeCopy(): void { document.querySelectorAll('.copy-code-btn').forEach((button) => { if (button instanceof HTMLElement) {