316 lines
20 KiB
TypeScript
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;
|