diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..7a45a68 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Vite + React + TS + Litecloud
diff --git a/frontend/src/components/FileComponent.tsx b/frontend/src/components/FileComponent.tsx index 582e7b1..9b2b1a8 100644 --- a/frontend/src/components/FileComponent.tsx +++ b/frontend/src/components/FileComponent.tsx @@ -26,9 +26,11 @@ export const FilesComponent: React.FC = ({ isDarkMode }) => const [shareDuration, setShareDuration] = useState(7); // durata di default 7 giorni const [previewFile, setPreviewFile] = useState<{ url: string; name: string } | null>(null); const [previewBlobUrl, setPreviewBlobUrl] = useState(null); + const [uploadProgress, setUploadProgress] = useState(null); + const [uploadFileName, setUploadFileName] = useState(null); + const [uploadStartTime, setUploadStartTime] = useState(null); const fileInputRef = useRef(null); - const dropdownRef = useRef(null); // Recupera il token solo una volta al mount o quando cambia useEffect(() => { @@ -55,13 +57,21 @@ export const FilesComponent: React.FC = ({ isDarkMode }) => if (token) fetchFiles(); }, [token]); + // Fix gestione click fuori dal dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + // Se il dropdown è aperto e il click NON è dentro un menu dropdown, chiudi + const dropdowns = document.querySelectorAll('.custom-dropdown'); + let clickedInside = false; + dropdowns.forEach(dropdown => { + if (dropdown.contains(event.target as Node)) { + clickedInside = true; + } + }); + if (!clickedInside) { setOpenDropdown(null); } }; - document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); @@ -73,32 +83,51 @@ export const FilesComponent: React.FC = ({ isDarkMode }) => try { setLoading(true); setError(null); + setUploadProgress(0); + setUploadFileName(file.name); + setUploadStartTime(Date.now()); const formData = new FormData(); formData.append('file', file); formData.append('filename', file.name); if (!token) throw new Error('No auth token found'); - const response = await fetch('/api/upload', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - }, - body: formData, - }); - - if (response.ok) { - setSuccess('File uploaded successfully!'); - fetchFiles(); - } else { - throw new Error('Upload failed'); - } + const xhr = new XMLHttpRequest(); + xhr.open('POST', '/api/upload'); + xhr.setRequestHeader('Authorization', `Bearer ${token}`); + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + setUploadProgress(Math.round((e.loaded / e.total) * 100)); + } + }; + xhr.onload = async () => { + if (xhr.status >= 200 && xhr.status < 300) { + setSuccess('File uploaded successfully!'); + fetchFiles(); + } else { + setError('Upload failed'); + } + setLoading(false); + setUploadProgress(null); + setUploadFileName(null); + setUploadStartTime(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + }; + xhr.onerror = () => { + setError('Failed to upload file'); + setLoading(false); + setUploadProgress(null); + setUploadFileName(null); + setUploadStartTime(null); + if (fileInputRef.current) fileInputRef.current.value = ''; + }; + xhr.send(formData); } catch (err) { setError('Failed to upload file - ' + (err instanceof Error ? err.message : 'Unknown error')); - } finally { setLoading(false); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } + setUploadProgress(null); + setUploadFileName(null); + setUploadStartTime(null); + if (fileInputRef.current) fileInputRef.current.value = ''; } }; @@ -227,6 +256,18 @@ export const FilesComponent: React.FC = ({ isDarkMode }) => // Funzione per ottenere la URL del file (puoi adattare se serve fetch protetto) + // Calcola il tempo rimanente stimato + const getEstimatedTimeLeft = () => { + if (uploadProgress === null || uploadProgress === 0 || !uploadStartTime) return null; + const now = Date.now(); + const elapsed = (now - uploadStartTime) / 1000; // in secondi + const estimatedTotal = elapsed / (uploadProgress / 100); + const remaining = estimatedTotal - elapsed; + if (!isFinite(remaining) || remaining < 0) return null; + if (remaining < 60) return `${Math.round(remaining)}s`; + return `${Math.floor(remaining / 60)}m ${Math.round(remaining % 60)}s`; + }; + return (
{/* Success/Error Messages */} @@ -306,7 +347,7 @@ export const FilesComponent: React.FC = ({ isDarkMode }) => {formatFileSize(file.size)} {formatDate(file.uploaded_at)} - +
)} + + {/* Modal/progressbar upload */} + {uploadProgress !== null && uploadFileName && ( +
+
+
{uploadFileName}
+
+ + + + {uploadProgress}% + +
+
+ {getEstimatedTimeLeft() ? `~${getEstimatedTimeLeft()} left` : ''} +
+
+
+ )} ); }; \ No newline at end of file diff --git a/frontend/src/components/ShareComponent.tsx b/frontend/src/components/ShareComponent.tsx index 3907d9f..d0cfbb0 100644 --- a/frontend/src/components/ShareComponent.tsx +++ b/frontend/src/components/ShareComponent.tsx @@ -84,7 +84,7 @@ export const SharesComponent: React.FC = ({ isDarkMode }) const copyToClipboard = async (shareId: string) => { try { - const shareUrl = `${window.location.origin}/shared/${shareId}`; + const shareUrl = `${window.location.origin}/api/shared/${shareId}`; await navigator.clipboard.writeText(shareUrl); setSuccess('Share link copied to clipboard!'); } catch (err) { diff --git a/frontend/src/index.css b/frontend/src/index.css index 9696592..f4cb3c8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -364,10 +364,12 @@ body, html, #root { /* Sidebar Title */ .sidebar-title { - font-size: 1.5rem; + font-size: 2rem; font-weight: 700; - margin: 0 0 2rem 0; + letter-spacing: 0.05em; + margin-bottom: 2rem; text-align: center; + transition: color 0.3s; } /* Sidebar Navigation */ @@ -929,4 +931,89 @@ body, html, #root { padding: 0.5rem; font-size: 0.875rem; } +} + +/* Upload progress popout styles */ +.upload-progress-popout { + position: fixed; + right: 32px; + bottom: 32px; + z-index: 2000; + background: rgba(34, 34, 34, 0.95); + color: #fff; + border-radius: 18px; + box-shadow: 0 4px 24px rgba(0,0,0,0.18); + padding: 18px 28px 18px 18px; + min-width: 220px; + min-height: 110px; + display: flex; + align-items: center; + gap: 18px; + animation: popout-fadein 0.3s; +} +.upload-progress-content { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; +} +.upload-progress-filename { + font-size: 1.05rem; + font-weight: 500; + margin-bottom: 2px; + text-align: center; + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.upload-progress-bar-container { + width: 48px; + height: 48px; + position: relative; + margin-bottom: 2px; +} +.upload-progress-svg { + width: 48px; + height: 48px; +} +.upload-progress-bg { + fill: none; + stroke: #444; + stroke-width: 4; +} +.upload-progress-bar { + fill: none; + stroke: #4ade80; + stroke-width: 4; + stroke-linecap: round; + transition: stroke-dashoffset 0.3s; +} +.upload-progress-text { + font-size: 0.95rem; + font-weight: 600; + fill: #fff; + dominant-baseline: middle; +} +.upload-progress-eta { + font-size: 0.92rem; + color: #b3e6c7; + margin-top: 2px; + text-align: center; +} +@media (max-width: 600px) { + .upload-progress-popout { + right: 8px; + bottom: 8px; + min-width: 150px; + padding: 10px 12px 10px 10px; + } + .upload-progress-bar-container, .upload-progress-svg { + width: 36px; + height: 36px; + } +} +@keyframes popout-fadein { + from { opacity: 0; transform: translateY(30px) scale(0.98); } + to { opacity: 1; transform: translateY(0) scale(1); } } \ No newline at end of file