195 lines
6.1 KiB
TypeScript
195 lines
6.1 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
|
import styles from './DragDropOverlay.module.css';
|
|
import { IoFolderOpen } from 'react-icons/io5';
|
|
|
|
export interface DragDropConfig {
|
|
enabled: boolean;
|
|
onDrop?: (files: File[]) => Promise<void> | void;
|
|
accept?: string; // MIME types or file extensions
|
|
multiple?: boolean;
|
|
disabled?: boolean;
|
|
overlayText?: string;
|
|
overlaySubtext?: string;
|
|
}
|
|
|
|
interface DragDropOverlayProps {
|
|
config: DragDropConfig;
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}
|
|
|
|
export function DragDropOverlay({
|
|
config,
|
|
children,
|
|
className = ''
|
|
}: DragDropOverlayProps) {
|
|
const { t } = useLanguage();
|
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
|
|
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (config.disabled || !config.enabled) return;
|
|
|
|
setIsDragOver(true);
|
|
}, [config.disabled, config.enabled]);
|
|
|
|
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
// Only hide overlay if we're leaving the container entirely
|
|
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
setIsDragOver(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}, []);
|
|
|
|
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
if (config.disabled || !config.enabled) return;
|
|
|
|
setIsDragOver(false);
|
|
setIsProcessing(true);
|
|
|
|
try {
|
|
const files = Array.from(e.dataTransfer.files);
|
|
|
|
// Filter files by accept type if specified
|
|
let filteredFiles = files;
|
|
if (config.accept && config.accept !== '*/*') {
|
|
filteredFiles = files.filter(file => {
|
|
const acceptTypes = config.accept!.split(',').map(type => type.trim());
|
|
return acceptTypes.some(acceptType => {
|
|
// Handle MIME types
|
|
if (acceptType.startsWith('.')) {
|
|
return file.name.toLowerCase().endsWith(acceptType.toLowerCase());
|
|
}
|
|
// Handle MIME types like "image/*" or "application/pdf"
|
|
if (acceptType.includes('*')) {
|
|
const baseType = acceptType.split('/')[0];
|
|
return file.type.startsWith(baseType);
|
|
}
|
|
return file.type === acceptType;
|
|
});
|
|
});
|
|
}
|
|
|
|
console.log('🔍 DragDropOverlay file filtering:', {
|
|
originalFiles: files.length,
|
|
filteredFiles: filteredFiles.length,
|
|
accept: config.accept,
|
|
fileDetails: files.map(f => ({ name: f.name, type: f.type }))
|
|
});
|
|
|
|
// Respect multiple setting
|
|
const filesToProcess = config.multiple ? filteredFiles : filteredFiles.slice(0, 1);
|
|
|
|
if (filesToProcess.length > 0 && config.onDrop) {
|
|
console.log('🎯 DragDropOverlay calling onDrop with files:', filesToProcess);
|
|
await config.onDrop(filesToProcess);
|
|
console.log('✅ DragDropOverlay onDrop completed');
|
|
} else {
|
|
console.log('⚠️ DragDropOverlay: No files to process or no onDrop handler');
|
|
console.log('Files to process:', filesToProcess.length);
|
|
console.log('Has onDrop:', !!config.onDrop);
|
|
}
|
|
} catch (error) {
|
|
console.error(t('dragdrop.overlay.error', 'Error processing files'), error);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
}, [config, t]);
|
|
|
|
const handleFileInputChange = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (config.disabled || !config.enabled) return;
|
|
|
|
const files = Array.from(e.target.files || []);
|
|
console.log('🔍 DragDropOverlay file input:', {
|
|
fileCount: files.length,
|
|
accept: config.accept,
|
|
fileDetails: files.map(f => ({ name: f.name, type: f.type }))
|
|
});
|
|
|
|
if (files.length > 0 && config.onDrop) {
|
|
setIsProcessing(true);
|
|
try {
|
|
await config.onDrop(files);
|
|
} catch (error) {
|
|
console.error(t('dragdrop.overlay.error', 'Error processing files'), error);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
// Reset input
|
|
e.target.value = '';
|
|
}
|
|
}
|
|
}, [config, t]);
|
|
|
|
if (!config.enabled) {
|
|
return <>{children}</>;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={`${styles.dragDropContainer} ${className}`}
|
|
onDragEnter={handleDragEnter}
|
|
onDragLeave={handleDragLeave}
|
|
onDragOver={handleDragOver}
|
|
onDrop={handleDrop}
|
|
>
|
|
{children}
|
|
|
|
{/* Hidden file input for click-to-upload */}
|
|
<input
|
|
type="file"
|
|
accept={config.accept}
|
|
multiple={config.multiple}
|
|
onChange={handleFileInputChange}
|
|
className={styles.hiddenFileInput}
|
|
disabled={config.disabled}
|
|
/>
|
|
|
|
{/* Drag overlay */}
|
|
{isDragOver && (
|
|
<div className={styles.dragOverlay}>
|
|
<div className={styles.dragOverlayContent}>
|
|
<div className={styles.dragIcon}>
|
|
<IoFolderOpen />
|
|
</div>
|
|
<div className={styles.dragText}>
|
|
{config.overlayText || t('dragdrop.overlay.default_text', 'Drop files here')}
|
|
</div>
|
|
{(config.overlaySubtext || !config.overlayText) && (
|
|
<div className={styles.dragSubtext}>
|
|
{config.overlaySubtext || t('dragdrop.overlay.default_subtext', 'You can also click the upload button')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Processing overlay */}
|
|
{isProcessing && (
|
|
<div className={styles.processingOverlay}>
|
|
<div className={styles.processingContent}>
|
|
<div className={styles.spinner}></div>
|
|
<div className={styles.processingText}>
|
|
{t('dragdrop.overlay.processing', 'Processing files...')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default DragDropOverlay;
|