aboutsummaryrefslogtreecommitdiff
path: root/src/components/CodeSnippet.tsx
diff options
context:
space:
mode:
authorschererleander <leander@schererleander.de>2025-05-30 01:01:17 +0200
committerschererleander <leander@schererleander.de>2025-05-30 01:01:17 +0200
commitafdc982863b6cca573f1db58e1795aa8c45fabca (patch)
tree6b94d2ffdcb0e1b5ccbaf584c825763ab72ab99d /src/components/CodeSnippet.tsx
parent8f2c8393510dfefc22871661b0ef9964569e290b (diff)
rewrite site
Diffstat (limited to 'src/components/CodeSnippet.tsx')
-rw-r--r--src/components/CodeSnippet.tsx94
1 files changed, 94 insertions, 0 deletions
diff --git a/src/components/CodeSnippet.tsx b/src/components/CodeSnippet.tsx
new file mode 100644
index 0000000..73ac1e7
--- /dev/null
+++ b/src/components/CodeSnippet.tsx
@@ -0,0 +1,94 @@
+import React, { useState, useMemo, useCallback } from 'react';
+
+interface CodeSnippetProps {
+ code: string;
+ initialLines?: number;
+}
+
+const ExpandIcon = () => (
+ <svg width="16" height="16" viewBox="0 0 24 24">
+ <path d="M19 13H13v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor" />
+ </svg>
+);
+
+const CollapseIcon = () => (
+ <svg width="16" height="16" viewBox="0 0 24 24">
+ <path d="M19 13H5v-2h14v2z" fill="currentColor" />
+ </svg>
+);
+
+const CopyIcon = () => (
+ <svg width="16" height="16" viewBox="0 0 24 24">
+ <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z" fill="currentColor" />
+ </svg>
+);
+
+export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
+ ({ code, initialLines = 5 }) => {
+ const [expanded, setExpanded] = useState(false);
+ const [copied, setCopied] = useState(false);
+
+ const lines = useMemo(() => code.split('\n'), [code]);
+ const shouldTruncate = useMemo(
+ () => lines.length > initialLines,
+ [lines.length, initialLines]
+ );
+ const displayedLines = useMemo(
+ () => (expanded || !shouldTruncate ? lines : lines.slice(0, initialLines)),
+ [expanded, shouldTruncate, lines, initialLines]
+ );
+
+ const toggleExpanded = useCallback(
+ () => setExpanded(prev => !prev),
+ []
+ );
+
+ const handleCopy = useCallback(() => {
+ navigator.clipboard.writeText(code)
+ .then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ })
+ .catch(() => {});
+ }, [code]);
+
+ return (
+ <div className="relative rounded-lg p-4 my-2 font-mono bg-gray-200 dark:bg-white/5">
+ {shouldTruncate && (
+ <div className="absolute top-2 right-2 flex space-x-2">
+ <button
+ onClick={handleCopy}
+ aria-label={copied ? 'Copied!' : 'Copy code'}
+ className="p-0 cursor-pointer text-gray-600 hover:text-gray-800"
+ >
+ <CopyIcon />
+ </button>
+ <button
+ onClick={toggleExpanded}
+ aria-label={expanded ? 'Collapse code' : 'Expand code'}
+ className="p-0 cursor-pointer text-gray-600 hover:text-gray-800"
+ >
+ {expanded ? <CollapseIcon /> : <ExpandIcon />}
+ </button>
+ </div>
+ )}
+
+ <pre className="overflow-x-auto m-0 text-sm">
+ {displayedLines.map((line, idx) => (
+ <div key={idx} className="flex">
+ <span className="text-gray-500 w-8 text-right pr-2">
+ {idx + 1}
+ </span>
+ <code className="flex-1 whitespace-pre-wrap">{line}</code>
+ </div>
+ ))}
+ {shouldTruncate && !expanded && (
+ <div className="text-center text-gray-400">...</div>
+ )}
+ </pre>
+ </div>
+ );
+ }
+);
+
+export default CodeSnippet; \ No newline at end of file