|
|
@ -234,9 +234,17 @@ export function QuestionFile({ |
|
|
parseInitialDocs(storedValue), |
|
|
parseInitialDocs(storedValue), |
|
|
); |
|
|
); |
|
|
const [isUploading, setIsUploading] = useState(false); |
|
|
const [isUploading, setIsUploading] = useState(false); |
|
|
|
|
|
const [uploadProgress, setUploadProgress] = useState<number | null>(null); |
|
|
const isInitiatorRef = useRef(false); |
|
|
const isInitiatorRef = useRef(false); |
|
|
const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
const fileInputRef = useRef<HTMLInputElement>(null); |
|
|
|
|
|
|
|
|
|
|
|
// Sync state when answers load asynchronously from server/cache
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
if (storedValue !== undefined && storedValue !== null) { |
|
|
|
|
|
setUploadedDocs(parseInitialDocs(storedValue)); |
|
|
|
|
|
} |
|
|
|
|
|
}, [storedValue, parseInitialDocs]); |
|
|
|
|
|
|
|
|
const acceptedFiles = (question.extras?.options ?? []) |
|
|
const acceptedFiles = (question.extras?.options ?? []) |
|
|
.map((option) => option.replace(/^\./, "")) |
|
|
.map((option) => option.replace(/^\./, "")) |
|
|
.join(", "); |
|
|
.join(", "); |
|
|
@ -262,10 +270,12 @@ export function QuestionFile({ |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
setIsUploading(false); |
|
|
setIsUploading(false); |
|
|
|
|
|
setUploadProgress(null); |
|
|
}, |
|
|
}, |
|
|
onError: (error) => { |
|
|
onError: (error) => { |
|
|
console.error("File upload error:", error); |
|
|
console.error("File upload error:", error); |
|
|
setIsUploading(false); |
|
|
setIsUploading(false); |
|
|
|
|
|
setUploadProgress(null); |
|
|
}, |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
@ -284,11 +294,20 @@ export function QuestionFile({ |
|
|
case "picking": |
|
|
case "picking": |
|
|
break; |
|
|
break; |
|
|
case "picked": |
|
|
case "picked": |
|
|
case "progress": |
|
|
|
|
|
setIsUploading(true); |
|
|
setIsUploading(true); |
|
|
|
|
|
setUploadProgress(0); |
|
|
|
|
|
break; |
|
|
|
|
|
case "progress": { |
|
|
|
|
|
setIsUploading(true); |
|
|
|
|
|
const p = (event.data as any)?.progress; |
|
|
|
|
|
if (typeof p === "number") { |
|
|
|
|
|
setUploadProgress(Math.round(p)); |
|
|
|
|
|
} |
|
|
break; |
|
|
break; |
|
|
|
|
|
} |
|
|
case "completed": { |
|
|
case "completed": { |
|
|
setIsUploading(false); |
|
|
setIsUploading(false); |
|
|
|
|
|
setUploadProgress(null); |
|
|
isInitiatorRef.current = false; |
|
|
isInitiatorRef.current = false; |
|
|
const incomingFiles = event.data?.files || []; |
|
|
const incomingFiles = event.data?.files || []; |
|
|
const newDocs: UploadedDoc[] = []; |
|
|
const newDocs: UploadedDoc[] = []; |
|
|
@ -341,6 +360,7 @@ export function QuestionFile({ |
|
|
case "cancelled": |
|
|
case "cancelled": |
|
|
case "failed": |
|
|
case "failed": |
|
|
setIsUploading(false); |
|
|
setIsUploading(false); |
|
|
|
|
|
setUploadProgress(null); |
|
|
isInitiatorRef.current = false; |
|
|
isInitiatorRef.current = false; |
|
|
break; |
|
|
break; |
|
|
} |
|
|
} |
|
|
@ -380,12 +400,13 @@ export function QuestionFile({ |
|
|
if (!files || files.length === 0) return; |
|
|
if (!files || files.length === 0) return; |
|
|
|
|
|
|
|
|
setIsUploading(true); |
|
|
setIsUploading(true); |
|
|
|
|
|
setUploadProgress(0); |
|
|
const newDocs: UploadedDoc[] = []; |
|
|
const newDocs: UploadedDoc[] = []; |
|
|
|
|
|
|
|
|
for (let i = 0; i < files.length; i++) { |
|
|
for (let i = 0; i < files.length; i++) { |
|
|
const file = files[i]; |
|
|
const file = files[i]; |
|
|
try { |
|
|
try { |
|
|
const res = await uploadTmpMedia(file); |
|
|
|
|
|
|
|
|
const res = await uploadTmpMedia(file, (p) => setUploadProgress(p)); |
|
|
if (res.path) { |
|
|
if (res.path) { |
|
|
const resolved = res.path.startsWith("http") |
|
|
const resolved = res.path.startsWith("http") |
|
|
? res.path |
|
|
? res.path |
|
|
@ -402,6 +423,7 @@ export function QuestionFile({ |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
setIsUploading(false); |
|
|
setIsUploading(false); |
|
|
|
|
|
setUploadProgress(null); |
|
|
|
|
|
|
|
|
if (newDocs.length > 0) { |
|
|
if (newDocs.length > 0) { |
|
|
setUploadedDocs((prev) => { |
|
|
setUploadedDocs((prev) => { |
|
|
@ -484,10 +506,10 @@ export function QuestionFile({ |
|
|
: undefined |
|
|
: undefined |
|
|
} |
|
|
} |
|
|
className={[ |
|
|
className={[ |
|
|
"relative flex w-full flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-all duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777] active:border-[#111111] overflow-hidden p-4", |
|
|
|
|
|
|
|
|
"relative flex w-full flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 hover:border-[#6F6F6F] overflow-hidden select-none", |
|
|
hasDocuments |
|
|
hasDocuments |
|
|
? "min-h-[156px]" |
|
|
|
|
|
: "aspect-[727/330] min-h-[156px] cursor-pointer", |
|
|
|
|
|
|
|
|
? "min-h-[156px] p-3.5" |
|
|
|
|
|
: "aspect-[727/330] min-h-[156px] cursor-pointer p-4", |
|
|
].join(" ")} |
|
|
].join(" ")} |
|
|
> |
|
|
> |
|
|
{hasDocuments ? ( |
|
|
{hasDocuments ? ( |
|
|
@ -608,12 +630,21 @@ export function QuestionFile({ |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
{/* Smooth Circular Loading Spinner Overlay (Matching Flutter ProfileAvatar) */} |
|
|
|
|
|
|
|
|
{/* Smooth Circular Loading Spinner Overlay with Upload Progress */} |
|
|
{isPending && ( |
|
|
{isPending && ( |
|
|
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center rounded-[29px] bg-black/40 backdrop-blur-[1px] transition-all duration-200 gap-2"> |
|
|
|
|
|
<div className="h-8 w-8 animate-spin rounded-full border-[2.5px] border-white border-t-transparent shadow-sm" /> |
|
|
|
|
|
|
|
|
<div className="absolute inset-0 z-30 flex flex-col items-center justify-center rounded-[29px] bg-black/50 backdrop-blur-[1px] transition-all duration-200 gap-2"> |
|
|
|
|
|
<div className="relative flex items-center justify-center"> |
|
|
|
|
|
<div className="h-10 w-10 animate-spin rounded-full border-[2.5px] border-white border-t-transparent shadow-sm" /> |
|
|
|
|
|
{uploadProgress !== null && uploadProgress > 0 && ( |
|
|
|
|
|
<span className="absolute text-[10px] font-bold text-white leading-none"> |
|
|
|
|
|
{uploadProgress}% |
|
|
|
|
|
</span> |
|
|
|
|
|
)} |
|
|
|
|
|
</div> |
|
|
<span className="text-xs font-semibold text-white"> |
|
|
<span className="text-xs font-semibold text-white"> |
|
|
{t.uploading} |
|
|
|
|
|
|
|
|
{uploadProgress !== null && uploadProgress > 0 |
|
|
|
|
|
? `${t.uploading} (${uploadProgress}%)` |
|
|
|
|
|
: t.uploading} |
|
|
</span> |
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
|