aboutsummaryrefslogtreecommitdiff
path: root/src/components/CodeSnippet.tsx
blob: 73ac1e7722377b9376b304885c91d2047d556551 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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;