forked from maik/IPFS-portal
1ddc89479c
Multi-file upload with Quick Upload + Crypto Payment tabs. Dashboard with live IPFS metrics, Explorer, Peers, Pins, History, Settings pages. Docker deploy (standalone output), full test suite (16 Playwright tests). Payment integration via IPFSPortalPayment + MockUSDC contracts on zkSync.
152 lines
5.4 KiB
TypeScript
152 lines
5.4 KiB
TypeScript
'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>
|
|
);
|
|
}
|