File size: 6,256 Bytes
8822914
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import { FaUpload } from 'react-icons/fa';
import { apiClient } from '@/utils/api';

type AcceptMap = {
  [mime: string]: string[];
};

interface FullscreenDropOverlayProps {
  datasetName: string; // where to upload
  onComplete?: () => void; // called after successful upload
  accept?: AcceptMap; // optional override
  multiple?: boolean; // default true
}

export default function FullscreenDropOverlay({
  datasetName,
  onComplete,
  accept,
  multiple = true,
}: FullscreenDropOverlayProps) {
  const [visible, setVisible] = useState(false);
  const [isUploading, setIsUploading] = useState(false);
  const [uploadProgress, setUploadProgress] = useState(0);
  const dragDepthRef = useRef(0); // drag-enter/leave tracking

  // Only show the overlay for real file drags (not text, images from page, etc)
  const isFileDrag = (e: DragEvent) => {
    const types = e?.dataTransfer?.types;
    return !!types && Array.from(types).includes('Files');
  };

  // Window-level drag listeners to toggle visibility
  useEffect(() => {
    const onDragEnter = (e: DragEvent) => {
      if (!isFileDrag(e)) return;
      dragDepthRef.current += 1;
      setVisible(true);
      e.preventDefault();
    };
    const onDragOver = (e: DragEvent) => {
      if (!isFileDrag(e)) return;
      // Must preventDefault to allow dropping in the browser
      e.preventDefault();
      if (!visible) setVisible(true);
    };
    const onDragLeave = (e: DragEvent) => {
      if (!isFileDrag(e)) return;
      dragDepthRef.current = Math.max(0, dragDepthRef.current - 1);
      if (dragDepthRef.current === 0 && !isUploading) {
        setVisible(false);
      }
    };
    const onDrop = (e: DragEvent) => {
      if (!isFileDrag(e)) return;
      // Prevent browser from opening the file
      e.preventDefault();
      dragDepthRef.current = 0;
      // We do NOT hide here; the dropzone onDrop will handle workflow visibility.
    };

    window.addEventListener('dragenter', onDragEnter);
    window.addEventListener('dragover', onDragOver);
    window.addEventListener('dragleave', onDragLeave);
    window.addEventListener('drop', onDrop);

    return () => {
      window.removeEventListener('dragenter', onDragEnter);
      window.removeEventListener('dragover', onDragOver);
      window.removeEventListener('dragleave', onDragLeave);
      window.removeEventListener('drop', onDrop);
    };
  }, [visible, isUploading]);

  const onDrop = useCallback(
    async (acceptedFiles: File[]) => {
      if (acceptedFiles.length === 0) {
        // no accepted files; hide overlay cleanly
        setVisible(false);
        return;
      }

      setIsUploading(true);
      setUploadProgress(0);

      const formData = new FormData();
      acceptedFiles.forEach(file => formData.append('files', file));
      formData.append('datasetName', datasetName || '');

      try {
        await apiClient.post(`/api/datasets/upload`, formData, {
          headers: { 'Content-Type': 'multipart/form-data' },
          onUploadProgress: pe => {
            const percent = Math.round(((pe.loaded || 0) * 100) / (pe.total || pe.loaded || 1));
            setUploadProgress(percent);
          },
          timeout: 0,
        });
        onComplete?.();
      } catch (err) {
        console.error('Upload failed:', err);
      } finally {
        setIsUploading(false);
        setUploadProgress(0);
        setVisible(false);
      }
    },
    [datasetName, onComplete],
  );

  const dropAccept = useMemo<AcceptMap>(
    () =>
      accept || {
        'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'],
        'video/*': ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.m4v', '.flv'],
        'text/*': ['.txt'],
      },
    [accept],
  );

  const { getRootProps, getInputProps, isDragActive } = useDropzone({
    onDrop,
    accept: dropAccept,
    multiple,
    noClick: true,
    noKeyboard: true,
    // Prevent "folder opens" by browser if someone drags outside the overlay mid-drop:
    preventDropOnDocument: true,
  });

  return (
    <div
      // When hidden: opacity-0 + pointer-events-none so the page is fully interactive
      // When visible or uploading: fade in and capture the drop
      className={`fixed inset-0 z-[9999] transition-opacity duration-200 ${
        visible || isUploading ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'
      }`}
      aria-hidden={!visible && !isUploading}
      {...getRootProps()}
    >
      {/* Fullscreen capture layer */}
      <input {...getInputProps()} />

      {/* Backdrop: keep it subtle so context remains visible */}
      <div className={`absolute inset-0 ${isUploading ? 'bg-gray-900/70' : 'bg-gray-900/40'}`} />

      {/* Center drop target UI */}
      <div className="absolute inset-0 flex items-center justify-center p-6">
        <div
          className={`w-full max-w-2xl rounded-2xl border-2 border-dashed px-8 py-10 text-center shadow-2xl backdrop-blur-sm
          ${isDragActive ? 'border-blue-400 bg-white/10' : 'border-white/30 bg-white/5'}`}
        >
          <div className="flex flex-col items-center gap-4">
            <FaUpload className="size-10 opacity-80" />
            {!isUploading ? (
              <>
                <p className="text-lg font-semibold">Drop files to upload</p>
                <p className="text-sm opacity-80">
                  Destination:&nbsp;<span className="font-mono">{datasetName || 'unknown'}</span>
                </p>
                <p className="text-xs opacity-70 mt-1">Images, videos, or .txt supported</p>
              </>
            ) : (
              <>
                <p className="text-lg font-semibold">Uploading… {uploadProgress}%</p>
                <div className="w-full h-2.5 bg-white/20 rounded-full overflow-hidden">
                  <div
                    className="h-2.5 bg-blue-500 rounded-full transition-[width] duration-150 ease-linear"
                    style={{ width: `${uploadProgress}%` }}
                  />
                </div>
              </>
            )}
          </div>
        </div>
      </div>
    </div>
  );
}