Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | 1x 44x 44x 44x 20x 9x 20x 20x 20x 9x 44x 7x 7x 9x 1x 7x 6x 6x 6x 44x 7x 6x 7x 44x 2x 1x 1x 44x 44x 44x 44x 18x 1x | // src/features/flyer/BulkImporter.tsx
import React, { useCallback, useState, useEffect } from 'react';
import { UploadIcon } from '../../components/icons/UploadIcon';
import { useDragAndDrop } from '../../hooks/useDragAndDrop';
import { XMarkIcon } from '../../components/icons/XMarkIcon';
import { DocumentTextIcon } from '../../components/icons/DocumentTextIcon';
interface BulkImporterProps {
onFilesChange: (files: File[]) => void;
isProcessing: boolean;
}
export const BulkImporter: React.FC<BulkImporterProps> = ({ onFilesChange, isProcessing }) => {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
// Effect to create and revoke object URLs for image previews
useEffect(() => {
const newUrls = selectedFiles.map((file) =>
file.type.startsWith('image/') ? URL.createObjectURL(file) : '',
);
setPreviewUrls(newUrls);
// Cleanup function to revoke URLs when component unmounts or files change
return () => {
newUrls.forEach((url) => {
if (url) URL.revokeObjectURL(url);
});
};
}, [selectedFiles]);
const handleFiles = useCallback(
(files: FileList) => {
Eif (files && files.length > 0 && !isProcessing) {
// Prevent duplicates by checking file names and sizes
const newFiles = Array.from(files).filter(
(newFile) =>
!selectedFiles.some(
(existingFile) =>
existingFile.name === newFile.name && existingFile.size === newFile.size,
),
);
if (newFiles.length > 0) {
const updatedFiles = [...selectedFiles, ...newFiles];
setSelectedFiles(updatedFiles);
onFilesChange(updatedFiles); // Call parent callback directly
}
}
},
[isProcessing, selectedFiles, onFilesChange],
);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
handleFiles(e.target.files);
}
// Reset input value to allow selecting the same file again
e.target.value = '';
};
const removeFile = (index: number) => {
const updatedFiles = selectedFiles.filter((_, i) => i !== index);
setSelectedFiles(updatedFiles);
onFilesChange(updatedFiles); // Also notify parent on removal
};
const { isDragging, dropzoneProps } = useDragAndDrop<HTMLLabelElement>({
onFilesDropped: handleFiles,
disabled: isProcessing,
});
const borderColor = isDragging ? 'border-brand-primary' : 'border-gray-300 dark:border-gray-600';
const bgColor = isDragging
? 'bg-brand-light/50 dark:bg-brand-dark/20'
: 'bg-gray-50 dark:bg-gray-800';
return (
<div className="w-full">
<label
htmlFor="bulk-file-upload"
{...dropzoneProps}
className={`flex flex-col items-center justify-center w-full h-48 border-2 ${borderColor} ${bgColor} border-dashed rounded-lg transition-colors duration-300 ${isProcessing ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700'}`}
>
<div className="flex flex-col items-center justify-center pt-5 pb-6 text-center">
<UploadIcon className="w-10 h-10 mb-3 text-gray-400" />
{isProcessing ? (
<p className="mb-2 text-sm text-gray-600 dark:text-gray-300 font-semibold">
Processing, please wait...
</p>
) : (
<>
<p className="mb-2 text-sm text-gray-500 dark:text-gray-400">
<span className="font-semibold text-brand-primary">Click to upload</span> or drag
and drop
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">PNG, JPG, WEBP, or PDF</p>
</>
)}
</div>
<input
id="bulk-file-upload"
type="file"
className="absolute w-px h-px p-0 -m-px overflow-hidden clip-rect-0 whitespace-nowrap border-0"
accept="image/png, image/jpeg, image/webp, application/pdf"
onChange={handleFileChange}
disabled={isProcessing}
multiple
/>
</label>
{selectedFiles.length > 0 && (
<div className="mt-4 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{selectedFiles.map((file, index) => (
<div
key={index}
className="relative group border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
>
{previewUrls[index] ? (
<img
src={previewUrls[index]}
alt={file.name}
className="h-32 w-full object-cover"
/>
) : (
<div className="h-32 w-full flex items-center justify-center bg-gray-100 dark:bg-gray-700">
<DocumentTextIcon className="w-12 h-12 text-gray-400" />
</div>
)}
<div className="absolute bottom-0 left-0 right-0 bg-black bg-opacity-50 text-white text-xs p-1 truncate">
{file.name}
</div>
<button
type="button"
onClick={() => removeFile(index)}
className="absolute top-1 right-1 bg-red-600 text-white rounded-full p-0.5 opacity-0 group-hover:opacity-100 transition-opacity"
aria-label={`Remove ${file.name}`}
>
<XMarkIcon className="w-4 h-4" />
</button>
</div>
))}
</div>
)}
</div>
);
};
|