diff --git a/src/components/FileDropzone.jsx b/src/components/FileDropzone.jsx new file mode 100644 index 0000000..b6e06e3 --- /dev/null +++ b/src/components/FileDropzone.jsx @@ -0,0 +1,108 @@ +import { useState, useRef } from 'react'; +import Box from '@mui/material/Box'; + +/** + * An otherwise invisible dropzone wrapper. + * Renders seamlessly around children without altering layout or style, + * and highlights with an outline only while a file is dragged over it. + */ +export function FileDropzone({ + accept = 'application/pdf,.pdf', + disabled = false, + onFileDrop, + children, + sx, + ...props +}) { + const [isDragging, setIsDragging] = useState(false); + const dragCounter = useRef(0); + + const handleDragEnter = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (disabled) return; + dragCounter.current += 1; + if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { + setIsDragging(true); + } + }; + + const handleDragLeave = (e) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current -= 1; + if (dragCounter.current <= 0) { + dragCounter.current = 0; + setIsDragging(false); + } + }; + + const handleDragOver = (e) => { + e.preventDefault(); + e.stopPropagation(); + if (disabled) { + e.dataTransfer.dropEffect = 'none'; + return; + } + e.dataTransfer.dropEffect = 'copy'; + }; + + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + dragCounter.current = 0; + setIsDragging(false); + if (disabled) return; + + const file = e.dataTransfer.files?.[0]; + if (!file) return; + + if (accept) { + const allowed = accept + .split(',') + .map((s) => s.trim().toLowerCase()) + .filter(Boolean); + const name = (file.name || '').toLowerCase(); + const dotIndex = name.lastIndexOf('.'); + const ext = dotIndex !== -1 ? name.slice(dotIndex) : ''; + const mimeType = (file.type || '').toLowerCase(); + + const matches = allowed.some((item) => { + if (item.startsWith('.')) { + return ext === item; + } + return mimeType === item; + }); + + if (!matches) return; + } + + onFileDrop?.(file); + }; + + return ( + + {typeof children === 'function' ? children({ isDragging }) : children} + + ); +} + +export default FileDropzone; diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index d9349b4..01c3e4a 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -7,6 +7,7 @@ import TextField from '@mui/material/TextField'; import UploadFileIcon from '@mui/icons-material/UploadFile'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; import { PanelTile } from 'dashgui/panels'; +import FileDropzone from '../FileDropzone'; import { manifestConfig, partyDisplayName } from './manifestConfig'; class ManifestHeader extends PureComponent { @@ -122,16 +123,42 @@ class ManifestHeader extends PureComponent { /> ) : null} {onPdfUpload ? ( - + {({ isDragging }) => ( + + )} + ) : null} {onPdfUpload ? (