1
0
forked from maik/IPFS-portal
Files
IPFS-portal-deploy/src/app/explorer/components/FilePreview.tsx
T

152 lines
5.4 KiB
TypeScript
Raw Normal View History

'use client';
import { X, Download } from 'lucide-react';
interface FilePreviewProps {
cid: string;
content: string | null;
loading: boolean;
onClose: () => void;
filename?: string;
}
function detectFileType(filename: string | undefined): 'image' | 'markdown' | 'json' | 'text' | 'other' {
const ext = filename?.split('.').pop()?.toLowerCase() ?? '';
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) return 'image';
if (ext === 'md') return 'markdown';
if (ext === 'json') return 'json';
if (['txt', 'js', 'ts', 'py', 'css', 'html', 'sh', 'yaml', 'yml', 'toml', 'xml'].includes(ext)) return 'text';
return 'other';
}
function renderMarkdown(text: string): string {
return text
.replace(/^### (.+)$/gm, '<h3 class="text-white font-semibold text-sm mt-3 mb-1">$1</h3>')
.replace(/^## (.+)$/gm, '<h2 class="text-white font-semibold text-base mt-4 mb-1">$1</h2>')
.replace(/^# (.+)$/gm, '<h1 class="text-white font-bold text-lg mt-4 mb-2">$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-white">$1</strong>')
.replace(/\*(.+?)\*/g, '<em class="text-surface-300">$1</em>')
.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" class="text-brand-400 underline hover:text-brand-300" target="_blank" rel="noopener">$1</a>')
.replace(/^- (.+)$/gm, '<li class="text-surface-300 ml-4 list-disc">$1</li>')
.replace(/`(.+?)`/g, '<code class="text-accent-cyan bg-surface-800 px-1 py-0.5 rounded text-xs">$1</code>')
.replace(/\n\n/g, '</p><p class="text-surface-300 text-sm mb-2">')
.replace(/^(?!<[hop])/gm, (match) => match ? match : '');
}
function renderJson(text: string): string {
try {
const parsed = JSON.parse(text);
const syntax = JSON.stringify(parsed, null, 2);
return syntax;
} catch {
return text;
}
}
function JsonDisplay({ text }: { text: string }) {
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
return <pre className="text-sm font-mono whitespace-pre-wrap text-surface-300">{text}</pre>;
}
const formatted = JSON.stringify(parsed, null, 2);
const colored = formatted.replace(
/("(?:\\.|[^"\\])*")\s*:/g,
'<span class="text-accent-cyan">$1</span>:',
).replace(
/:\s*("(?:\\.|[^"\\])*")/g,
':<span class="text-accent-green"> $1</span>',
).replace(
/:\s*(true|false)/g,
':<span class="text-accent-purple"> $1</span>',
).replace(
/:\s*(\d+\.?\d*)/g,
':<span class="text-accent-amber"> $1</span>',
).replace(
/:\s*(null)/g,
':<span class="text-surface-500"> $1</span>',
);
return (
<pre
className="text-sm font-mono whitespace-pre-wrap text-surface-300"
dangerouslySetInnerHTML={{ __html: colored }}
/>
);
}
export default function FilePreview({ cid, content, loading, onClose, filename }: FilePreviewProps) {
const fileType = detectFileType(filename);
return (
<div className="glass rounded-xl p-5 animate-fade-in">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-white">
{filename || cid.slice(0, 16) + '…'}
</span>
<span className="text-xs text-surface-500 uppercase px-1.5 py-0.5 rounded bg-surface-800">
{fileType}
</span>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg hover:bg-surface-700 transition-colors"
>
<X className="w-4 h-4 text-surface-400" />
</button>
</div>
{/* Content */}
{loading ? (
<div className="animate-pulse space-y-2">
<div className="h-3 rounded bg-surface-700 w-full" />
<div className="h-3 rounded bg-surface-700 w-5/6" />
<div className="h-3 rounded bg-surface-700 w-4/6" />
<div className="h-3 rounded bg-surface-700 w-3/6" />
</div>
) : fileType === 'image' ? (
<div className="flex items-center justify-center bg-surface-900 rounded-lg p-2">
<img
src={`https://ipfs.io/ipfs/${cid}`}
alt={filename ?? 'IPFS content'}
className="max-w-full max-h-96 rounded object-contain"
/>
</div>
) : fileType === 'markdown' && content ? (
<div
className="prose prose-invert max-w-none text-sm text-surface-300 max-h-96 overflow-y-auto whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: renderMarkdown(content) }}
/>
) : fileType === 'json' && content ? (
<div className="max-h-96 overflow-y-auto">
<JsonDisplay text={content} />
</div>
) : fileType === 'text' && content ? (
<pre className="text-sm font-mono whitespace-pre-wrap text-surface-300 max-h-96 overflow-y-auto">
{content}
</pre>
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<p className="text-sm text-surface-500 mb-4">
Preview not available for this file type
</p>
<a
href={`https://ipfs.io/ipfs/${cid}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-surface-800 text-sm text-surface-200 hover:bg-surface-700 transition-colors"
>
<Download className="w-4 h-4" />
Download file
</a>
</div>
)}
</div>
);
}