Spaces:
Sleeping
Sleeping
| 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; |