Spaces:
Running
Running
import React, { useCallback, useState } from 'react'; | |
import { useDropzone } from 'react-dropzone'; | |
import { motion, AnimatePresence } from 'framer-motion'; | |
import { | |
CloudArrowUpIcon, | |
DocumentIcon, | |
CheckCircleIcon, | |
XCircleIcon, | |
XMarkIcon | |
} from '@heroicons/react/24/outline'; | |
import { uploadDocument } from '../services/api'; | |
import toast from 'react-hot-toast'; | |
const FileUploader = ({ darkMode, onClose }) => { | |
const [uploading, setUploading] = useState(false); | |
const [uploadedFiles, setUploadedFiles] = useState([]); | |
const onDrop = useCallback(async (acceptedFiles) => { | |
setUploading(true); | |
for (const file of acceptedFiles) { | |
try { | |
const formData = new FormData(); | |
formData.append('file', file); | |
await uploadDocument(formData); | |
setUploadedFiles(prev => [...prev, { | |
name: file.name, | |
size: file.size, | |
status: 'success' | |
}]); | |
toast.success(`${file.name} uploaded successfully!`); | |
} catch (error) { | |
setUploadedFiles(prev => [...prev, { | |
name: file.name, | |
size: file.size, | |
status: 'error' | |
}]); | |
toast.error(`Failed to upload ${file.name}`); | |
} | |
} | |
setUploading(false); | |
}, []); | |
const { getRootProps, getInputProps, isDragActive } = useDropzone({ | |
onDrop, | |
accept: { | |
'application/pdf': ['.pdf'], | |
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], | |
'text/plain': ['.txt'] | |
}, | |
multiple: true | |
}); | |
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 parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
}; | |
const removeFile = (index) => { | |
setUploadedFiles(prev => prev.filter((_, i) => i !== index)); | |
}; | |
return ( | |
<div className="space-y-4"> | |
{/* Dropzone */} | |
<motion.div | |
{...getRootProps()} | |
whileHover={{ scale: 1.02 }} | |
whileTap={{ scale: 0.98 }} | |
className={`file-drop-zone border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all ${ | |
isDragActive | |
? darkMode | |
? 'border-primary-400 bg-primary-900/20' | |
: 'border-primary-500 bg-primary-50' | |
: darkMode | |
? 'border-gray-600 hover:border-gray-500 bg-gray-800' | |
: 'border-gray-300 hover:border-gray-400 bg-gray-50' | |
}`} | |
> | |
<input {...getInputProps()} /> | |
<CloudArrowUpIcon className={`w-12 h-12 mx-auto mb-4 ${ | |
isDragActive | |
? darkMode ? 'text-primary-400' : 'text-primary-500' | |
: darkMode ? 'text-gray-400' : 'text-gray-500' | |
}`} /> | |
<h3 className={`text-lg font-semibold mb-2 ${ | |
darkMode ? 'text-white' : 'text-gray-900' | |
}`}> | |
{isDragActive ? 'Drop files here' : 'Upload study materials'} | |
</h3> | |
<p className={`mb-4 ${ | |
darkMode ? 'text-gray-400' : 'text-gray-600' | |
}`}> | |
Drag & drop files here, or click to browse | |
</p> | |
<div className="flex justify-center space-x-2"> | |
<span className={`px-3 py-1 rounded-full text-xs font-medium ${ | |
darkMode | |
? 'bg-blue-900/30 text-blue-400' | |
: 'bg-blue-100 text-blue-700' | |
}`}> | |
</span> | |
<span className={`px-3 py-1 rounded-full text-xs font-medium ${ | |
darkMode | |
? 'bg-green-900/30 text-green-400' | |
: 'bg-green-100 text-green-700' | |
}`}> | |
DOCX | |
</span> | |
<span className={`px-3 py-1 rounded-full text-xs font-medium ${ | |
darkMode | |
? 'bg-purple-900/30 text-purple-400' | |
: 'bg-purple-100 text-purple-700' | |
}`}> | |
TXT | |
</span> | |
</div> | |
</motion.div> | |
{/* Upload Progress */} | |
{uploading && ( | |
<motion.div | |
initial={{ opacity: 0, y: 20 }} | |
animate={{ opacity: 1, y: 0 }} | |
className={`p-4 rounded-lg ${ | |
darkMode ? 'bg-gray-800' : 'bg-gray-100' | |
}`} | |
> | |
<div className="flex items-center space-x-3"> | |
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-primary-500"></div> | |
<span className={`${darkMode ? 'text-gray-300' : 'text-gray-700'}`}> | |
Uploading files... | |
</span> | |
</div> | |
</motion.div> | |
)} | |
{/* Uploaded Files List */} | |
<AnimatePresence> | |
{uploadedFiles.length > 0 && ( | |
<motion.div | |
initial={{ opacity: 0, height: 0 }} | |
animate={{ opacity: 1, height: 'auto' }} | |
exit={{ opacity: 0, height: 0 }} | |
className="space-y-2" | |
> | |
<h4 className={`font-medium ${ | |
darkMode ? 'text-gray-300' : 'text-gray-700' | |
}`}> | |
Uploaded Files | |
</h4> | |
{uploadedFiles.map((file, index) => ( | |
<motion.div | |
key={index} | |
initial={{ opacity: 0, x: -20 }} | |
animate={{ opacity: 1, x: 0 }} | |
className={`flex items-center justify-between p-3 rounded-lg ${ | |
darkMode ? 'bg-gray-800' : 'bg-gray-100' | |
}`} | |
> | |
<div className="flex items-center space-x-3"> | |
<DocumentIcon className={`w-5 h-5 ${ | |
darkMode ? 'text-gray-400' : 'text-gray-500' | |
}`} /> | |
<div> | |
<p className={`text-sm font-medium ${ | |
darkMode ? 'text-white' : 'text-gray-900' | |
}`}> | |
{file.name} | |
</p> | |
<p className={`text-xs ${ | |
darkMode ? 'text-gray-500' : 'text-gray-400' | |
}`}> | |
{formatFileSize(file.size)} | |
</p> | |
</div> | |
</div> | |
<div className="flex items-center space-x-2"> | |
{file.status === 'success' ? ( | |
<CheckCircleIcon className="w-5 h-5 text-green-500" /> | |
) : ( | |
<XCircleIcon className="w-5 h-5 text-red-500" /> | |
)} | |
<button | |
onClick={() => removeFile(index)} | |
className={`p-1 rounded transition-colors ${ | |
darkMode | |
? 'hover:bg-gray-700 text-gray-400' | |
: 'hover:bg-gray-200 text-gray-500' | |
}`} | |
> | |
<XMarkIcon className="w-4 h-4" /> | |
</button> | |
</div> | |
</motion.div> | |
))} | |
</motion.div> | |
)} | |
</AnimatePresence> | |
{/* Close Button */} | |
{onClose && ( | |
<div className="flex justify-end"> | |
<button | |
onClick={onClose} | |
className={`px-4 py-2 rounded-lg font-medium transition-colors ${ | |
darkMode | |
? 'bg-gray-700 hover:bg-gray-600 text-white' | |
: 'bg-gray-200 hover:bg-gray-300 text-gray-700' | |
}`} | |
> | |
Close | |
</button> | |
</div> | |
)} | |
</div> | |
); | |
}; | |
export default FileUploader; |