Skip to content

Commit 78ba4a7

Browse files
committed
feat: create CodeBlock component with syntax highlighting
1 parent 501b9be commit 78ba4a7

File tree

1 file changed

+135
-0
lines changed

1 file changed

+135
-0
lines changed

src/components/CodeBlock.jsx

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import React, { useState, memo, useCallback } from 'react';
2+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
3+
import { oneDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
4+
import { useTheme } from '../contexts/ThemeContext';
5+
6+
/**
7+
* Enhanced code block component with syntax highlighting and copy functionality
8+
*/
9+
const CodeBlock = memo(({
10+
language = '',
11+
children,
12+
inline = false,
13+
showLineNumbers = false,
14+
className = ''
15+
}) => {
16+
const [copied, setCopied] = useState(false);
17+
const { isDarkMode } = useTheme();
18+
19+
const code = String(children).replace(/\n$/, '');
20+
21+
const handleCopy = useCallback(async () => {
22+
try {
23+
await navigator.clipboard.writeText(code);
24+
setCopied(true);
25+
setTimeout(() => setCopied(false), 2000);
26+
} catch (err) {
27+
console.error('Failed to copy code:', err);
28+
// Fallback for older browsers
29+
try {
30+
const textArea = document.createElement('textarea');
31+
textArea.value = code;
32+
textArea.style.position = 'fixed';
33+
textArea.style.left = '-999999px';
34+
document.body.appendChild(textArea);
35+
textArea.focus();
36+
textArea.select();
37+
document.execCommand('copy');
38+
document.body.removeChild(textArea);
39+
setCopied(true);
40+
setTimeout(() => setCopied(false), 2000);
41+
} catch (fallbackErr) {
42+
console.error('Fallback copy also failed:', fallbackErr);
43+
}
44+
}
45+
}, [code]);
46+
47+
// Inline code rendering
48+
if (inline) {
49+
return (
50+
<code className={`px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-blue-600 dark:text-blue-400 font-mono text-sm ${className}`}>
51+
{children}
52+
</code>
53+
);
54+
}
55+
56+
// Full code block rendering - use a span wrapper to avoid div in p issue
57+
const codeBlock = (
58+
<div className="relative group my-4">
59+
<span className="absolute right-2 top-2 z-10">
60+
<button
61+
onClick={handleCopy}
62+
className="opacity-0 group-hover:opacity-100 transition-opacity duration-200 px-2 py-1 text-xs bg-gray-700 dark:bg-gray-600 text-white rounded hover:bg-gray-600 dark:hover:bg-gray-500 flex items-center gap-1"
63+
title="Copy code"
64+
>
65+
{copied ? (
66+
<>
67+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
68+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
69+
</svg>
70+
Copied!
71+
</>
72+
) : (
73+
<>
74+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
76+
</svg>
77+
Copy
78+
</>
79+
)}
80+
</button>
81+
</span>
82+
83+
{language && (
84+
<span className="absolute left-3 top-2 text-xs text-gray-400 dark:text-gray-500 font-mono block">
85+
{language}
86+
</span>
87+
)}
88+
89+
<SyntaxHighlighter
90+
language={language || 'text'}
91+
style={isDarkMode ? oneDark : oneLight}
92+
showLineNumbers={showLineNumbers}
93+
PreTag="pre"
94+
customStyle={{
95+
margin: 0,
96+
borderRadius: '0.5rem',
97+
fontSize: '0.875rem',
98+
padding: language ? '2.5rem 1rem 1rem' : '1rem',
99+
}}
100+
codeTagProps={{
101+
style: {
102+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace',
103+
}
104+
}}
105+
>
106+
{code}
107+
</SyntaxHighlighter>
108+
</div>
109+
);
110+
111+
return codeBlock;
112+
});
113+
114+
CodeBlock.displayName = 'CodeBlock';
115+
116+
/**
117+
* Wrapper component for use with ReactMarkdown
118+
*/
119+
export const MarkdownCodeBlock = ({ node, inline, className, children, ...props }) => {
120+
const match = /language-(\w+)/.exec(className || '');
121+
const language = match ? match[1] : '';
122+
123+
return (
124+
<CodeBlock
125+
language={language}
126+
inline={inline}
127+
className={className}
128+
{...props}
129+
>
130+
{children}
131+
</CodeBlock>
132+
);
133+
};
134+
135+
export default CodeBlock;

0 commit comments

Comments
 (0)