GlobalDragDropOverlay.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { useLayoutEffect, useRef, useState, useCallback } from 'react';
  2. import { useLanguage } from '../contexts/LanguageContext';
  3. import { motion, AnimatePresence } from 'framer-motion';
  4. import { FileUp, ShieldCheck, FileText, Image as ImageIcon } from 'lucide-react';
  5. interface GlobalDragDropProps {
  6. onFilesSelected: (files: FileList) => void;
  7. isAdmin: boolean;
  8. }
  9. let isDragDropEnabled = true;
  10. let forceHideCallback: (() => void) | null = null;
  11. export const setDragDropEnabled = (enabled: boolean) => {
  12. isDragDropEnabled = enabled;
  13. if (!enabled && forceHideCallback) {
  14. forceHideCallback();
  15. }
  16. };
  17. export const GlobalDragDropOverlay: React.FC<GlobalDragDropProps> = ({ onFilesSelected, isAdmin }) => {
  18. const { t } = useLanguage();
  19. const overlayRef = useRef<HTMLDivElement>(null);
  20. const [isVisible, setIsVisible] = useState(false);
  21. const dragCounterRef = useRef(0);
  22. const isDragActiveRef = useRef(false);
  23. const hasFiles = useCallback((dt: DataTransfer | null) => {
  24. if (!dt) return false;
  25. const hasFileType = dt.types && dt.types.includes('Files');
  26. if (!hasFileType) return false;
  27. if (dt.items && dt.items.length > 0) {
  28. for (let i = 0; i < dt.items.length; i++) {
  29. if (dt.items[i].kind === 'file') return true;
  30. }
  31. return false;
  32. }
  33. return hasFileType;
  34. }, []);
  35. const handleDragEnter = useCallback((e: DragEvent) => {
  36. if (!isDragDropEnabled || !hasFiles(e.dataTransfer)) return;
  37. e.preventDefault();
  38. e.stopPropagation();
  39. dragCounterRef.current++;
  40. if (dragCounterRef.current === 1) {
  41. setIsVisible(true);
  42. isDragActiveRef.current = true;
  43. }
  44. }, [hasFiles]);
  45. const handleDragOver = useCallback((e: DragEvent) => {
  46. if (!isDragDropEnabled || !hasFiles(e.dataTransfer)) return;
  47. e.preventDefault();
  48. e.stopPropagation();
  49. e.dataTransfer!.dropEffect = 'copy';
  50. }, [hasFiles]);
  51. const handleDragLeave = useCallback((e: DragEvent) => {
  52. if (!isDragDropEnabled || !hasFiles(e.dataTransfer)) return;
  53. e.preventDefault();
  54. e.stopPropagation();
  55. dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
  56. if (dragCounterRef.current === 0 && isDragActiveRef.current) {
  57. setIsVisible(false);
  58. isDragActiveRef.current = false;
  59. }
  60. }, [hasFiles]);
  61. const handleDrop = useCallback((e: DragEvent) => {
  62. if (!isDragDropEnabled || !hasFiles(e.dataTransfer)) return;
  63. e.preventDefault();
  64. e.stopPropagation();
  65. dragCounterRef.current = 0;
  66. setIsVisible(false);
  67. isDragActiveRef.current = false;
  68. if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
  69. onFilesSelected(e.dataTransfer.files);
  70. }
  71. }, [hasFiles, onFilesSelected]);
  72. useLayoutEffect(() => {
  73. if (!isAdmin) return;
  74. dragCounterRef.current = 0;
  75. isDragActiveRef.current = false;
  76. forceHideCallback = () => {
  77. dragCounterRef.current = 0;
  78. isDragActiveRef.current = false;
  79. setIsVisible(false);
  80. };
  81. document.addEventListener('dragenter', handleDragEnter);
  82. document.addEventListener('dragover', handleDragOver);
  83. document.addEventListener('dragleave', handleDragLeave);
  84. document.addEventListener('drop', handleDrop);
  85. return () => {
  86. document.removeEventListener('dragenter', handleDragEnter);
  87. document.removeEventListener('dragover', handleDragOver);
  88. document.removeEventListener('dragleave', handleDragLeave);
  89. document.removeEventListener('drop', handleDrop);
  90. forceHideCallback = null;
  91. dragCounterRef.current = 0;
  92. isDragActiveRef.current = false;
  93. setIsVisible(false);
  94. };
  95. }, [isAdmin, handleDragEnter, handleDragOver, handleDragLeave, handleDrop]);
  96. if (!isAdmin || typeof window === 'undefined') return null;
  97. return (
  98. <AnimatePresence>
  99. {isVisible && (
  100. <motion.div
  101. initial={{ opacity: 0 }}
  102. animate={{ opacity: 1 }}
  103. exit={{ opacity: 0 }}
  104. className="fixed inset-0 bg-blue-600/10 backdrop-blur-md items-center justify-center z-[9999] pointer-events-none flex p-8"
  105. >
  106. <motion.div
  107. initial={{ scale: 0.9, y: 20 }}
  108. animate={{ scale: 1, y: 0 }}
  109. exit={{ scale: 0.9, y: 20 }}
  110. className="w-full max-w-2xl bg-white rounded-[2.5rem] p-12 text-center shadow-[0_32px_64px_-12px_rgba(0,0,0,0.14)] border border-white pointer-events-auto"
  111. >
  112. <div className="flex flex-col items-center justify-center gap-8">
  113. <div className="w-24 h-24 bg-blue-600 text-white rounded-3xl flex items-center justify-center shadow-xl shadow-blue-200 animate-bounce">
  114. <FileUp size={48} />
  115. </div>
  116. <div className="space-y-3">
  117. <h3 className="text-3xl font-black text-slate-900 tracking-tight">
  118. {t('dragDropUploadTitle')}
  119. </h3>
  120. <p className="text-lg text-slate-500 font-medium">
  121. {t('dropAnywhere')}
  122. </p>
  123. </div>
  124. <div className="flex flex-wrap items-center justify-center gap-6 py-8 border-y border-slate-100 w-full">
  125. <div className="flex items-center gap-3 px-4 py-2 bg-slate-50 rounded-2xl text-sm font-bold text-slate-600 uppercase tracking-wider">
  126. <ShieldCheck size={20} className="text-emerald-500" />
  127. <span>{t('secureProcessing')}</span>
  128. </div>
  129. <div className="flex items-center gap-3 px-4 py-2 bg-slate-50 rounded-2xl text-sm font-bold text-slate-600 uppercase tracking-wider">
  130. <FileText size={20} className="text-blue-500" />
  131. <span>{t('allFormats')}</span>
  132. </div>
  133. <div className="flex items-center gap-3 px-4 py-2 bg-slate-50 rounded-2xl text-sm font-bold text-slate-600 uppercase tracking-wider">
  134. <ImageIcon size={20} className="text-purple-500" />
  135. <span>{t('visualVision')}</span>
  136. </div>
  137. </div>
  138. <div className="text-slate-400 font-bold text-xs uppercase tracking-[0.3em]">
  139. {t('releaseToIngest')}
  140. </div>
  141. </div>
  142. </motion.div>
  143. </motion.div>
  144. )}
  145. </AnimatePresence>
  146. );
  147. };