| | 'use client' |
| |
|
| | import { FC, memo } from 'react' |
| | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter' |
| | import { coldarkDark } from 'react-syntax-highlighter/dist/cjs/styles/prism' |
| |
|
| | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' |
| | import { IconCheck, IconCopy, IconDownload } from '@/components/ui/icons' |
| | import { Button } from '@/components/ui/button' |
| |
|
| | interface Props { |
| | language: string |
| | value: string |
| | } |
| |
|
| | interface languageMap { |
| | [key: string]: string | undefined |
| | } |
| |
|
| | export const programmingLanguages: languageMap = { |
| | javascript: '.js', |
| | python: '.py', |
| | java: '.java', |
| | c: '.c', |
| | cpp: '.cpp', |
| | 'c++': '.cpp', |
| | 'c#': '.cs', |
| | ruby: '.rb', |
| | php: '.php', |
| | swift: '.swift', |
| | 'objective-c': '.m', |
| | kotlin: '.kt', |
| | typescript: '.ts', |
| | go: '.go', |
| | perl: '.pl', |
| | rust: '.rs', |
| | scala: '.scala', |
| | haskell: '.hs', |
| | lua: '.lua', |
| | shell: '.sh', |
| | sql: '.sql', |
| | html: '.html', |
| | css: '.css' |
| | |
| | } |
| |
|
| | export const generateRandomString = (length: number, lowercase = false) => { |
| | const chars = 'ABCDEFGHJKLMNPQRSTUVWXY3456789' |
| | let result = '' |
| | for (let i = 0; i < length; i++) { |
| | result += chars.charAt(Math.floor(Math.random() * chars.length)) |
| | } |
| | return lowercase ? result.toLowerCase() : result |
| | } |
| |
|
| | const CodeBlock: FC<Props> = memo(({ language, value }) => { |
| | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) |
| |
|
| | const downloadAsFile = () => { |
| | if (typeof window === 'undefined') { |
| | return |
| | } |
| | const fileExtension = programmingLanguages[language] || '.file' |
| | const suggestedFileName = `file-${generateRandomString( |
| | 3, |
| | true |
| | )}${fileExtension}` |
| | const fileName = window.prompt('Enter file name' || '', suggestedFileName) |
| |
|
| | if (!fileName) { |
| | |
| | return |
| | } |
| |
|
| | const blob = new Blob([value], { type: 'text/plain' }) |
| | const url = URL.createObjectURL(blob) |
| | const link = document.createElement('a') |
| | link.download = fileName |
| | link.href = url |
| | link.style.display = 'none' |
| | document.body.appendChild(link) |
| | link.click() |
| | document.body.removeChild(link) |
| | URL.revokeObjectURL(url) |
| | } |
| |
|
| | const onCopy = () => { |
| | if (isCopied) return |
| | copyToClipboard(value) |
| | } |
| |
|
| | return ( |
| | <div className="codeblock relative w-full bg-zinc-950 font-sans"> |
| | <div className="flex w-full items-center justify-between bg-zinc-800 px-6 py-2 pr-4 text-zinc-100"> |
| | <span className="text-xs lowercase">{language}</span> |
| | <div className="flex items-center space-x-1"> |
| | <Button |
| | variant="ghost" |
| | className="hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0" |
| | onClick={downloadAsFile} |
| | size="icon" |
| | > |
| | <IconDownload /> |
| | <span className="sr-only">Download</span> |
| | </Button> |
| | <Button |
| | variant="ghost" |
| | size="icon" |
| | className="text-xs hover:bg-zinc-800 focus-visible:ring-1 focus-visible:ring-slate-700 focus-visible:ring-offset-0" |
| | onClick={onCopy} |
| | > |
| | {isCopied ? <IconCheck /> : <IconCopy />} |
| | <span className="sr-only">Copy code</span> |
| | </Button> |
| | </div> |
| | </div> |
| | <SyntaxHighlighter |
| | language={language} |
| | style={coldarkDark} |
| | PreTag="div" |
| | showLineNumbers |
| | customStyle={{ |
| | margin: 0, |
| | width: '100%', |
| | background: 'transparent', |
| | padding: '1.5rem 1rem' |
| | }} |
| | codeTagProps={{ |
| | style: { |
| | fontSize: '0.9rem', |
| | fontFamily: 'var(--font-mono)' |
| | } |
| | }} |
| | > |
| | {value} |
| | </SyntaxHighlighter> |
| | </div> |
| | ) |
| | }) |
| | CodeBlock.displayName = 'CodeBlock' |
| |
|
| | export { CodeBlock } |
| |
|