“vinit5112”
Upgrade UI
6f1f94e
raw
history blame
16 kB
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 {
console.log(`Starting upload for file: ${file.name}, size: ${file.size} bytes`);
const formData = new FormData();
formData.append('file', file);
// Check file size limits
const maxSize = 100 * 1024 * 1024; // 100MB
if (file.size > maxSize) {
toast.error(`${file.name} is too large (${formatFileSize(file.size)}). Maximum size is 100MB.`);
setUploadedFiles(prev => [...prev, {
name: file.name,
size: file.size,
status: 'error',
error: 'File too large (max 100MB)'
}]);
continue; // Skip this file
} else if (file.size > 10 * 1024 * 1024) {
toast(`Warning: ${file.name} is large (${formatFileSize(file.size)}). Upload may take time.`, {
icon: '⚠️',
duration: 6000
});
}
await uploadDocument(formData);
setUploadedFiles(prev => [...prev, {
name: file.name,
size: file.size,
status: 'success'
}]);
toast.success(`${file.name} uploaded successfully!`);
console.log(`Successfully uploaded: ${file.name}`);
} catch (error) {
console.error(`Failed to upload ${file.name}:`, error);
setUploadedFiles(prev => [...prev, {
name: file.name,
size: file.size,
status: 'error',
error: error.message
}]);
// More specific error messages
let errorMessage = `Failed to upload ${file.name}`;
if (error.message.includes('Network Error')) {
errorMessage += ': Network connection failed. Check if the server is running.';
} else if (error.message.includes('timeout')) {
errorMessage += ': Upload timed out. File may be too large.';
} else if (error.message.includes('413')) {
errorMessage += ': File too large (max 100MB allowed).';
} else if (error.message.includes('415')) {
errorMessage += ': Unsupported file type.';
} else if (error.message.includes('500')) {
errorMessage += ': Server error. Check server logs.';
} else {
errorMessage += `: ${error.message}`;
}
toast.error(errorMessage);
}
}
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 md:space-y-6">
{/* Dropzone - Mobile optimized */}
<motion.div
{...getRootProps()}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.99 }}
className={`file-drop-zone border-2 border-dashed rounded-2xl md:rounded-3xl p-6 md:p-8 text-center cursor-pointer transition-all touch-manipulation ${
isDragActive
? darkMode
? 'border-primary-400 bg-primary-900/20 scale-[1.02]'
: 'border-primary-500 bg-primary-50 scale-[1.02]'
: darkMode
? 'border-gray-600 hover:border-gray-500 active:border-primary-400 bg-gray-800/50 hover:bg-gray-800'
: 'border-gray-300 hover:border-gray-400 active:border-primary-300 bg-gray-50 hover:bg-gray-100'
} min-h-[200px] md:min-h-[240px] flex flex-col justify-center`}
>
<input {...getInputProps()} />
{/* Upload Icon - Mobile optimized */}
<motion.div
animate={isDragActive ? { y: -5, scale: 1.1 } : { y: 0, scale: 1 }}
transition={{ duration: 0.2 }}
>
<CloudArrowUpIcon className={`w-16 h-16 md:w-20 md:h-20 mx-auto mb-4 md:mb-6 ${
isDragActive
? darkMode ? 'text-primary-400' : 'text-primary-500'
: darkMode ? 'text-gray-400' : 'text-gray-500'
}`} />
</motion.div>
{/* Upload Text - Mobile optimized */}
<h3 className={`text-xl md:text-2xl font-bold mb-3 md:mb-4 ${
darkMode ? 'text-white' : 'text-gray-900'
}`}>
{isDragActive ? 'Drop files here!' : 'Upload study materials'}
</h3>
<p className={`mb-4 md:mb-6 text-base md:text-lg px-2 ${
darkMode ? 'text-gray-400' : 'text-gray-600'
}`}>
{isDragActive
? 'Release to upload your files'
: 'Drag & drop files here, or tap to browse'
}
</p>
{/* File size info - Mobile optimized */}
<p className={`text-sm md:text-base mb-4 md:mb-6 ${
darkMode ? 'text-gray-500' : 'text-gray-500'
}`}>
Maximum file size: <span className="font-semibold">100MB</span>
</p>
{/* File type badges - Mobile optimized */}
<div className="flex flex-wrap justify-center gap-2 md:gap-3">
<motion.span
whileHover={{ scale: 1.05 }}
className={`px-4 py-2 md:px-5 md:py-2.5 rounded-full text-sm md:text-base font-semibold shadow-md ${
darkMode
? 'bg-blue-900/40 text-blue-300 border border-blue-700/50'
: 'bg-blue-100 text-blue-800 border border-blue-200'
}`}
>
📄 PDF
</motion.span>
<motion.span
whileHover={{ scale: 1.05 }}
className={`px-4 py-2 md:px-5 md:py-2.5 rounded-full text-sm md:text-base font-semibold shadow-md ${
darkMode
? 'bg-green-900/40 text-green-300 border border-green-700/50'
: 'bg-green-100 text-green-800 border border-green-200'
}`}
>
📝 DOCX
</motion.span>
<motion.span
whileHover={{ scale: 1.05 }}
className={`px-4 py-2 md:px-5 md:py-2.5 rounded-full text-sm md:text-base font-semibold shadow-md ${
darkMode
? 'bg-purple-900/40 text-purple-300 border border-purple-700/50'
: 'bg-purple-100 text-purple-800 border border-purple-200'
}`}
>
📋 TXT
</motion.span>
</div>
{/* Mobile-specific help text */}
<div className="md:hidden mt-4">
<p className={`text-xs ${
darkMode ? 'text-gray-500' : 'text-gray-400'
}`}>
💡 Tip: You can select multiple files at once
</p>
</div>
</motion.div>
{/* Upload Progress - Mobile optimized */}
<AnimatePresence>
{uploading && (
<motion.div
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -20, scale: 0.95 }}
className={`p-4 md:p-6 rounded-2xl ${
darkMode ? 'bg-gray-800/50 border border-gray-700/50' : 'bg-gray-100/50 border border-gray-200/50'
} backdrop-blur-sm`}
>
<div className="flex items-center space-x-3 md:space-x-4">
<div className="relative">
<div className="animate-spin rounded-full h-6 w-6 md:h-8 md:w-8 border-b-2 border-primary-500"></div>
<div className="absolute inset-0 rounded-full border-2 border-primary-500/20"></div>
</div>
<div>
<span className={`font-medium text-base md:text-lg ${
darkMode ? 'text-gray-200' : 'text-gray-800'
}`}>
Uploading files...
</span>
<p className={`text-sm ${
darkMode ? 'text-gray-400' : 'text-gray-600'
}`}>
Please wait while we process your documents
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Uploaded Files List - Mobile optimized */}
<AnimatePresence>
{uploadedFiles.length > 0 && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="space-y-3 md:space-y-4"
>
<h4 className={`font-bold text-lg md:text-xl ${
darkMode ? 'text-gray-200' : 'text-gray-800'
}`}>
Uploaded Files ({uploadedFiles.length})
</h4>
<div className="space-y-2 md:space-y-3">
{uploadedFiles.map((file, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className={`flex items-center justify-between p-4 md:p-5 rounded-xl md:rounded-2xl transition-all ${
darkMode
? 'bg-gray-800/70 border border-gray-700/50 hover:bg-gray-800'
: 'bg-gray-50 border border-gray-200/50 hover:bg-gray-100'
} shadow-sm hover:shadow-md`}
>
<div className="flex items-center space-x-3 md:space-x-4 flex-1 min-w-0">
{/* File Icon */}
<div className={`p-2 md:p-3 rounded-lg flex-shrink-0 ${
file.status === 'success'
? darkMode
? 'bg-green-900/30 text-green-400'
: 'bg-green-100 text-green-700'
: darkMode
? 'bg-red-900/30 text-red-400'
: 'bg-red-100 text-red-700'
}`}>
<DocumentIcon className="w-5 h-5 md:w-6 md:h-6" />
</div>
{/* File Info */}
<div className="flex-1 min-w-0">
<p className={`font-medium text-sm md:text-base truncate ${
darkMode ? 'text-gray-100' : 'text-gray-900'
}`}>
{file.name}
</p>
<div className="flex items-center space-x-2 md:space-x-3 mt-1">
<p className={`text-xs md:text-sm ${
darkMode ? 'text-gray-400' : 'text-gray-500'
}`}>
{formatFileSize(file.size)}
</p>
{file.status === 'error' && file.error && (
<>
<span className={`text-xs ${
darkMode ? 'text-gray-600' : 'text-gray-400'
}`}></span>
<p className={`text-xs truncate ${
darkMode ? 'text-red-400' : 'text-red-600'
}`}>
{file.error}
</p>
</>
)}
</div>
</div>
</div>
{/* Status and Actions */}
<div className="flex items-center space-x-2 md:space-x-3 flex-shrink-0">
{/* Status Icon */}
{file.status === 'success' ? (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", delay: 0.2 }}
>
<CheckCircleIcon className="w-6 h-6 md:w-7 md:h-7 text-green-500" />
</motion.div>
) : (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", delay: 0.2 }}
>
<XCircleIcon className="w-6 h-6 md:w-7 md:h-7 text-red-500" />
</motion.div>
)}
{/* Remove Button - Larger touch target */}
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => removeFile(index)}
className={`p-2 md:p-2.5 rounded-lg transition-colors touch-manipulation ${
darkMode
? 'hover:bg-gray-700 active:bg-gray-600 text-gray-400 hover:text-gray-200'
: 'hover:bg-gray-200 active:bg-gray-300 text-gray-500 hover:text-gray-700'
}`}
title="Remove file"
>
<XMarkIcon className="w-4 h-4 md:w-5 md:h-5" />
</motion.button>
</div>
</motion.div>
))}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Action Buttons - Mobile optimized */}
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 pt-2">
{/* Upload More Button */}
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
{...getRootProps()}
className={`flex-1 sm:flex-none px-6 py-3 md:py-3.5 rounded-xl md:rounded-2xl font-semibold transition-all touch-manipulation ${
darkMode
? 'bg-primary-600 hover:bg-primary-700 active:bg-primary-800 text-white shadow-lg'
: 'bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white shadow-lg'
} hover:shadow-xl active:shadow-2xl disabled:opacity-50 disabled:cursor-not-allowed`}
disabled={uploading}
>
<input {...getInputProps()} />
{uploading ? 'Uploading...' : 'Upload More Files'}
</motion.button>
{/* Close Button */}
{onClose && (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={onClose}
className={`flex-1 sm:flex-none px-6 py-3 md:py-3.5 rounded-xl md:rounded-2xl font-semibold transition-all touch-manipulation ${
darkMode
? 'bg-gray-700 hover:bg-gray-600 active:bg-gray-500 text-gray-200 shadow-lg'
: 'bg-gray-200 hover:bg-gray-300 active:bg-gray-400 text-gray-700 shadow-lg'
} hover:shadow-xl active:shadow-2xl`}
>
Done
</motion.button>
)}
</div>
</div>
);
};
export default FileUploader;