From 2337845b3519f6089a211693757de736fe3a2c9d Mon Sep 17 00:00:00 2001
From: Nancy <9d.24.nancy.sangani@gmail.com>
Date: Wed, 24 Jun 2026 12:47:56 +0530
Subject: [PATCH] feat(ui): full-page drag-and-drop upload overlay with hover
animations
---
frontend/src/app/dashboard/page.tsx | 29 ++++
.../document/DashboardDropOverlay.tsx | 59 ++++++++
frontend/src/hooks/useDashboardDrop.ts | 138 ++++++++++++++++++
3 files changed, 226 insertions(+)
create mode 100644 frontend/src/components/document/DashboardDropOverlay.tsx
create mode 100644 frontend/src/hooks/useDashboardDrop.ts
diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx
index 86b297d2..3d3392be 100644
--- a/frontend/src/app/dashboard/page.tsx
+++ b/frontend/src/app/dashboard/page.tsx
@@ -15,6 +15,8 @@ import DocumentSidebar from "@/components/document/DocumentSidebar";
import ChatSessionSidebar from "@/components/chat/ChatSessionSidebar";
import ChatPanel from "@/components/chat/ChatPanel";
import CompareView from "@/components/document/CompareView";
+import DashboardDropOverlay from "@/components/document/DashboardDropOverlay";
+import { useDashboardDrop } from "@/hooks/useDashboardDrop";
function PDFViewerSkeleton() {
return (
@@ -150,6 +152,29 @@ export default function DashboardPage() {
})();
}, [user, loadDocuments]);
+ // ── Full-page drag-and-drop ──────────────────────────────────────────────
+ const handlePageDrop = useCallback(
+ async (files: File[]) => {
+ for (const file of files) {
+ const formData = new FormData();
+ formData.append("file", file);
+ try {
+ await api.postForm("/api/v1/documents/upload", formData);
+ } catch (err) {
+ const message = err instanceof Error ? err.message : "Upload failed";
+ console.error(`Upload failed for ${file.name}:`, message);
+ }
+ }
+ void loadDocuments();
+ },
+ [loadDocuments],
+ );
+
+ const { isDraggingOver, dropZoneProps } = useDashboardDrop({
+ onDrop: handlePageDrop,
+ disabled: !user,
+ });
+
// Ingest status change toast notification handler
useEffect(() => {
const prev = prevDocsRef.current;
@@ -213,6 +238,10 @@ export default function DashboardPage() {
return (
+
setSidebarOpen(!sidebarOpen)}
diff --git a/frontend/src/components/document/DashboardDropOverlay.tsx b/frontend/src/components/document/DashboardDropOverlay.tsx
new file mode 100644
index 00000000..0000a18a
--- /dev/null
+++ b/frontend/src/components/document/DashboardDropOverlay.tsx
@@ -0,0 +1,59 @@
+"use client";
+
+import { UploadCloud } from "lucide-react";
+
+interface DashboardDropOverlayProps {
+ isDraggingOver: boolean;
+ dropZoneProps: {
+ onDragEnter: (e: React.DragEvent) => void;
+ onDragOver: (e: React.DragEvent) => void;
+ onDragLeave: (e: React.DragEvent) => void;
+ onDrop: (e: React.DragEvent) => void;
+ };
+}
+
+export default function DashboardDropOverlay({
+ isDraggingOver,
+ dropZoneProps,
+}: DashboardDropOverlayProps) {
+ if (!isDraggingOver) return null;
+
+ return (
+
+ {/* Blurred backdrop */}
+
+
+ {/* Animated dashed border card */}
+
+ {/* Pulsing icon ring */}
+
+
+
+
+
+
+
+
+ Drop files to upload
+
+
+ PDF, DOCX, TXT, MD supported
+
+
+
+
+ );
+}
diff --git a/frontend/src/hooks/useDashboardDrop.ts b/frontend/src/hooks/useDashboardDrop.ts
new file mode 100644
index 00000000..aa05f95a
--- /dev/null
+++ b/frontend/src/hooks/useDashboardDrop.ts
@@ -0,0 +1,138 @@
+/**
+ * useDashboardDrop
+ *
+ * Tracks when the user drags files over the browser window and exposes
+ * an `isDraggingOver` flag so the dashboard can render a full-page
+ * drop-zone overlay. The actual file handling is delegated to the
+ * caller via `onDrop`.
+ */
+import { useCallback, useEffect, useRef, useState } from "react";
+
+const ACCEPTED_EXTENSIONS = [".pdf", ".docx", ".txt", ".md"];
+const ACCEPTED_MIME_TYPES = [
+ "application/pdf",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ "text/plain",
+ "text/markdown",
+];
+
+function hasAcceptedFile(transfer: DataTransfer): boolean {
+ // DataTransferItemList gives MIME type before the drop
+ if (transfer.items && transfer.items.length > 0) {
+ return Array.from(transfer.items).some(
+ (item) => item.kind === "file" && ACCEPTED_MIME_TYPES.includes(item.type),
+ );
+ }
+ // Fallback: FileList (available after drop)
+ if (transfer.files && transfer.files.length > 0) {
+ return Array.from(transfer.files).some((file) => {
+ const ext = "." + file.name.split(".").pop()?.toLowerCase();
+ return ACCEPTED_EXTENSIONS.includes(ext);
+ });
+ }
+ return false;
+}
+
+interface UseDashboardDropOptions {
+ onDrop: (files: File[]) => void;
+ disabled?: boolean;
+}
+
+interface UseDashboardDropResult {
+ isDraggingOver: boolean;
+ dropZoneProps: {
+ onDragEnter: (e: React.DragEvent) => void;
+ onDragOver: (e: React.DragEvent) => void;
+ onDragLeave: (e: React.DragEvent) => void;
+ onDrop: (e: React.DragEvent) => void;
+ };
+}
+
+export function useDashboardDrop({
+ onDrop,
+ disabled = false,
+}: UseDashboardDropOptions): UseDashboardDropResult {
+ const [isDraggingOver, setIsDraggingOver] = useState(false);
+ // Counter tracks nested dragenter/dragleave pairs so we don't flicker
+ const dragCounter = useRef(0);
+
+ const handleWindowDragEnter = useCallback(
+ (e: DragEvent) => {
+ if (disabled) return;
+ if (!e.dataTransfer || !hasAcceptedFile(e.dataTransfer)) return;
+ dragCounter.current += 1;
+ if (dragCounter.current === 1) setIsDraggingOver(true);
+ },
+ [disabled],
+ );
+
+ const handleWindowDragLeave = useCallback(
+ (e: DragEvent) => {
+ if (disabled) return;
+ dragCounter.current = Math.max(0, dragCounter.current - 1);
+ if (dragCounter.current === 0) setIsDraggingOver(false);
+ },
+ [disabled],
+ );
+
+ const handleWindowDrop = useCallback((e: DragEvent) => {
+ dragCounter.current = 0;
+ setIsDraggingOver(false);
+ }, []);
+
+ // Attach window-level listeners so ANY drag over the page triggers the overlay
+ useEffect(() => {
+ window.addEventListener("dragenter", handleWindowDragEnter);
+ window.addEventListener("dragleave", handleWindowDragLeave);
+ window.addEventListener("drop", handleWindowDrop);
+ return () => {
+ window.removeEventListener("dragenter", handleWindowDragEnter);
+ window.removeEventListener("dragleave", handleWindowDragLeave);
+ window.removeEventListener("drop", handleWindowDrop);
+ };
+ }, [handleWindowDragEnter, handleWindowDragLeave, handleWindowDrop]);
+
+ // Reset when disabled
+ useEffect(() => {
+ if (disabled) {
+ dragCounter.current = 0;
+ const id = setTimeout(() => setIsDraggingOver(false), 0);
+ return () => clearTimeout(id);
+ }
+ }, [disabled]);
+
+ // Props spread onto the overlay itself
+ const dropZoneProps = {
+ onDragEnter: (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ onDragOver: (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ e.dataTransfer.dropEffect = "copy";
+ },
+ onDragLeave: (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ onDrop: (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dragCounter.current = 0;
+ setIsDraggingOver(false);
+ if (disabled) return;
+
+ const files = Array.from(e.dataTransfer.files).filter((file) => {
+ const ext = "." + file.name.split(".").pop()?.toLowerCase();
+ return ACCEPTED_EXTENSIONS.includes(ext);
+ });
+
+ if (files.length > 0) {
+ onDrop(files);
+ }
+ },
+ };
+
+ return { isDraggingOver, dropZoneProps };
+}