feat(evidence): inline preview for images and text/JSON files
Adds a View button (eye icon) on each evidence card for previewable file types. Opens a full-screen modal: - Images (png/jpg/gif/webp/svg/…): rendered directly via <img> tag - JSON: fetched authenticated, pretty-printed in green mono - Text/log/md/csv/xml/yaml/…: fetched authenticated, shown in <pre> Non-previewable files only show the Download button as before. Modal closes on Escape or backdrop click.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { FileIcon, Download, ExternalLink, Copy, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { FileIcon, Download, Copy, Check, Eye } from "lucide-react";
|
||||
import EvidencePreviewModal, { getPreviewType } from "./EvidencePreviewModal";
|
||||
|
||||
interface Evidence {
|
||||
id: string;
|
||||
@@ -18,16 +19,16 @@ interface EvidenceListProps {
|
||||
|
||||
export default function EvidenceList({ evidences, onDownload }: EvidenceListProps) {
|
||||
const [copiedHash, setCopiedHash] = useState<string | null>(null);
|
||||
const [preview, setPreview] = useState<Evidence | null>(null);
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleDateString("en-US", {
|
||||
const formatDate = (dateStr: string) =>
|
||||
new Date(dateStr).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
};
|
||||
|
||||
const copyHash = async (hash: string) => {
|
||||
await navigator.clipboard.writeText(hash);
|
||||
@@ -45,53 +46,89 @@ export default function EvidenceList({ evidences, onDownload }: EvidenceListProp
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{evidences.map((evidence) => (
|
||||
<div
|
||||
key={evidence.id}
|
||||
className="rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:bg-gray-800/50"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-gray-700 p-2">
|
||||
<FileIcon className="h-5 w-5 text-gray-400" />
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
{evidences.map((evidence) => {
|
||||
const previewType = getPreviewType(evidence.file_name);
|
||||
const canPreview = previewType !== null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={evidence.id}
|
||||
className="rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:bg-gray-800/50"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="rounded-lg bg-gray-700 p-2">
|
||||
<FileIcon className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-200">{evidence.file_name}</p>
|
||||
<p className="mt-0.5 text-xs text-gray-500">
|
||||
Uploaded {formatDate(evidence.uploaded_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2">
|
||||
{canPreview && (
|
||||
<button
|
||||
onClick={() => setPreview(evidence)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors"
|
||||
title="Preview"
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
View
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => onDownload(evidence.id)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-gray-200">{evidence.file_name}</p>
|
||||
<p className="mt-0.5 text-xs text-gray-500">
|
||||
Uploaded {formatDate(evidence.uploaded_at)}
|
||||
</p>
|
||||
|
||||
{/* SHA256 Hash */}
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-500">SHA256:</span>
|
||||
<code className="flex-1 truncate rounded bg-gray-900 px-2 py-1 font-mono text-xs text-gray-400">
|
||||
{evidence.sha256_hash}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyHash(evidence.sha256_hash)}
|
||||
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-gray-300"
|
||||
title="Copy hash"
|
||||
>
|
||||
{copiedHash === evidence.sha256_hash ? (
|
||||
<Check className="h-4 w-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDownload(evidence.id)}
|
||||
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-1.5 text-sm text-gray-300 hover:border-cyan-500/50 hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* SHA256 Hash */}
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-500">SHA256:</span>
|
||||
<code className="flex-1 truncate rounded bg-gray-900 px-2 py-1 font-mono text-xs text-gray-400">
|
||||
{evidence.sha256_hash}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyHash(evidence.sha256_hash)}
|
||||
className="rounded p-1 text-gray-500 hover:bg-gray-700 hover:text-gray-300"
|
||||
title="Copy hash"
|
||||
>
|
||||
{copiedHash === evidence.sha256_hash ? (
|
||||
<Check className="h-4 w-4 text-green-400" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Preview modal */}
|
||||
{preview && (
|
||||
<EvidencePreviewModal
|
||||
evidenceId={preview.id}
|
||||
fileName={preview.file_name}
|
||||
previewType={getPreviewType(preview.file_name)}
|
||||
downloadUrl={preview.download_url ?? `/api/v1/evidence/${preview.id}/file`}
|
||||
onClose={() => setPreview(null)}
|
||||
onDownload={() => {
|
||||
onDownload(preview.id);
|
||||
setPreview(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user