feat: Implement file upload progress tracking and enhance share link functionality

This commit is contained in:
Mercurio 2025-06-07 23:54:32 +02:00
parent 2cf335c5ef
commit 18f1cd150b
4 changed files with 180 additions and 26 deletions

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Litecloud</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View file

@ -26,9 +26,11 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
const [shareDuration, setShareDuration] = useState(7); // durata di default 7 giorni const [shareDuration, setShareDuration] = useState(7); // durata di default 7 giorni
const [previewFile, setPreviewFile] = useState<{ url: string; name: string } | null>(null); const [previewFile, setPreviewFile] = useState<{ url: string; name: string } | null>(null);
const [previewBlobUrl, setPreviewBlobUrl] = useState<string | null>(null); const [previewBlobUrl, setPreviewBlobUrl] = useState<string | null>(null);
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
const [uploadFileName, setUploadFileName] = useState<string | null>(null);
const [uploadStartTime, setUploadStartTime] = useState<number | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const dropdownRef = useRef<HTMLTableDataCellElement | null>(null);
// Recupera il token solo una volta al mount o quando cambia // Recupera il token solo una volta al mount o quando cambia
useEffect(() => { useEffect(() => {
@ -55,13 +57,21 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
if (token) fetchFiles(); if (token) fetchFiles();
}, [token]); }, [token]);
// Fix gestione click fuori dal dropdown
useEffect(() => { useEffect(() => {
const handleClickOutside = (event: MouseEvent) => { 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); setOpenDropdown(null);
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, []); }, []);
@ -73,32 +83,51 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
setUploadProgress(0);
setUploadFileName(file.name);
setUploadStartTime(Date.now());
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('filename', file.name); formData.append('filename', file.name);
if (!token) throw new Error('No auth token found'); if (!token) throw new Error('No auth token found');
const response = await fetch('/api/upload', { const xhr = new XMLHttpRequest();
method: 'POST', xhr.open('POST', '/api/upload');
headers: { xhr.setRequestHeader('Authorization', `Bearer ${token}`);
'Authorization': `Bearer ${token}`, xhr.upload.onprogress = (e) => {
}, if (e.lengthComputable) {
body: formData, setUploadProgress(Math.round((e.loaded / e.total) * 100));
}); }
};
if (response.ok) { xhr.onload = async () => {
setSuccess('File uploaded successfully!'); if (xhr.status >= 200 && xhr.status < 300) {
fetchFiles(); setSuccess('File uploaded successfully!');
} else { fetchFiles();
throw new Error('Upload failed'); } 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) { } catch (err) {
setError('Failed to upload file - ' + (err instanceof Error ? err.message : 'Unknown error')); setError('Failed to upload file - ' + (err instanceof Error ? err.message : 'Unknown error'));
} finally {
setLoading(false); setLoading(false);
if (fileInputRef.current) { setUploadProgress(null);
fileInputRef.current.value = ''; setUploadFileName(null);
} setUploadStartTime(null);
if (fileInputRef.current) fileInputRef.current.value = '';
} }
}; };
@ -227,6 +256,18 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
// Funzione per ottenere la URL del file (puoi adattare se serve fetch protetto) // 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 ( return (
<div className="component-container"> <div className="component-container">
{/* Success/Error Messages */} {/* Success/Error Messages */}
@ -306,7 +347,7 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
</td> </td>
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>{formatFileSize(file.size)}</td> <td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>{formatFileSize(file.size)}</td>
<td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>{formatDate(file.uploaded_at)}</td> <td className={`table-cell ${isDarkMode ? 'text-gray-300' : 'text-gray-500'}`}>{formatDate(file.uploaded_at)}</td>
<td className="table-cell relative actions-cell" ref={dropdownRef}> <td className="table-cell relative actions-cell">
<button <button
onClick={() => setOpenDropdown(openDropdown === file.id ? null : file.id)} onClick={() => setOpenDropdown(openDropdown === file.id ? null : file.id)}
className={`actions-button ellipsis-button ${isDarkMode ? 'actions-button-dark' : 'actions-button-light'}`} className={`actions-button ellipsis-button ${isDarkMode ? 'actions-button-dark' : 'actions-button-light'}`}
@ -366,6 +407,32 @@ export const FilesComponent: React.FC<FilesComponentProps> = ({ isDarkMode }) =>
</div> </div>
)} )}
</div> </div>
{/* Modal/progressbar upload */}
{uploadProgress !== null && uploadFileName && (
<div className="upload-progress-popout">
<div className="upload-progress-content">
<div className="upload-progress-filename">{uploadFileName}</div>
<div className="upload-progress-bar-container">
<svg className="upload-progress-svg" viewBox="0 0 40 40">
<circle className="upload-progress-bg" cx="20" cy="20" r="18" />
<circle
className="upload-progress-bar"
cx="20"
cy="20"
r="18"
strokeDasharray={2 * Math.PI * 18}
strokeDashoffset={2 * Math.PI * 18 * (1 - uploadProgress / 100)}
/>
<text x="50%" y="54%" textAnchor="middle" className="upload-progress-text">{uploadProgress}%</text>
</svg>
</div>
<div className="upload-progress-eta">
{getEstimatedTimeLeft() ? `~${getEstimatedTimeLeft()} left` : ''}
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View file

@ -84,7 +84,7 @@ export const SharesComponent: React.FC<SharesComponentProps> = ({ isDarkMode })
const copyToClipboard = async (shareId: string) => { const copyToClipboard = async (shareId: string) => {
try { try {
const shareUrl = `${window.location.origin}/shared/${shareId}`; const shareUrl = `${window.location.origin}/api/shared/${shareId}`;
await navigator.clipboard.writeText(shareUrl); await navigator.clipboard.writeText(shareUrl);
setSuccess('Share link copied to clipboard!'); setSuccess('Share link copied to clipboard!');
} catch (err) { } catch (err) {

View file

@ -364,10 +364,12 @@ body, html, #root {
/* Sidebar Title */ /* Sidebar Title */
.sidebar-title { .sidebar-title {
font-size: 1.5rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
margin: 0 0 2rem 0; letter-spacing: 0.05em;
margin-bottom: 2rem;
text-align: center; text-align: center;
transition: color 0.3s;
} }
/* Sidebar Navigation */ /* Sidebar Navigation */
@ -929,4 +931,89 @@ body, html, #root {
padding: 0.5rem; padding: 0.5rem;
font-size: 0.875rem; 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); }
} }