Skip to main content

Upload Progress

Dropup provides detailed progress tracking for all uploads.

Progress Tracking

Individual File Progress

Each file has a progress property (0-100):

const { files } = useDropup({
upload: { url: '/api/upload' },
});

{files.map(file => (
<div key={file.id}>
<span>{file.name}</span>
{file.status === 'uploading' && (
<progress value={file.progress} max={100} />
)}
</div>
))}

Overall Progress

Track progress across all files:

const { state } = useDropup();

// state.progress - Average progress of all files (0-100)
// state.isUploading - Whether any upload is in progress

<div>
{state.isUploading && (
<p>Uploading: {state.progress}%</p>
)}
</div>

Progress Callbacks

Per-File Progress

useDropup({
upload: { url: '/api/upload' },
onUploadProgress: (file, progress) => {
console.log(`${file.name}: ${progress}%`);
},
});

Upload Events

useDropup({
upload: { url: '/api/upload' },

onUploadStart: (file) => {
console.log(`Started: ${file.name}`);
},

onUploadProgress: (file, progress) => {
console.log(`Progress: ${file.name} - ${progress}%`);
},

onUploadComplete: (file, response) => {
console.log(`Complete: ${file.name}`);
console.log(`URL: ${file.uploadedUrl}`);
},

onUploadError: (file, error) => {
console.error(`Failed: ${file.name}`, error);
},

onAllComplete: (files) => {
console.log(`All done! ${files.length} files uploaded`);
},
});

Building Progress UI

Simple Progress Bar

function ProgressBar({ value }: { value: number }) {
return (
<div style={{
width: '100%',
height: 8,
backgroundColor: '#e0e0e0',
borderRadius: 4,
}}>
<div style={{
width: `${value}%`,
height: '100%',
backgroundColor: '#4caf50',
borderRadius: 4,
transition: 'width 0.2s',
}} />
</div>
);
}

// Usage
{file.status === 'uploading' && (
<ProgressBar value={file.progress} />
)}

Circular Progress

function CircularProgress({ value, size = 40 }: { value: number; size?: number }) {
const strokeWidth = 4;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const offset = circumference - (value / 100) * circumference;

return (
<svg width={size} height={size}>
<circle
stroke="#e0e0e0"
fill="transparent"
strokeWidth={strokeWidth}
r={radius}
cx={size / 2}
cy={size / 2}
/>
<circle
stroke="#4caf50"
fill="transparent"
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
r={radius}
cx={size / 2}
cy={size / 2}
style={{
transform: 'rotate(-90deg)',
transformOrigin: '50% 50%',
transition: 'stroke-dashoffset 0.2s',
}}
/>
<text
x="50%"
y="50%"
textAnchor="middle"
dy=".3em"
fontSize={size / 4}
>
{value}%
</text>
</svg>
);
}

Complete File Upload Card

function FileUploadCard({ file }: { file: DropupFile }) {
const getStatusIcon = () => {
switch (file.status) {
case 'idle': return '⏳';
case 'uploading': return '📤';
case 'complete': return '✅';
case 'error': return '❌';
case 'paused': return '⏸️';
default: return '📄';
}
};

const getStatusColor = () => {
switch (file.status) {
case 'uploading': return '#2196f3';
case 'complete': return '#4caf50';
case 'error': return '#f44336';
default: return '#9e9e9e';
}
};

return (
<div style={{
display: 'flex',
alignItems: 'center',
padding: 12,
border: '1px solid #e0e0e0',
borderRadius: 8,
marginBottom: 8,
}}>
{/* Preview or Icon */}
{file.preview ? (
<img
src={file.preview}
alt=""
style={{ width: 48, height: 48, objectFit: 'cover', borderRadius: 4 }}
/>
) : (
<span style={{ fontSize: 32 }}>{getStatusIcon()}</span>
)}

{/* File Info */}
<div style={{ flex: 1, marginLeft: 12 }}>
<div style={{ fontWeight: 500 }}>{file.name}</div>
<div style={{ fontSize: 12, color: '#666' }}>
{(file.size / 1024).toFixed(1)} KB
</div>

{/* Progress Bar */}
{file.status === 'uploading' && (
<div style={{
marginTop: 4,
height: 4,
backgroundColor: '#e0e0e0',
borderRadius: 2,
}}>
<div style={{
width: `${file.progress}%`,
height: '100%',
backgroundColor: getStatusColor(),
borderRadius: 2,
}} />
</div>
)}

{/* Error Message */}
{file.error && (
<div style={{ color: '#f44336', fontSize: 12, marginTop: 4 }}>
{file.error.message}
</div>
)}
</div>

{/* Status Badge */}
<span style={{
padding: '4px 8px',
borderRadius: 4,
fontSize: 12,
color: 'white',
backgroundColor: getStatusColor(),
}}>
{file.status === 'uploading' ? `${file.progress}%` : file.status}
</span>
</div>
);
}

Status Counts

Track upload status counts:

const { files, state } = useDropup();

const counts = {
total: files.length,
pending: files.filter(f => f.status === 'idle').length,
uploading: files.filter(f => f.status === 'uploading').length,
complete: files.filter(f => f.status === 'complete').length,
failed: files.filter(f => f.status === 'error').length,
};

// Display
<div>
<p>Total: {counts.total}</p>
<p>Uploading: {counts.uploading}</p>
<p>Complete: {counts.complete}</p>
<p>Failed: {counts.failed}</p>
</div>

Upload Speed & ETA

Calculate upload speed and estimated time:

import { useState, useRef, useEffect } from 'react';

function useUploadSpeed(file: DropupFile) {
const [speed, setSpeed] = useState(0);
const [eta, setEta] = useState<number | null>(null);
const lastProgress = useRef(0);
const lastTime = useRef(Date.now());

useEffect(() => {
if (file.status !== 'uploading') return;

const interval = setInterval(() => {
const now = Date.now();
const elapsed = (now - lastTime.current) / 1000;
const progressDiff = file.progress - lastProgress.current;
const bytesDiff = (progressDiff / 100) * file.size;

if (elapsed > 0) {
const currentSpeed = bytesDiff / elapsed;
setSpeed(currentSpeed);

const remaining = file.size * (1 - file.progress / 100);
if (currentSpeed > 0) {
setEta(remaining / currentSpeed);
}
}

lastProgress.current = file.progress;
lastTime.current = now;
}, 1000);

return () => clearInterval(interval);
}, [file.status, file.progress, file.size]);

return { speed, eta };
}

// Usage
function FileProgress({ file }: { file: DropupFile }) {
const { speed, eta } = useUploadSpeed(file);

return (
<div>
<p>Speed: {formatSpeed(speed)}</p>
<p>ETA: {formatTime(eta)}</p>
</div>
);
}

function formatSpeed(bytesPerSecond: number): string {
if (bytesPerSecond < 1024) return `${bytesPerSecond.toFixed(0)} B/s`;
if (bytesPerSecond < 1024 * 1024) return `${(bytesPerSecond / 1024).toFixed(1)} KB/s`;
return `${(bytesPerSecond / 1024 / 1024).toFixed(1)} MB/s`;
}

function formatTime(seconds: number | null): string {
if (!seconds) return '--';
if (seconds < 60) return `${Math.ceil(seconds)}s`;
return `${Math.ceil(seconds / 60)}m`;
}