Files
docqube_frontend/src/features/components/upload/UploadModal.tsx
T
2026-09-08 18:35:07 +05:30

316 lines
20 KiB
TypeScript

import React, { useState, useCallback, useRef, useEffect } from 'react';
import { X, Upload, File, ShieldAlert, FileText, ImageIcon, Film, Music, Loader2, Cloud } from 'lucide-react';
// import { listChildren, preScanFile, uploadFile } from '@/services/drive';
// import { ConfirmDialog } from '@/components/ui';
// import { useToast } from '@/components/ui/ToastProvider';
// import axiosInstance from '@/services/axios';
// import { useGoogleDrivePicker } from '@/hooks/useGoogleDrivePicker';
// import GoogleDriveIcon from '@/assets/Google_Drive_icon_(2020).svg';
interface UploadFileState {
file: File;
progress: number;
status: 'pending' | 'scanning' | 'ready' | 'uploading' | 'compressing' | 'processing' | 'completed' | 'error';
error?: string;
virusName?: string;
wasCompressed?: boolean;
isSignedPdf?: boolean;
}
interface UploadModalProps {
isOpen: boolean;
onClose: () => void;
folderId: number;
onUploadComplete: () => void;
}
const getFileIcon = (fileName: string) => {
const ext = fileName.split('.').pop()?.toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(ext || '')) return ImageIcon;
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext || '')) return Film;
if (['mp3', 'wav', 'ogg'].includes(ext || '')) return Music;
if (['pdf', 'doc', 'docx', 'txt', 'rtf'].includes(ext || '')) return FileText;
return File;
};
const sanitizeFilename = (name: string) => {
if (!name) return "file";
let namePart = name;
let extPart = "";
if (name.includes(".")) {
const lastDot = name.lastIndexOf(".");
namePart = name.substring(0, lastDot);
extPart = name.substring(lastDot);
}
const sanitized = namePart.replace(/[^\w.-]/g, "_").replace(/_+/g, "_").replace(/^[_.]+|[_.]+$/g, "");
const finalName = sanitized || "file";
return extPart ? `${finalName}${extPart}` : finalName;
};
import { useUpload } from '@/context/UploadContext';
export const UploadModal: React.FC<UploadModalProps> = ({ isOpen, onClose, folderId, onUploadComplete }) => {
const { uploads, startUploads, isUploading, clearUploads } = useUpload();
const [isDragging, setIsDragging] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
// const { error: showToastError } = useToast();
const activeCount = uploads.filter(u => ['pending', 'scanning', 'ready', 'uploading', 'compressing', 'processing'].includes(u.status)).length;
const hasVirus = uploads.some(u => u.status === 'error');
// const { openDrivePicker, isDownloading } = useGoogleDrivePicker({
// onFileSelected: (file) => {
// startUploads([file], folderId, onUploadComplete);
// onClose();
// },
// onError: (err) => showToastError(err)
// });
const handleStartUploads = (files: File[]) => {
startUploads(files, folderId, onUploadComplete);
onClose(); // Auto-close to show the progress panel
};
const handleFinish = async () => {
onClose();
};
const onDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) handleStartUploads(files);
}, [handleStartUploads]);
const onFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files || []);
if (files.length > 0) handleStartUploads(files);
if (fileInputRef.current) fileInputRef.current.value = '';
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-[150] flex items-center justify-center p-3 sm:p-4 animate-fade-in">
{/* Backdrop */}
<div
className="absolute inset-0 bg-slate-950/40 backdrop-blur-md transition-all duration-500"
onClick={onClose}
/>
{/* Modal Card */}
<div
className="relative w-full max-w-lg bg-white rounded-3xl shadow-[0_32px_64px_-12px_rgba(0,0,0,0.14)] overflow-hidden flex flex-col transform transition-all duration-300 scale-100 border border-slate-100/50 my-auto"
style={{ maxHeight: 'calc(100vh - 2rem)' }}
>
{/* Header */}
<div className="flex items-center justify-between px-6 py-3.5 border-b border-slate-100 bg-white/80 backdrop-blur-md z-10 shrink-0">
<div>
<h3 className="text-base font-bold text-slate-900 tracking-tight">Upload Files</h3>
<div className="flex items-center gap-2 mt-0.5">
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest">Secure Document Gateway</p>
<span className="w-1 h-1 bg-slate-300 rounded-full" />
<p className="text-[10px] text-blue-500 font-bold uppercase tracking-widest">Up to 10 files Max 30MB each</p>
</div>
</div>
<button
onClick={onClose}
className="group p-1.5 rounded-xl transition-all duration-300 hover:bg-slate-50 text-slate-400 hover:text-slate-900"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-6 py-3.5 space-y-3 min-h-0">
{/* Dropzone */}
<div
onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }}
onDragLeave={() => setIsDragging(false)}
onDrop={onDrop}
className={`group p-4 rounded-2xl border-2 border-dashed transition-all duration-300 flex flex-col items-center justify-center gap-2 relative overflow-hidden
${isDragging
? 'border-[#3b82f6] bg-blue-50/30 scale-[0.99] shadow-inner'
: 'border-[#e2e8f0] bg-white hover:bg-slate-50/50 hover:border-[#cbd5e1]'}
`}
>
<div className="flex justify-center -space-x-2 mb-0.5">
<div className="w-8 h-10 bg-blue-100 rounded border-2 border-white flex flex-col items-center justify-center rotate-[-15deg] shadow-sm">
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
<div className="w-3 h-0.5 bg-blue-300"></div>
</div>
<div className="w-9 h-11 bg-blue-500 rounded border-2 border-white flex flex-col items-center justify-center z-10 shadow-md">
<div className="w-5 h-0.5 bg-blue-200 mb-1"></div>
<div className="w-5 h-0.5 bg-blue-200 mb-1"></div>
<div className="w-3.5 h-0.5 bg-blue-200"></div>
</div>
<div className="w-8 h-10 bg-blue-100 rounded border-2 border-white flex flex-col items-center justify-center rotate-[15deg] shadow-sm">
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
<div className="w-4 h-0.5 bg-blue-300 mb-1"></div>
<div className="w-3 h-0.5 bg-blue-300"></div>
</div>
</div>
<div className="text-center space-y-0.5">
<h3 className="text-sm font-bold text-slate-800">Drag and drop files here</h3>
<p className="text-[11px] text-slate-400 font-medium max-w-xs leading-relaxed">Supported formats: .pdf, .doc, .docx, .xls, .xlsx, .txt, .csv, .jpg, .jpeg, .png, .gif, .webp, .svg</p>
<p className="text-[10px] text-slate-400 font-semibold">Maximum file size: 30MB</p>
</div>
<div className="w-full relative flex items-center justify-center my-0.5">
<div className="absolute w-full border-t border-dashed border-slate-200"></div>
<span className="bg-white px-3 text-[10px] uppercase font-bold text-slate-400 relative z-10">or</span>
</div>
<div className="flex gap-3 w-full justify-center">
{/* My Computer */}
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="flex items-center justify-center gap-2 px-4 py-2 rounded-xl border border-slate-200/80 bg-slate-50 hover:bg-blue-50 hover:border-blue-300 transition-all duration-200 group cursor-pointer shadow-xs"
>
<div className="w-6 h-6 flex items-center justify-center group-hover:scale-110 transition-transform">
<svg viewBox="0 0 24 24" fill="none" className="w-5 h-5 text-blue-600" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</div>
<span className="text-xs font-bold text-slate-700 group-hover:text-blue-600 transition-colors">Browse from Computer</span>
</button>
</div>
<input
type="file"
multiple
accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,.csv,.jpg,.jpeg,.png,.gif,.webp,.svg"
className="hidden"
ref={fileInputRef}
onChange={onFileSelect}
/>
</div>
{/* Upload Queue Section */}
{uploads.length > 0 && (
<div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center justify-between px-2">
<h4 className="text-[9px] font-black text-slate-400 uppercase tracking-[0.2em]">Live Queue {uploads.length}</h4>
{activeCount === 0 && (
<button
onClick={() => clearUploads()}
className="text-[9px] font-black text-blue-600 hover:text-blue-700 uppercase tracking-widest transition-colors py-1 px-2 hover:bg-blue-50 rounded-lg"
>
Reset All
</button>
)}
</div>
<div className="space-y-2.5">
{uploads.map((upload, idx) => {
const Icon = getFileIcon(upload.file.name);
return (
<div key={`${upload.file.name}-${idx}`} className="group p-3.5 bg-white border border-slate-100 rounded-2xl shadow-sm hover:shadow-xl hover:shadow-slate-100/50 hover:border-blue-100 transition-all duration-300 flex items-center gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center shrink-0 border border-slate-50 transition-colors duration-500
${upload.status === 'completed' || upload.status === 'ready' ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-50 text-slate-400 group-hover:bg-blue-50 group-hover:text-blue-600'}
`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1.5">
<p className="text-xs font-bold text-slate-800 truncate pr-6 leading-tight">
{upload.file.name}
</p>
<div className="shrink-0">
{upload.status === 'scanning' && <span className="text-[8px] font-black text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full animate-pulse tracking-tight">VIRUS SCANNING...</span>}
{upload.status === 'ready' && <span className="text-[8px] font-black text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full tracking-tight">SCAN COMPLETED</span>}
{upload.status === 'uploading' && <span className="text-[8px] font-black text-blue-600 bg-blue-50 px-2 py-0.5 rounded-full tracking-tight">UPLOADING {upload.progress}%...</span>}
{upload.status === 'compressing' && <span className="text-[8px] font-black text-purple-600 bg-purple-50 px-2 py-0.5 rounded-full animate-pulse tracking-tight">COMPRESSING...</span>}
{upload.status === 'processing' && <span className="text-[8px] font-black text-purple-600 bg-purple-50 px-2 py-0.5 rounded-full animate-pulse tracking-tight">PROCESSING...</span>}
{upload.status === 'completed' && <span className="text-[8px] font-black text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full flex items-center gap-1 tracking-tight">COMPLETED</span>}
{upload.status === 'error' && <span className="text-[8px] font-black text-rose-600 bg-rose-50 px-2 py-0.5 rounded-full flex items-center gap-1 tracking-tight">REJECTED</span>}
</div>
</div>
{upload.status === 'error' ? (
<div className="flex items-center gap-1.5 px-1">
<ShieldAlert className="w-3 h-3 text-rose-500" />
<p className="text-[9px] text-rose-600 font-bold tracking-tight">
{upload.error || 'Blocked'}
</p>
</div>
) : (
<>
<div className="relative w-full h-1 bg-slate-50 rounded-full overflow-hidden">
<div
className={`absolute top-0 left-0 h-full transition-all duration-1000 ease-[cubic-bezier(0.23,1,0.32,1)] rounded-full
${upload.status === 'completed' || upload.status === 'ready' ? 'bg-emerald-400' : upload.status === 'compressing' || upload.status === 'processing' ? 'bg-purple-500' : 'bg-blue-500'}
`}
style={{ width: `${upload.status === 'completed' || upload.status === 'ready' ? 100 : (upload.status === 'scanning' ? 40 : upload.status === 'uploading' ? upload.progress : upload.status === 'compressing' || upload.status === 'processing' ? 99 : 0)}%` }}
/>
</div>
{(upload.status === 'ready' || upload.status === 'completed') && (
<p className="text-[8px] text-emerald-600 font-bold mt-1 px-1 flex items-center gap-1">
<span className="w-1 h-1 bg-emerald-500 rounded-full animate-pulse" />
No virus detected
</p>
)}
</>
)}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="px-6 py-3.5 border-t border-slate-100 bg-slate-50/50 flex items-center justify-between shrink-0">
<div className="flex flex-col">
<span className="text-[9px] text-slate-400 font-black uppercase tracking-widest mb-0.5">Status Report</span>
<div className="flex items-center gap-1.5">
<div className={`w-1.5 h-1.5 rounded-full transition-colors duration-500 ${activeCount > 0 ? 'bg-blue-500 animate-pulse' : (uploads.length > 0 ? 'bg-emerald-500' : 'bg-slate-300')}`} />
<span className="text-[11px] text-slate-700 font-bold">
{uploads.length === 0 ? 'Awaiting Input' : `${uploads.filter(u => u.status === 'completed' || u.status === 'ready').length} of ${uploads.length} Secured`}
</span>
</div>
</div>
<button
onClick={() => uploads.length > 0 && handleFinish()}
disabled={activeCount > 0 || hasVirus}
className={`px-5 py-2 text-[10px] font-black rounded-xl transition-all duration-500 tracking-[0.1em] flex items-center gap-2
${(uploads.length > 0 && activeCount === 0)
? 'bg-emerald-600 text-white hover:bg-emerald-700 hover:-translate-y-0.5 shadow-lg shadow-emerald-200'
: activeCount > 0
? 'bg-slate-100 text-slate-300 cursor-not-allowed scale-95'
: hasVirus
? 'bg-rose-100 text-rose-400 cursor-not-allowed border border-rose-200'
: uploads.length > 0
? 'bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-200 hover:-translate-y-0.5'
: 'bg-slate-200 text-slate-500 hover:bg-slate-300'}
`}
>
{activeCount > 0 && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
{uploads.length > 0 && activeCount === 0
? 'FINISH'
: uploads.some(u => u.status === 'uploading')
? 'UPLOADING...'
: uploads.some(u => u.status === 'scanning')
? 'SECURI-SCANNING...'
: hasVirus
? 'VIRUS DETECTED'
: 'AWAITING FILES'}
</button>
</div>
</div>
</div>
);
};
export default UploadModal;