From 9f1c4c28c92d26dd5334e5b8465995d350105891 Mon Sep 17 00:00:00 2001 From: kitos Date: Wed, 3 Jun 2026 11:06:22 +0200 Subject: [PATCH] feat(evidence): paste screenshot directly from clipboard (Ctrl+V) - Global document paste listener captures image/* items from clipboard - Auto-generates filename: screenshot-YYYY-MM-DDTHH-MM-SS.png - Brief cyan pulse animation confirms the paste was detected - Shows image preview before uploading (max-h 192px, object-contain) - Drop zone hint now says 'Drag & drop, browse, or Ctrl+V to paste' - Works with any source: OS screenshot (PrintScreen/Cmd+Shift+4), browser Inspect screenshots, any image copied to clipboard --- frontend/src/components/EvidenceUpload.tsx | 183 +++++++++++++++------ 1 file changed, 130 insertions(+), 53 deletions(-) diff --git a/frontend/src/components/EvidenceUpload.tsx b/frontend/src/components/EvidenceUpload.tsx index 2cda3c0..f966b96 100644 --- a/frontend/src/components/EvidenceUpload.tsx +++ b/frontend/src/components/EvidenceUpload.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback, useRef } from "react"; -import { Upload, Loader2, X, FileIcon } from "lucide-react"; +import { useState, useCallback, useRef, useEffect } from "react"; +import { Upload, Loader2, X, FileIcon, ClipboardPaste, ImageIcon } from "lucide-react"; interface EvidenceUploadProps { onUpload: (file: File) => Promise; @@ -9,8 +9,49 @@ interface EvidenceUploadProps { export default function EvidenceUpload({ onUpload, isUploading }: EvidenceUploadProps) { const [isDragging, setIsDragging] = useState(false); const [selectedFile, setSelectedFile] = useState(null); + const [pasteHint, setPasteHint] = useState(false); const fileInputRef = useRef(null); + const containerRef = useRef(null); + // ── Clipboard paste handler ───────────────────────────────────── + const handlePaste = useCallback( + async (e: ClipboardEvent) => { + const items = e.clipboardData?.items; + if (!items) return; + + for (const item of Array.from(items)) { + if (item.type.startsWith("image/")) { + e.preventDefault(); + const blob = item.getAsFile(); + if (!blob) continue; + + // Generate a timestamped filename for pasted screenshots + const ext = item.type.split("/")[1] || "png"; + const ts = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, 19); + const file = new File([blob], `screenshot-${ts}.${ext}`, { + type: item.type, + }); + setSelectedFile(file); + // Brief pulse animation to confirm paste was detected + setPasteHint(true); + setTimeout(() => setPasteHint(false), 1500); + return; + } + } + }, + [], + ); + + // Listen for paste on the whole document while this component is mounted + useEffect(() => { + document.addEventListener("paste", handlePaste); + return () => document.removeEventListener("paste", handlePaste); + }, [handlePaste]); + + // ── Drag & drop ───────────────────────────────────────────────── const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); @@ -25,33 +66,27 @@ export default function EvidenceUpload({ onUpload, isUploading }: EvidenceUpload e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; - if (file) { - setSelectedFile(file); - } + if (file) setSelectedFile(file); }, []); + // ── File picker ───────────────────────────────────────────────── const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; - if (file) { - setSelectedFile(file); - } + if (file) setSelectedFile(file); }; + // ── Upload ─────────────────────────────────────────────────────── const handleUpload = async () => { if (selectedFile) { await onUpload(selectedFile); setSelectedFile(null); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } + if (fileInputRef.current) fileInputRef.current.value = ""; } }; const clearSelection = () => { setSelectedFile(null); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } + if (fileInputRef.current) fileInputRef.current.value = ""; }; const formatFileSize = (bytes: number): string => { @@ -60,16 +95,20 @@ export default function EvidenceUpload({ onUpload, isUploading }: EvidenceUpload return (bytes / (1024 * 1024)).toFixed(1) + " MB"; }; + const isImage = selectedFile?.type.startsWith("image/"); + return ( -
+
{/* Drop zone */}
fileInputRef.current?.click()} - className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${ - isDragging + className={`cursor-pointer rounded-lg border-2 border-dashed p-6 text-center transition-all ${ + pasteHint + ? "border-cyan-400 bg-cyan-500/15 scale-[1.01]" + : isDragging ? "border-cyan-500 bg-cyan-500/10" : "border-gray-700 bg-gray-800/50 hover:border-gray-600 hover:bg-gray-800" }`} @@ -81,57 +120,95 @@ export default function EvidenceUpload({ onUpload, isUploading }: EvidenceUpload className="hidden" />

- {isDragging ? ( + {pasteHint ? ( + Screenshot detected ✓ + ) : isDragging ? ( "Drop file here" ) : ( <> - Drag and drop a file, or browse + Drag & drop, browse, or{" "} + Ctrl+V to paste a screenshot )}

-

- Screenshots, logs, pcap files, etc. (max 50 MB) -

+
+ + Paste from clipboard + + · + + Screenshots, logs, pcap… (max 50 MB) + +
{/* Selected file preview */} {selectedFile && ( -
-
- -
-

{selectedFile.name}

-

{formatFileSize(selectedFile.size)}

+
+ {/* Image preview for pasted screenshots */} + {isImage && ( +
+ Screenshot preview +
+ preview +
-
-
- - +
+

+ {selectedFile.name} +

+

+ {formatFileSize(selectedFile.size)} + {isImage && ( + screenshot + )} +

+
+
+
+ + +
)}