174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Code Block Expanded Modal Component
|
|
*
|
|
* Full-screen code viewer for better readability
|
|
* Features:
|
|
* - Full-screen overlay with dark backdrop
|
|
* - Centered modal with max-width constraint
|
|
* - Language tabs for switching between languages
|
|
* - Close button (X) in top-right
|
|
* - Escape key support
|
|
* - Larger code text in expanded mode
|
|
* - Copy button with feedback
|
|
* - Language badge
|
|
*
|
|
* Usage:
|
|
* <CodeBlockExpanded
|
|
* isOpen={isOpen}
|
|
* onClose={() => setIsOpen(false)}
|
|
* codes={{ curl: '...', javascript: '...', python: '...', go: '...' }}
|
|
* initialLanguage="curl"
|
|
* filename="example.js"
|
|
* />
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
type Language = 'curl' | 'javascript' | 'python' | 'go';
|
|
|
|
interface CodeBlockExpandedProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
codes: Record<Language, string>;
|
|
initialLanguage: Language;
|
|
filename?: string;
|
|
}
|
|
|
|
export const CodeBlockExpanded = ({
|
|
isOpen,
|
|
onClose,
|
|
codes,
|
|
initialLanguage,
|
|
filename,
|
|
}: CodeBlockExpandedProps) => {
|
|
const [currentLanguage, setCurrentLanguage] = useState<Language>(initialLanguage);
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
// Update language when modal opens
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setCurrentLanguage(initialLanguage);
|
|
}
|
|
}, [isOpen, initialLanguage]);
|
|
|
|
// Handle escape key
|
|
useEffect(() => {
|
|
const handleEscape = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener('keydown', handleEscape);
|
|
// Prevent body scroll when modal is open
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('keydown', handleEscape);
|
|
document.body.style.overflow = 'unset';
|
|
};
|
|
}, [isOpen, onClose]);
|
|
|
|
// Copy to clipboard
|
|
const copyCode = () => {
|
|
navigator.clipboard.writeText(codes[currentLanguage]);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
|
onClick={onClose}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Expanded code view"
|
|
>
|
|
{/* Modal Content */}
|
|
<div
|
|
className="relative w-full max-w-7xl max-h-[90vh] overflow-hidden rounded-2xl bg-slate-950 border border-slate-700 shadow-2xl"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-700 bg-slate-900/80">
|
|
{/* Left: Language Tabs */}
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-400 mr-2">Language:</span>
|
|
<div className="flex gap-2">
|
|
{(['curl', 'javascript', 'python', 'go'] as Language[]).map((lang) => (
|
|
<button
|
|
key={lang}
|
|
onClick={() => setCurrentLanguage(lang)}
|
|
className={`px-3 py-1.5 text-xs rounded-lg transition-colors ${
|
|
currentLanguage === lang
|
|
? 'bg-purple-500/20 text-purple-300 font-semibold'
|
|
: 'text-gray-400 hover:text-white hover:bg-slate-800'
|
|
}`}
|
|
>
|
|
{lang === 'javascript' ? 'JavaScript' : lang.charAt(0).toUpperCase() + lang.slice(1)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right: Actions */}
|
|
<div className="flex items-center gap-2">
|
|
{/* Copy Button */}
|
|
<button
|
|
onClick={copyCode}
|
|
className={`px-4 py-1.5 text-xs rounded-lg transition-colors ${
|
|
copied
|
|
? 'bg-green-500/20 text-green-400'
|
|
: 'bg-purple-600/20 hover:bg-purple-600/30 text-purple-400'
|
|
}`}
|
|
>
|
|
{copied ? '✓ Copied!' : 'Copy'}
|
|
</button>
|
|
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 text-gray-400 hover:text-white transition-colors rounded-lg hover:bg-slate-800"
|
|
aria-label="Close expanded view"
|
|
>
|
|
<svg
|
|
className="w-5 h-5"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M6 18L18 6M6 6l12 12"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Code Content - Larger text, better spacing */}
|
|
<div className="overflow-auto max-h-[calc(90vh-6rem)] p-8 bg-slate-950/50">
|
|
<pre className="text-base text-gray-300 leading-relaxed">
|
|
<code>{codes[currentLanguage]}</code>
|
|
</pre>
|
|
</div>
|
|
|
|
{/* Hint Text */}
|
|
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 px-3 py-1.5 bg-slate-900/90 border border-slate-700 rounded-full">
|
|
<p className="text-xs text-gray-400">
|
|
Press <kbd className="px-1.5 py-0.5 bg-slate-800 rounded text-purple-400">Esc</kbd> to close
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|