147 lines
5.6 KiB
TypeScript
147 lines
5.6 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
Code,
|
|
Eye,
|
|
Copy,
|
|
Check,
|
|
ShieldCheck,
|
|
ExternalLink,
|
|
} from 'lucide-react';
|
|
import { BrandLogo } from '../ui/BrandLogo';
|
|
import { parseMarkdownToHtml, isHtmlContent } from '../../lib/markdownUtils';
|
|
|
|
interface MarkdownViewerProps {
|
|
content: string;
|
|
subject?: string;
|
|
showBrandingHeader?: boolean;
|
|
className?: string;
|
|
}
|
|
|
|
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
|
|
content,
|
|
subject,
|
|
showBrandingHeader = true,
|
|
className = '',
|
|
}) => {
|
|
const [viewMode, setViewMode] = useState<'rendered' | 'raw'>('rendered');
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const isHtml = useMemo(() => isHtmlContent(content), [content]);
|
|
|
|
const renderedHtml = useMemo(() => {
|
|
if (!content) return '<p class="text-slate-400 italic">No content</p>';
|
|
if (isHtml) return content;
|
|
return parseMarkdownToHtml(content);
|
|
}, [content, isHtml]);
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(content);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className={`flex flex-col rounded-2xl bg-slate-900/40 border border-slate-800/80 overflow-hidden ${className}`}>
|
|
{/* Top Controls Bar */}
|
|
<div className="px-4 py-2 border-b border-slate-800 bg-[#0d1322]/80 flex items-center justify-between text-xs text-slate-400">
|
|
<div className="flex items-center gap-2">
|
|
{showBrandingHeader && (
|
|
<div className="flex items-center gap-1.5 text-slate-300 font-semibold pr-2 border-r border-slate-700/60">
|
|
<BrandLogo size={18} showGlow={false} animateOnHover={false} />
|
|
<span className="text-[11px] tracking-tight">PFM Markdown Engine</span>
|
|
</div>
|
|
)}
|
|
<span className="text-[10px] text-blue-400 font-mono">
|
|
{isHtml ? 'Format: HTML Email' : 'Format: GitHub Flavored Markdown'}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{/* Toggle View Mode */}
|
|
<div className="flex items-center rounded-lg bg-slate-950/70 p-0.5 border border-slate-800">
|
|
<button
|
|
type="button"
|
|
onClick={() => setViewMode('rendered')}
|
|
className={`flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium transition cursor-pointer ${
|
|
viewMode === 'rendered'
|
|
? 'bg-blue-600/30 text-white font-semibold shadow-xs'
|
|
: 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
<Eye className="w-3 h-3" />
|
|
<span>Rendered</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setViewMode('raw')}
|
|
className={`flex items-center gap-1 px-2 py-1 rounded-md text-[11px] font-medium transition cursor-pointer ${
|
|
viewMode === 'raw'
|
|
? 'bg-blue-600/30 text-white font-semibold shadow-xs'
|
|
: 'text-slate-400 hover:text-white'
|
|
}`}
|
|
>
|
|
<Code className="w-3 h-3" />
|
|
<span>Raw Source</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Copy Button */}
|
|
<button
|
|
type="button"
|
|
onClick={handleCopy}
|
|
className="flex items-center gap-1 px-2 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-[11px] font-medium transition cursor-pointer border border-slate-700/60"
|
|
title="Copy content to clipboard"
|
|
>
|
|
{copied ? (
|
|
<>
|
|
<Check className="w-3 h-3 text-emerald-400" />
|
|
<span className="text-emerald-300">Copied</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Copy className="w-3 h-3" />
|
|
<span>Copy</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Rendering */}
|
|
{viewMode === 'rendered' ? (
|
|
<div className="p-6 overflow-x-auto text-sm leading-relaxed text-slate-200 font-sans">
|
|
{isHtml ? (
|
|
<div
|
|
className="prose prose-invert max-w-none"
|
|
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
|
/>
|
|
) : (
|
|
<div
|
|
className="prose prose-invert max-w-none [&_h1]:text-white [&_h2]:text-white [&_h3]:text-white [&_h4]:text-white [&_a]:text-blue-400 [&_a:hover]:underline [&_table]:border-collapse [&_th]:border [&_th]:border-slate-700 [&_th]:p-2 [&_td]:border [&_td]:border-slate-800 [&_td]:p-2 [&_blockquote]:border-l-4 [&_blockquote]:border-blue-500 [&_blockquote]:pl-4 [&_blockquote]:italic [&_code]:bg-slate-800 [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:rounded [&_code]:text-blue-300"
|
|
dangerouslySetInnerHTML={{ __html: renderedHtml }}
|
|
/>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<pre className="p-5 overflow-x-auto font-mono text-xs text-slate-300 bg-[#070b13] select-all whitespace-pre-wrap leading-relaxed">
|
|
{content}
|
|
</pre>
|
|
)}
|
|
|
|
{/* Branded Footer Stamp */}
|
|
{showBrandingHeader && (
|
|
<div className="px-5 py-2.5 border-t border-slate-800/80 bg-[#0a0f1b] flex items-center justify-between text-[11px] text-slate-400">
|
|
<div className="flex items-center gap-1.5">
|
|
<ShieldCheck className="w-3.5 h-3.5 text-blue-400" />
|
|
<span>Verified PFM Secure Platform Formatting</span>
|
|
</div>
|
|
<span className="font-mono text-[10px] text-slate-400">
|
|
{content.length} characters
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|