|
|
import { useEffect, useState } from "react" |
|
|
import { motion } from "framer-motion" |
|
|
import { Copy, Check } from "lucide-react" |
|
|
import { Button } from "@/components/ui/button" |
|
|
import Prism from "prismjs" |
|
|
import "prismjs/themes/prism-tomorrow.css" |
|
|
import "prismjs/components/prism-javascript" |
|
|
import "prismjs/components/prism-typescript" |
|
|
import "prismjs/components/prism-python" |
|
|
import "prismjs/components/prism-jsx" |
|
|
import "prismjs/components/prism-tsx" |
|
|
import "prismjs/components/prism-json" |
|
|
import "prismjs/components/prism-bash" |
|
|
import "prismjs/components/prism-markup" |
|
|
import "prismjs/components/prism-css" |
|
|
|
|
|
interface CodeSnippetProps { |
|
|
filename?: string |
|
|
language?: string |
|
|
code: string |
|
|
delay?: number |
|
|
className?: string |
|
|
showLineNumbers?: boolean |
|
|
copyable?: boolean |
|
|
} |
|
|
|
|
|
const LANGUAGE_MAP: Record<string, string> = { |
|
|
js: "javascript", |
|
|
javascript: "javascript", |
|
|
ts: "typescript", |
|
|
typescript: "typescript", |
|
|
py: "python", |
|
|
python: "python", |
|
|
jsx: "jsx", |
|
|
tsx: "tsx", |
|
|
json: "json", |
|
|
bash: "bash", |
|
|
sh: "bash", |
|
|
html: "markup", |
|
|
css: "css", |
|
|
sql: "sql", |
|
|
go: "go", |
|
|
rust: "rust", |
|
|
java: "java", |
|
|
}; |
|
|
|
|
|
export function CodeSnippet({ |
|
|
filename = "example.js", |
|
|
language = "javascript", |
|
|
code, |
|
|
delay = 0.5, |
|
|
className = "", |
|
|
showLineNumbers = false, |
|
|
copyable = true, |
|
|
}: CodeSnippetProps) { |
|
|
const [copied, setCopied] = useState(false); |
|
|
const prismLanguage = LANGUAGE_MAP[language.toLowerCase()] || language; |
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
Prism.highlightAll(); |
|
|
}, [code, language]); |
|
|
|
|
|
const handleCopy = async () => { |
|
|
try { |
|
|
await navigator.clipboard.writeText(code); |
|
|
setCopied(true); |
|
|
setTimeout(() => setCopied(false), 2000); |
|
|
} catch (err) { |
|
|
console.error("Failed to copy:", err); |
|
|
} |
|
|
}; |
|
|
|
|
|
const displayLanguage = language.toUpperCase(); |
|
|
|
|
|
return ( |
|
|
<motion.div |
|
|
initial={{ opacity: 0, y: 40 }} |
|
|
animate={{ opacity: 1, y: 0 }} |
|
|
transition={{ duration: 0.7, delay }} |
|
|
className={`group relative w-full max-w-3xl rounded-xl overflow-hidden border border-white/10 shadow-2xl bg-[#0d1117]/80 backdrop-blur-sm ${className}`} |
|
|
> |
|
|
{/* Header */} |
|
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/5 bg-gradient-to-r from-white/[0.03] to-white/[0.01]"> |
|
|
<div className="flex items-center gap-2"> |
|
|
<div className="flex gap-1.5"> |
|
|
<div className="w-3 h-3 rounded-full bg-red-500/80" /> |
|
|
<div className="w-3 h-3 rounded-full bg-yellow-500/80" /> |
|
|
<div className="w-3 h-3 rounded-full bg-green-500/80" /> |
|
|
</div> |
|
|
<div className="text-xs text-muted-foreground ml-2 font-mono truncate"> |
|
|
{filename} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div className="flex items-center gap-3"> |
|
|
{copyable && ( |
|
|
<Button |
|
|
variant="ghost" |
|
|
size="sm" |
|
|
onClick={handleCopy} |
|
|
className="h-7 px-2 hover:bg-white/10 transition-colors" |
|
|
> |
|
|
{copied ? ( |
|
|
<Check className="w-3.5 h-3.5 text-green-400" /> |
|
|
) : ( |
|
|
<Copy className="w-3.5 h-3.5 text-gray-400" /> |
|
|
)} |
|
|
<span className="ml-1 text-xs"> |
|
|
{copied ? "Copied!" : "Copy"} |
|
|
</span> |
|
|
</Button> |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
{/* Code Area */} |
|
|
<div className="relative"> |
|
|
<pre className={`font-mono text-sm leading-relaxed m-0 overflow-x-auto p-6 ${showLineNumbers ? 'line-numbers' : ''}`}> |
|
|
<code className={`language-${prismLanguage}`}> |
|
|
{code} |
|
|
</code> |
|
|
</pre> |
|
|
|
|
|
<div className="absolute right-0 top-0 bottom-0 w-8 bg-gradient-to-l from-[#0d1117] to-transparent pointer-events-none" /> |
|
|
</div> |
|
|
</motion.div> |
|
|
); |
|
|
} |
|
|
|