feat(upload): show progress bar in drop zone during upload
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 2m23s
CI / Backend Unit Tests (push) Successful in 2m13s
CI / E2E Tests (push) Failing after 29m59s

Replaces fetch with XMLHttpRequest to get upload progress events.
The drop zone shows a filling progress bar and percentage while
files are uploading, then reverts to the normal hint when done.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #74.
This commit is contained in:
Marcel
2026-03-26 11:37:28 +01:00
parent d078ad8224
commit 1ea84e4dc8

View File

@@ -25,6 +25,7 @@ let isDragging = $state(false);
let windowDragging = $state(false); let windowDragging = $state(false);
let dragCounter = 0; let dragCounter = 0;
let isUploading = $state(false); let isUploading = $state(false);
let uploadProgress = $state(0);
let uploadMessages = $state<{ text: string; isError: boolean; link?: string }[]>([]); let uploadMessages = $state<{ text: string; isError: boolean; link?: string }[]>([]);
let fileInput: HTMLInputElement; let fileInput: HTMLInputElement;
@@ -85,19 +86,26 @@ async function uploadFiles(files: File[]) {
} }
isUploading = true; isUploading = true;
uploadProgress = 0;
try { try {
const formData = new FormData(); const formData = new FormData();
for (const file of valid) { for (const file of valid) {
formData.append('files', file); formData.append('files', file);
} }
const res = await fetch('/api/documents/quick-upload', { const { ok, body } = await new Promise<{ ok: boolean; body: string }>((resolve, reject) => {
method: 'POST', const xhr = new XMLHttpRequest();
body: formData xhr.open('POST', '/api/documents/quick-upload');
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) uploadProgress = Math.round((e.loaded / e.total) * 100);
});
xhr.addEventListener('load', () => resolve({ ok: xhr.status < 300, body: xhr.responseText }));
xhr.addEventListener('error', () => reject(new Error('Network error')));
xhr.send(formData);
}); });
if (res.ok) { if (ok) {
const result = await res.json(); const result = JSON.parse(body);
if (result.created?.length > 0) { if (result.created?.length > 0) {
messages.push({ text: m.upload_success({ count: result.created.length }), isError: false }); messages.push({ text: m.upload_success({ count: result.created.length }), isError: false });
} }
@@ -122,6 +130,7 @@ async function uploadFiles(files: File[]) {
} }
} finally { } finally {
isUploading = false; isUploading = false;
uploadProgress = 0;
uploadMessages = messages; uploadMessages = messages;
} }
} }
@@ -372,10 +381,20 @@ $effect(() => {
<polyline points="17 8 12 3 7 8" /> <polyline points="17 8 12 3 7 8" />
<line x1="12" y1="3" x2="12" y2="15" /> <line x1="12" y1="3" x2="12" y2="15" />
</svg> </svg>
<span class="font-sans font-medium"> {#if isUploading}
{isUploading ? '…' : m.upload_drop_hint()} <div class="flex w-48 flex-col items-center gap-1">
</span> <div class="h-1.5 w-full overflow-hidden rounded-full bg-ink/10">
<span class="font-sans text-xs text-ink-3">{m.upload_accepted_types()}</span> <div
class="h-full rounded-full bg-primary transition-all duration-200"
style="width: {uploadProgress}%"
></div>
</div>
<span class="font-sans text-xs text-ink-3">{uploadProgress}%</span>
</div>
{:else}
<span class="font-sans font-medium">{m.upload_drop_hint()}</span>
<span class="font-sans text-xs text-ink-3">{m.upload_accepted_types()}</span>
{/if}
</div> </div>
{#if uploadMessages.length > 0} {#if uploadMessages.length > 0}