From 5fbc0a3c95a80a5e43e690cfbede191123b581a8 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 21 May 2026 21:38:54 +0300 Subject: [PATCH] =?UTF-8?q?fix(upload):=20two-phase=20UI=20=E2=80=94=20upl?= =?UTF-8?q?oading=20bar=20then=20'processing'=20spinner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XHR's upload.progress event only tracks browser→server byte transfer. It fires 100% the moment the multipart body has been pushed, but at that point our route handler hasn't even started streaming the files into Appwrite Storage yet. A 200MB upload completes the network phase fast, then sits 30-60 seconds while the server does sequential storage.createFile + tablesDB.createRow per file. The UI was stuck on '100%' the whole time, making it look frozen. Switched the form to a discriminated phase state: idle → uploading (real bytes %) → processing (full bar + spinner) → idle (success/fail) Listening on xhr.upload's own load event flips us to 'processing' the instant the body is done. The outer xhr.load fires when the route handler responds with JSON — that's when we toast + router.refresh(). User now sees: bar fills, then 'Sunucu Appwrite'a yazıyor — büyük dosyalar 30-60 sn sürebilir' message until the response comes back. No more mystery wait. --- .../[jobId]/components/job-files-panel.tsx | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx b/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx index 91d5d36..2b99eeb 100644 --- a/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx +++ b/src/app/(dashboard)/jobs/[jobId]/components/job-files-panel.tsx @@ -64,23 +64,26 @@ export function JobFilesPanel({ const MAX_FILE_BYTES = 200 * 1024 * 1024; const MAX_BATCH_BYTES = 200 * 1024 * 1024; // proxy cap; one large file fills the whole batch +type UploadPhase = "idle" | "uploading" | "processing"; + function UploadForm({ jobId }: { jobId: string }) { const router = useRouter(); const inputRef = useRef(null); const xhrRef = useRef(null); const [selected, setSelected] = useState([]); - const [uploading, setUploading] = useState(false); + const [phase, setPhase] = useState("idle"); const [progress, setProgress] = useState(0); // 0–100 const totalBytes = selected.reduce((s, f) => s + f.size, 0); const overSize = selected.find((f) => f.size > MAX_FILE_BYTES); const overBatch = totalBytes > MAX_BATCH_BYTES; const blocked = Boolean(overSize) || overBatch; + const busy = phase !== "idle"; function reset() { setSelected([]); setProgress(0); - setUploading(false); + setPhase("idle"); if (inputRef.current) inputRef.current.value = ""; } @@ -91,16 +94,24 @@ function UploadForm({ jobId }: { jobId: string }) { const xhr = new XMLHttpRequest(); xhrRef.current = xhr; - setUploading(true); + setPhase("uploading"); setProgress(0); xhr.upload.addEventListener("progress", (e) => { if (!e.lengthComputable) return; - setProgress(Math.round((e.loaded / e.total) * 100)); + const pct = Math.round((e.loaded / e.total) * 100); + setProgress(pct); + }); + + // The browser has finished pushing bytes; the server is now writing to + // Appwrite. Flip to "processing" so the user sees something is still + // happening (large files take 30-60s to land in Storage). + xhr.upload.addEventListener("load", () => { + setPhase("processing"); + setProgress(100); }); xhr.addEventListener("load", () => { - setUploading(false); let payload: { ok?: boolean; uploaded?: number; error?: string } = {}; try { payload = JSON.parse(xhr.responseText); @@ -113,18 +124,19 @@ function UploadForm({ jobId }: { jobId: string }) { router.refresh(); } else { toast.error(payload.error || `Yükleme başarısız (HTTP ${xhr.status}).`); + setPhase("idle"); setProgress(0); } }); xhr.addEventListener("error", () => { - setUploading(false); + setPhase("idle"); setProgress(0); toast.error("Ağ hatası. Tekrar deneyin."); }); xhr.addEventListener("abort", () => { - setUploading(false); + setPhase("idle"); setProgress(0); toast.message("Yükleme iptal edildi."); }); @@ -157,7 +169,7 @@ function UploadForm({ jobId }: { jobId: string }) { variant="outline" size="sm" onClick={() => inputRef.current?.click()} - disabled={uploading} + disabled={busy} > Dosya seç @@ -181,10 +193,15 @@ function UploadForm({ jobId }: { jobId: string }) { "Tarama (STL/OBJ), görsel veya PDF — max 200MB / dosya" )} - {uploading ? ( + {phase === "uploading" ? ( + ) : phase === "processing" ? ( + ) : (