NotebookDragDropUpload.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import React, { useCallback, useState } from 'react';
  2. import { Upload as UploadIcon, FileText, Image as ImageIcon, Folder } from 'lucide-react';
  3. import { useLanguage } from '../contexts/LanguageContext';
  4. import { GROUP_ALLOWED_EXTENSIONS, IMAGE_MIME_TYPES } from '../constants/fileSupport';
  5. interface NotebookDragDropUploadProps {
  6. onFilesSelected: (files: FileList) => void;
  7. isAdmin: boolean;
  8. globalMode?: boolean;
  9. }
  10. export const NotebookDragDropUpload: React.FC<NotebookDragDropUploadProps> = ({ onFilesSelected, isAdmin, globalMode = false }) => {
  11. const { t } = useLanguage();
  12. const [isDragging, setIsDragging] = useState(false);
  13. const handleDragEnter = useCallback((e: React.DragEvent) => {
  14. e.preventDefault();
  15. e.stopPropagation();
  16. if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
  17. setIsDragging(true);
  18. }
  19. }, []);
  20. const handleDragLeave = useCallback((e: React.DragEvent) => {
  21. e.preventDefault();
  22. e.stopPropagation();
  23. // マウスが実際にドラッグ領域を離れた場合のみfalseに設定
  24. setTimeout(() => setIsDragging(false), 100);
  25. }, []);
  26. const handleDragOver = useCallback((e: React.DragEvent) => {
  27. e.preventDefault();
  28. e.stopPropagation();
  29. e.dataTransfer.dropEffect = 'copy';
  30. }, []);
  31. const handleDrop = useCallback((e: React.DragEvent) => {
  32. e.preventDefault();
  33. e.stopPropagation();
  34. setIsDragging(false);
  35. if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
  36. onFilesSelected(e.dataTransfer.files);
  37. e.dataTransfer.clearData();
  38. }
  39. }, [onFilesSelected]);
  40. const handleFileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
  41. if (e.target.files && e.target.files.length > 0) {
  42. onFilesSelected(e.target.files);
  43. // Reset the input so the same file can be selected again
  44. e.target.value = '';
  45. }
  46. };
  47. if (!isAdmin) {
  48. return null;
  49. }
  50. // モードに応じてCSSクラスを決定
  51. const containerClass = globalMode
  52. ? `fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 transition-opacity duration-300 ${isDragging ? 'opacity-100' : 'opacity-0 pointer-events-none'}`
  53. : `border-2 border-dashed rounded-xl p-8 text-center transition-all duration-200 cursor-pointer ${isDragging
  54. ? 'border-blue-500 bg-blue-50 scale-[1.02] shadow-lg'
  55. : 'border-slate-300 bg-slate-50 hover:border-blue-400 hover:bg-blue-25'
  56. }`;
  57. const contentClass = globalMode
  58. ? "w-3/4 max-w-2xl"
  59. : "";
  60. return (
  61. <div className={containerClass}>
  62. <div className={contentClass}>
  63. <div
  64. className={`border-2 border-dashed rounded-xl p-8 text-center transition-all duration-200 cursor-pointer ${isDragging
  65. ? 'border-blue-500 bg-blue-50 scale-[1.02] shadow-lg'
  66. : 'border-slate-300 bg-slate-50 hover:border-blue-400 hover:bg-blue-25'
  67. }`}
  68. onDragEnter={handleDragEnter}
  69. onDragOver={handleDragOver}
  70. onDragLeave={handleDragLeave}
  71. onDrop={handleDrop}
  72. onClick={() => document.getElementById('notebook-file-upload-input')?.click()}
  73. >
  74. <div className="flex flex-col items-center justify-center gap-6">
  75. <div className="p-4 bg-blue-100 rounded-full">
  76. <UploadIcon className="w-10 h-10 text-blue-600" />
  77. </div>
  78. <div className="space-y-2">
  79. <h3 className="text-lg font-semibold text-slate-700">
  80. {t('dragDropUploadTitle')}
  81. </h3>
  82. <p className="text-sm text-slate-500">
  83. {t('dragDropUploadDesc')}
  84. </p>
  85. </div>
  86. <div className="flex items-center gap-6 mt-2">
  87. <div className="flex items-center gap-2 text-xs text-slate-500 bg-white px-3 py-1.5 rounded-full border border-slate-200">
  88. <FileText className="w-4 h-4" />
  89. <span>{t('supportedFormats')}</span>
  90. </div>
  91. <div className="flex items-center gap-2 text-xs text-slate-500 bg-white px-3 py-1.5 rounded-full border border-slate-200">
  92. <ImageIcon className="w-4 h-4" />
  93. <span>{GROUP_ALLOWED_EXTENSIONS.slice(0, 10).join(', ').toUpperCase()}...</span>
  94. </div>
  95. </div>
  96. <div className="pt-2">
  97. <button
  98. type="button"
  99. className="flex items-center gap-2 px-5 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium shadow-sm"
  100. >
  101. <Folder className="w-4 h-4" />
  102. {t('browseFiles')}
  103. </button>
  104. <input
  105. type="file"
  106. multiple
  107. onChange={handleFileInput}
  108. className="hidden"
  109. id="notebook-file-upload-input"
  110. accept={GROUP_ALLOWED_EXTENSIONS.map(ext => `.${ext}`).join(',') + ',' + IMAGE_MIME_TYPES.join(',')}
  111. />
  112. </div>
  113. </div>
  114. </div>
  115. </div>
  116. </div>
  117. );
  118. };