feat(upload): bulk drag-and-drop upload on home page (#66) #74

Merged
marcel merged 13 commits from feature/66-bulk-upload-drop-zone into main 2026-03-26 12:00:09 +01:00
Showing only changes of commit 1ea84e4dc8 - Show all commits

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}