fix(upload): wire drag-and-drop on admin + user upload zones (#504)
The dashed-border upload area in `PhotoUpload` (admin) and `UserPhotoUpload` (gallery user-upload) is styled and labelled as a drop zone — every locale's `upload.clickToUpload` already reads "Click to upload or drag and drop" or its translation — but neither component had any `onDragOver` / `onDragEnter` / `onDragLeave` / `onDrop` handlers. Files dropped on the zone fell through to the browser's default behaviour (open the image in a new tab), which is what Rekoo-PS reported. Added native HTML5 drag-and-drop wiring on both components, plumbed through the same filter/limit/toast pipeline used by the click path (`addFiles` helper). Visual highlight on drag-over via an `isDragOver` flag; the listener guards against the `dragleave` strobing that fires on every child node. Also reset the `<input>` value after onChange so re-picking the same file still triggers an upload — matches the new drop-then-pick mental model.
This commit is contained in:
@@ -79,14 +79,16 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
);
|
||||
|
||||
const remainingSlots = Math.max(maxFilesPerUpload - selectedFiles.length, 0);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
// Shared filter + per-upload-limit pipeline used by both the file-input
|
||||
// change handler and the drop handler. #504 — without the drop handler
|
||||
// the dashed-border zone looked draggable but silently fell through to
|
||||
// the browser's default "open the file in a new tab" behaviour.
|
||||
const addFiles = (incoming: File[]) => {
|
||||
const imageFiles = incoming.filter((file) => allowedMimeTypes.includes(file.type));
|
||||
if (imageFiles.length === 0) return;
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(e.target.files || []);
|
||||
const imageFiles = files.filter(file =>
|
||||
allowedMimeTypes.includes(file.type)
|
||||
);
|
||||
|
||||
// Check total file count with existing files
|
||||
const totalFiles = selectedFiles.length + imageFiles.length;
|
||||
if (totalFiles > maxFilesPerUpload) {
|
||||
const allowedNewFiles = maxFilesPerUpload - selectedFiles.length;
|
||||
@@ -101,11 +103,44 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
t('upload.someFilesSkipped', { allowed: allowedNewFiles, limit: maxFilesPerUpload }) ||
|
||||
`Only ${allowedNewFiles} more files can be added (limit ${maxFilesPerUpload})`
|
||||
);
|
||||
setSelectedFiles(prev => [...prev, ...imageFiles.slice(0, allowedNewFiles)]);
|
||||
setSelectedFiles((prev) => [...prev, ...imageFiles.slice(0, allowedNewFiles)]);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedFiles(prev => [...prev, ...imageFiles]);
|
||||
|
||||
setSelectedFiles((prev) => [...prev, ...imageFiles]);
|
||||
};
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
addFiles(Array.from(e.target.files || []));
|
||||
// Reset the input so picking the same files again still fires onChange.
|
||||
if (e.target.value) e.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// dropEffect must be set on every dragover for the cursor to render
|
||||
// the "copy" affordance in Chrome/Firefox.
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
if (!isDragOver) setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// dragleave fires for every child node the cursor passes — only flip
|
||||
// the highlight off when the cursor leaves the zone itself, otherwise
|
||||
// it strobes on/off as the user moves over the icon and text.
|
||||
if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
const files = Array.from(e.dataTransfer.files || []);
|
||||
addFiles(files);
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
@@ -349,14 +384,22 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* File Input Area */}
|
||||
{/* File Input Area — accepts both click-to-pick and drag-and-drop (#504). */}
|
||||
<div
|
||||
className={clsx(
|
||||
"border-2 border-dashed rounded-lg p-8 text-center transition-colors",
|
||||
"border-2 border-dashed rounded-lg p-8 text-center transition-colors cursor-pointer",
|
||||
"hover:border-accent-dark hover:bg-accent-dark/15",
|
||||
selectedFiles.length > 0 ? "border-accent-dark bg-accent-dark/15" : "border-neutral-300 dark:border-neutral-600"
|
||||
isDragOver
|
||||
? "border-accent-dark bg-accent-dark/25"
|
||||
: selectedFiles.length > 0
|
||||
? "border-accent-dark bg-accent-dark/15"
|
||||
: "border-neutral-300 dark:border-neutral-600"
|
||||
)}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Upload className="w-12 h-12 mx-auto text-neutral-400 dark:text-neutral-500 mb-4" />
|
||||
<p className="text-neutral-700 dark:text-neutral-300 font-medium mb-1">
|
||||
|
||||
@@ -28,6 +28,7 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
// bytes-on-wire for that file, so the UI can show "Processing…"
|
||||
// instead of a static 100% bar while the backend works.
|
||||
const [processingFiles, setProcessingFiles] = useState<{ [key: string]: boolean }>({});
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
|
||||
const { data: publicSettings } = usePublicSettings();
|
||||
|
||||
@@ -41,11 +42,9 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
[publicSettings?.allowed_file_types]
|
||||
);
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFiles = Array.from(e.target.files || []);
|
||||
|
||||
// Validate file types
|
||||
const validFiles = selectedFiles.filter(file => {
|
||||
// Shared filter pipeline for both <input> change and drag-and-drop (#504).
|
||||
const addFiles = (incoming: File[]) => {
|
||||
const validFiles = incoming.filter((file) => {
|
||||
if (!allowedMimeTypes.includes(file.type)) {
|
||||
toast.error(`Invalid file type: ${file.name}`);
|
||||
return false;
|
||||
@@ -57,8 +56,38 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (validFiles.length === 0) return;
|
||||
setFiles((prev) => [...prev, ...validFiles]);
|
||||
};
|
||||
|
||||
setFiles(prev => [...prev, ...validFiles]);
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
addFiles(Array.from(e.target.files || []));
|
||||
// Reset so re-selecting the same file fires onChange again.
|
||||
if (e.target.value) e.target.value = '';
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
if (!isDragOver) setIsDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// dragleave fires for every child node — only flip off when the cursor
|
||||
// leaves the zone itself.
|
||||
if (e.currentTarget.contains(e.relatedTarget as Node | null)) return;
|
||||
setIsDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragOver(false);
|
||||
if (uploading) return;
|
||||
addFiles(Array.from(e.dataTransfer.files || []));
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
@@ -154,10 +183,18 @@ export const UserPhotoUpload: React.FC<UserPhotoUploadProps> = ({
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 p-4 sm:p-6 overflow-y-auto min-h-0">
|
||||
{/* Upload Area */}
|
||||
{/* Upload Area — accepts both click-to-pick and drag-and-drop (#504). */}
|
||||
<div className="mb-4 sm:mb-6">
|
||||
<label className="block">
|
||||
<div className="border-2 border-dashed border-surface rounded-lg p-6 sm:p-8 text-center hover:border-accent-dark transition-colors cursor-pointer">
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 sm:p-8 text-center hover:border-accent-dark transition-colors cursor-pointer ${
|
||||
isDragOver ? 'border-accent-dark bg-accent-dark/10' : 'border-surface'
|
||||
}`}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnter={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Upload className="w-10 h-10 sm:w-12 sm:h-12 text-neutral-400 mx-auto mb-3" />
|
||||
<p className="text-sm font-medium text-muted-theme mb-1">
|
||||
{t('upload.clickToUpload')}
|
||||
|
||||
Reference in New Issue
Block a user