| import React, { useCallback, useState } from 'react'; | |
| import { Upload, FileText, Image as ImageIcon, X } from 'lucide-react'; | |
| const FileUpload = ({ onFilesSelected, disabled }) => { | |
| const [dragActive, setDragActive] = useState(false); | |
| const [selectedFiles, setSelectedFiles] = useState([]); | |
| const handleDrag = useCallback((e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (e.type === 'dragenter' || e.type === 'dragover') { | |
| setDragActive(true); | |
| } else if (e.type === 'dragleave') { | |
| setDragActive(false); | |
| } | |
| }, []); | |
| const handleDrop = useCallback((e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setDragActive(false); | |
| if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { | |
| handleFiles(Array.from(e.dataTransfer.files)); | |
| } | |
| }, []); | |
| const handleFiles = (files) => { | |
| const validFiles = files.filter(file => { | |
| const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'application/pdf']; | |
| return validTypes.includes(file.type); | |
| }); | |
| if (validFiles.length !== files.length) { | |
| alert('Some files were skipped. Only images (JPEG, PNG, GIF, WEBP) and PDF files are supported.'); | |
| } | |
| setSelectedFiles(prev => [...prev, ...validFiles]); | |
| }; | |
| const handleFileInput = (e) => { | |
| if (e.target.files && e.target.files.length > 0) { | |
| handleFiles(Array.from(e.target.files)); | |
| } | |
| }; | |
| const removeFile = (index) => { | |
| setSelectedFiles(prev => prev.filter((_, i) => i !== index)); | |
| }; | |
| const handleProcess = () => { | |
| if (selectedFiles.length > 0) { | |
| onFilesSelected(selectedFiles); | |
| setSelectedFiles([]); | |
| } | |
| }; | |
| const formatFileSize = (bytes) => { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]; | |
| }; | |
| return ( | |
| <div className="w-full space-y-4"> | |
| {/* Upload Zone */} | |
| <div | |
| className={`upload-zone relative border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${ | |
| dragActive ? 'drag-active' : '' | |
| } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`} | |
| onDragEnter={handleDrag} | |
| onDragLeave={handleDrag} | |
| onDragOver={handleDrag} | |
| onDrop={handleDrop} | |
| onClick={() => !disabled && document.getElementById('fileInput').click()} | |
| > | |
| <input | |
| id="fileInput" | |
| type="file" | |
| multiple | |
| accept="image/*,.pdf" | |
| onChange={handleFileInput} | |
| className="hidden" | |
| disabled={disabled} | |
| /> | |
| <div className="flex flex-col items-center justify-center space-y-4"> | |
| <div className="p-4 bg-primary-50 rounded-full"> | |
| <Upload className="w-12 h-12 text-primary-500" /> | |
| </div> | |
| <div> | |
| <p className="text-lg font-semibold text-gray-700 mb-1"> | |
| Drop your files here or click to browse | |
| </p> | |
| <p className="text-sm text-gray-500"> | |
| Support for images (JPEG, PNG, GIF, WEBP) and PDF files | |
| </p> | |
| </div> | |
| <div className="flex items-center gap-2 text-xs text-gray-400"> | |
| <ImageIcon className="w-4 h-4" /> | |
| <span>Multiple files supported</span> | |
| <span>•</span> | |
| <FileText className="w-4 h-4" /> | |
| <span>PDF to image conversion</span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Selected Files List */} | |
| {selectedFiles.length > 0 && ( | |
| <div className="space-y-3"> | |
| <div className="flex items-center justify-between"> | |
| <h3 className="text-sm font-semibold text-gray-700"> | |
| Selected Files ({selectedFiles.length}) | |
| </h3> | |
| <button | |
| onClick={() => setSelectedFiles([])} | |
| className="text-xs text-red-500 hover:text-red-700 font-medium" | |
| > | |
| Clear All | |
| </button> | |
| </div> | |
| <div className="space-y-2 max-h-64 overflow-y-auto"> | |
| {selectedFiles.map((file, index) => ( | |
| <div | |
| key={index} | |
| className="flex items-center justify-between bg-white p-3 rounded-lg border border-gray-200 hover:border-primary-300 transition-colors" | |
| > | |
| <div className="flex items-center space-x-3 flex-1 min-w-0"> | |
| <div className="flex-shrink-0"> | |
| {file.type === 'application/pdf' ? ( | |
| <FileText className="w-8 h-8 text-red-500" /> | |
| ) : ( | |
| <ImageIcon className="w-8 h-8 text-blue-500" /> | |
| )} | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <p className="text-sm font-medium text-gray-900 truncate"> | |
| {file.name} | |
| </p> | |
| <p className="text-xs text-gray-500"> | |
| {formatFileSize(file.size)} | |
| {file.type === 'application/pdf' && ' • Will be converted to images'} | |
| </p> | |
| </div> | |
| </div> | |
| <button | |
| onClick={() => removeFile(index)} | |
| className="ml-2 p-1 hover:bg-red-50 rounded-full transition-colors" | |
| title="Remove file" | |
| > | |
| <X className="w-5 h-5 text-red-500" /> | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| <button | |
| onClick={handleProcess} | |
| disabled={disabled} | |
| className="w-full bg-gradient-to-r from-primary-500 to-primary-600 text-white font-semibold py-3 px-6 rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center space-x-2" | |
| > | |
| <Upload className="w-5 h-5" /> | |
| <span>Process {selectedFiles.length} File{selectedFiles.length > 1 ? 's' : ''}</span> | |
| </button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default FileUpload; | |