diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index b662781..a089c80 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -164,6 +164,20 @@ function isImageFile( return /\.(jpg|jpeg|png|webp|gif|svg|bmp|avif)$/i.test(nameToCheck); } +function base64ToFile(dataUrl: string, filename = "document"): File { + const parts = dataUrl.split(","); + const mimeMatch = parts[0]?.match(/:(.*?);/); + const mime = mimeMatch ? mimeMatch[1] : "application/octet-stream"; + const base64Data = parts[1] || parts[0]; + const binaryString = atob(base64Data); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new File([bytes], filename, { type: mime }); +} + export type UploadedDoc = { url: string; name: string; @@ -335,15 +349,12 @@ export function QuestionFile({ previewUrl: resolved, }); } else if (f?.base64) { - const b64 = f.base64; - fetch(b64) - .then((res) => res.blob()) - .then((blob) => { - const fileObj = new File([blob], f.name ?? "document", { - type: blob.type, - }); - uploadTmpMediaMutation.mutate(fileObj); - }); + try { + const fileObj = base64ToFile(f.base64, f.name ?? "document"); + uploadTmpMediaMutation.mutate(fileObj); + } catch (e) { + console.error("Failed to decode base64 file:", e); + } } } @@ -618,7 +629,7 @@ export function QuestionFile({ {isImg && doc.previewUrl && !hasThumbError ? ( { setImgErrorSet((prev) => new Set(prev).add(index)); }} diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 1d84377..8292006 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -17,6 +17,20 @@ type QuestionPhotoProps = { disabled?: boolean; }; +function base64ToFile(dataUrl: string, filename = "photo.jpg"): File { + const parts = dataUrl.split(","); + const mimeMatch = parts[0]?.match(/:(.*?);/); + const mime = mimeMatch ? mimeMatch[1] : "image/jpeg"; + const base64Data = parts[1] || parts[0]; + const binaryString = atob(base64Data); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return new File([bytes], filename, { type: mime }); +} + export function QuestionPhoto({ question, description, @@ -37,7 +51,6 @@ export function QuestionPhoto({ ? question.extras.options.join(",") : "image/*"; - const { getAnswerValue, setAnswerValue, registerPendingUpload } = useQuestionAnswers(); const storedValue = getAnswerValue(question); @@ -61,6 +74,7 @@ export function QuestionPhoto({ const handleUploadFailure = useCallback( (errorMessage?: string) => { setIsUploading(false); + isInitiatorRef.current = false; if (activeBlobUrlRef.current) { URL.revokeObjectURL(activeBlobUrlRef.current); activeBlobUrlRef.current = null; @@ -92,15 +106,11 @@ export function QuestionPhoto({ if (fileInputRef.current) { fileInputRef.current.value = ""; } - // Only show the toast if the user was actively uploading; - // stale stored URLs from previous sessions fail silently. - if (isUploading || isInitiatorRef.current) { - setToastMessage( - t["Failed to load image. Please try uploading again."] ?? - "Failed to load image. Please try uploading again.", - ); - } - }, [question, setAnswerValue, t, isUploading]); + setToastMessage( + t["Failed to load image. Please try uploading again."] ?? + "Failed to load image. Please try uploading again.", + ); + }, [question, setAnswerValue, t]); const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { @@ -114,6 +124,7 @@ export function QuestionPhoto({ handleUploadFailure(); } setIsUploading(false); + isInitiatorRef.current = false; }, onError: (error) => { console.error("Photo upload error:", error); @@ -140,57 +151,70 @@ export function QuestionPhoto({ setIsUploading(true); setHasImageError(false); setToastMessage(null); - if (event.data?.files?.[0]?.base64) { - const b64 = event.data.files[0].base64; - setLocalPreviewUrl(b64); - } break; case "completed": { - setIsUploading(false); - isInitiatorRef.current = false; const file = event.data?.files?.[0]; const remoteUrl = file?.path || file?.url || (file?.data?.path as string | undefined) || (file?.data?.url as string | undefined); + if (remoteUrl) { + setIsUploading(false); + isInitiatorRef.current = false; setAnswerValue(question, remoteUrl); setLocalPreviewUrl((prev) => prev || remoteUrl); setHasImageError(false); pendingDeferredRef.current?.resolve(remoteUrl); pendingDeferredRef.current = null; } else if (file?.base64) { - const b64 = file.base64; - setLocalPreviewUrl(b64); - const uploadPromise = fetch(b64) - .then((res) => res.blob()) - .then((blob) => { - const f = new File([blob], file.name ?? "photo.jpg", { - type: blob.type || "image/jpeg", - }); - return uploadTmpMediaMutation.mutateAsync(f); - }) - .then((res) => { - if (res?.path) { - setAnswerValue(question, res.path); - setLocalPreviewUrl((prev) => prev || res.path); - setHasImageError(false); - pendingDeferredRef.current?.resolve(res.path); - } else { + setIsUploading(true); + setHasImageError(false); + setToastMessage(null); + try { + const f = base64ToFile(file.base64, file.name ?? "photo.jpg"); + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + } + const objectUrl = URL.createObjectURL(f); + activeBlobUrlRef.current = objectUrl; + setLocalPreviewUrl(objectUrl); + + const uploadPromise = uploadTmpMediaMutation + .mutateAsync(f) + .then((res) => { + if (res?.path) { + setAnswerValue(question, res.path); + setHasImageError(false); + if (!activeBlobUrlRef.current) { + setLocalPreviewUrl(res.path); + } + pendingDeferredRef.current?.resolve(res.path); + } else { + handleUploadFailure(); + pendingDeferredRef.current?.resolve(); + } + setIsUploading(false); + isInitiatorRef.current = false; + pendingDeferredRef.current = null; + }) + .catch((err) => { + console.error("Flutter photo upload to server error:", err); handleUploadFailure(); - pendingDeferredRef.current?.resolve(); - } - pendingDeferredRef.current = null; - }) - .catch((err) => { - console.error("Flutter photo upload error:", err); - handleUploadFailure(); - pendingDeferredRef.current?.reject(err); - pendingDeferredRef.current = null; - }); - registerPendingUpload?.(question.id, uploadPromise); + pendingDeferredRef.current?.reject(err); + pendingDeferredRef.current = null; + }); + registerPendingUpload?.(question.id, uploadPromise); + } catch (convErr) { + console.error("Failed to decode base64 photo:", convErr); + handleUploadFailure(); + pendingDeferredRef.current?.reject(convErr); + pendingDeferredRef.current = null; + } } else { + setIsUploading(false); + isInitiatorRef.current = false; pendingDeferredRef.current?.resolve(); pendingDeferredRef.current = null; } @@ -208,7 +232,7 @@ export function QuestionPhoto({ case "failed": setIsUploading(false); isInitiatorRef.current = false; - handleUploadFailure(); + handleUploadFailure(event.message || event.data?.message); pendingDeferredRef.current?.reject(new Error("Upload failed")); pendingDeferredRef.current = null; break; @@ -265,6 +289,9 @@ export function QuestionPhoto({ allowedExtensions: extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], maxBytes: 10_485_760, // 10 MB + maxWidth: 1024, + maxHeight: 1024, + imageQuality: 85, title: typeof question.title === "string" ? question.title : "Upload Photo", }); @@ -374,7 +401,7 @@ export function QuestionPhoto({ className="sr-only" />