From a1c3f8c3c0aebd734049acb4b4d6466569b93dd6 Mon Sep 17 00:00:00 2001 From: Sudha Rajput Date: Fri, 22 May 2026 19:09:39 +0000 Subject: [PATCH] feat: add page-level drag-and-drop overlay for video upload --- src/components/FileUpload.tsx | 277 ++++++++++++++++++++-------------- 1 file changed, 164 insertions(+), 113 deletions(-) diff --git a/src/components/FileUpload.tsx b/src/components/FileUpload.tsx index 6e10fd77..d76845c5 100644 --- a/src/components/FileUpload.tsx +++ b/src/components/FileUpload.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRef, useState, useEffect} from "react"; +import { useRef, useState, useEffect, useCallback } from "react"; import { Film, FolderOpen } from "lucide-react"; import LottiePlayer from "./LottiePlayer"; import uploadAnim from "@/lib/lottie/upload.json"; @@ -23,9 +23,12 @@ export default function FileUpload({ const inputRef = useRef(null); const [dragging, setDragging] = useState(false); + const [pageDragging, setPageDragging] = useState(false); const [error, setError] = useState(""); const [warning, setWarning] = useState(""); - + const dragCounterRef = useRef(0); + + // ── Keyboard shortcut Ctrl+O ────────────────────────── useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "o") { @@ -37,13 +40,55 @@ export default function FileUpload({ return () => document.removeEventListener("keydown", handler); }, []); - const handleFile = (file: File) => { + // ── Page-level drag overlay ─────────────────────────── + // Uses a counter so nested dragenter/dragleave don't flicker + useEffect(() => { + const onDragEnter = (e: DragEvent) => { + e.preventDefault(); + dragCounterRef.current += 1; + if (dragCounterRef.current === 1) setPageDragging(true); + }; + + const onDragLeave = (e: DragEvent) => { + e.preventDefault(); + dragCounterRef.current -= 1; + if (dragCounterRef.current === 0) setPageDragging(false); + }; + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); // required to allow drop + }; + + const onDrop = (e: DragEvent) => { + e.preventDefault(); + dragCounterRef.current = 0; + setPageDragging(false); + + const file = e.dataTransfer?.files?.[0]; + if (file) handleFile(file); + }; + + document.addEventListener("dragenter", onDragEnter); + document.addEventListener("dragleave", onDragLeave); + document.addEventListener("dragover", onDragOver); + document.addEventListener("drop", onDrop); + + return () => { + document.removeEventListener("dragenter", onDragEnter); + document.removeEventListener("dragleave", onDragLeave); + document.removeEventListener("dragover", onDragOver); + document.removeEventListener("drop", onDrop); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // ── File validation ─────────────────────────────────── + const handleFile = useCallback((file: File) => { setError(""); setWarning(""); - // Validate type if (!file.type.startsWith("video/")) { - setError("Only video files are allowed."); + setError("Please drop a valid video file (MP4, MOV, AVI, WebM, etc.)"); return; } @@ -52,89 +97,85 @@ export default function FileUpload({ return; } - // Hard limit if (file.size > MAX_FILE_SIZE) { setError( - `File too large (${formatBytes( - file.size - )}). Maximum allowed size is 2GB.` + `File too large (${formatBytes(file.size)}). Maximum allowed size is 2GB.` ); return; } - // Soft warning if (file.size > WARNING_FILE_SIZE) { - const estimatedMinutes = Math.max(1, Math.round(file.size / (100 * 1024 * 1024))); + const estimatedMinutes = Math.max( + 1, + Math.round(file.size / (100 * 1024 * 1024)) + ); setWarning( - `Large file detected (${formatBytes( - file.size - )}). Processing may take ~${estimatedMinutes} minutes and affect performance on low-memory devices.` + `Large file detected (${formatBytes(file.size)}). Processing may take ~${estimatedMinutes} minutes.` ); } onFileSelect(file); - }; + }, [onFileSelect]); + // ── Drop zone (inner) handler ───────────────────────── const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setDragging(false); - const file = e.dataTransfer.files?.[0]; if (file) handleFile(file); }; - const FileInfo = () => ( -
-
- -
- -
- -
- - - -
-
-

- {currentFile?.name} -

- {currentFile && ( - - {currentFile.name.includes(".") - ? currentFile.name.split(".").pop() - : "VIDEO"} - - )} + // ── File info (shown after upload) ─────────────────── + const FileInfo = () => ( +
+
+
+
+
-
-

{formatBytes(currentFile?.size ?? 0)}

- -

- {duration > 0 - ? `Duration: ${formatDuration(duration)}` - : "Loading duration..."} -

+ +
+
+

+ {currentFile?.name} +

+ {currentFile && ( + + {currentFile.name.includes(".") + ? currentFile.name.split(".").pop() + : "VIDEO"} + + )} +
+
+

{formatBytes(currentFile?.size ?? 0)}

+

+ {duration > 0 + ? `Duration: ${formatDuration(duration)}` + : "Loading duration..."} +

+
-
- + +
+

+ Supports: MP4, MOV, AVI, MKV, WebM, and most video formats +

{fileError && ( -

- {fileError} -

+

{fileError}

)} +
+ ); -

- Supports: MP4, MOV, AVI, MKV, WebM, and most video formats -

- - {fileError && ( -

{fileError}

- )} - - { - const f = e.target.files?.[0]; - if (f) handleFile(f); - }} - /> -
-); + // ── Drop zone (inner) ───────────────────────────────── const DropZone = () => (
{ e.preventDefault(); setDragging(true); @@ -192,25 +216,19 @@ export default function FileUpload({ : "border-[var(--border)] bg-[var(--bg)] hover:border-film-400 hover:bg-film-50/40" )} > - {/* Premium Light Beam Shimmer Effect */} {dragging && (
)} +

- {dragging - ? "Release to upload" - : "Drag & Drop your video in here"} -

- -

- or click to browse + {dragging ? "Release to upload" : "Drag & Drop your video here"}

- +

or click to browse

Ctrl+O / Cmd+O

@@ -229,33 +247,66 @@ export default function FileUpload({

{fileError}

)} - {currentFile && ( -

- Selected: {formatBytes(currentFile.size)} -

- )} - - { - const f = e.target.files?.[0]; - - if (f) handleFile(f); - }} - /> -
+ { + const f = e.target.files?.[0]; + if (f) handleFile(f); + }} + /> +
); return ( -
- {error &&

{error}

} + <> + {/* ── Page-level drag overlay ── */} + {pageDragging && ( +
+ {/* Animated ring */} +
+
+
+ +
+
- {warning &&

{warning}

} +
+

+ Drop your video anywhere +

+

+ Release to start uploading +

+
+
+ )} - {currentFile ? : } -
+ {/* ── Normal upload UI ── */} +
+ {error && ( +

+ {error} +

+ )} + {warning && ( +

+ {warning} +

+ )} + {currentFile ? : } +
+ ); -} +} \ No newline at end of file