forked from maik/IPFS-portal
feat: initial IPFS Portal — Next.js frontend for remote IPFS node management
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.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user