NotebookGlobalDragDropOverlay.tsx 6.1 KB

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