| 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> |
| ); |
| } |
|
|