147 lines
5.5 KiB
TypeScript
147 lines
5.5 KiB
TypeScript
// 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) => {
|
|
if (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>
|
|
);
|
|
};
|