Browse Source

Merge branch 'staging' of https://git.nwhco.ir/sina_sajjadi/marriage into staging

staging
Ali Alavi 4 days ago
parent
commit
83291d7229
  1. 29
      src/components/Componentes/question-file.tsx
  2. 79
      src/components/Componentes/question-photo.tsx

29
src/components/Componentes/question-file.tsx

@ -164,6 +164,20 @@ function isImageFile(
return /\.(jpg|jpeg|png|webp|gif|svg|bmp|avif)$/i.test(nameToCheck); 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 = { export type UploadedDoc = {
url: string; url: string;
name: string; name: string;
@ -335,15 +349,12 @@ export function QuestionFile({
previewUrl: resolved, previewUrl: resolved,
}); });
} else if (f?.base64) { } 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,
});
try {
const fileObj = base64ToFile(f.base64, f.name ?? "document");
uploadTmpMediaMutation.mutate(fileObj); uploadTmpMediaMutation.mutate(fileObj);
});
} catch (e) {
console.error("Failed to decode base64 file:", e);
}
} }
} }
@ -618,7 +629,7 @@ export function QuestionFile({
{isImg && doc.previewUrl && !hasThumbError ? ( {isImg && doc.previewUrl && !hasThumbError ? (
<img <img
src={doc.previewUrl} src={doc.previewUrl}
alt=""
alt={doc.name}
onError={() => { onError={() => {
setImgErrorSet((prev) => new Set(prev).add(index)); setImgErrorSet((prev) => new Set(prev).add(index));
}} }}

79
src/components/Componentes/question-photo.tsx

@ -17,6 +17,20 @@ type QuestionPhotoProps = {
disabled?: boolean; 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({ export function QuestionPhoto({
question, question,
description, description,
@ -37,7 +51,6 @@ export function QuestionPhoto({
? question.extras.options.join(",") ? question.extras.options.join(",")
: "image/*"; : "image/*";
const { getAnswerValue, setAnswerValue, registerPendingUpload } = const { getAnswerValue, setAnswerValue, registerPendingUpload } =
useQuestionAnswers(); useQuestionAnswers();
const storedValue = getAnswerValue(question); const storedValue = getAnswerValue(question);
@ -61,6 +74,7 @@ export function QuestionPhoto({
const handleUploadFailure = useCallback( const handleUploadFailure = useCallback(
(errorMessage?: string) => { (errorMessage?: string) => {
setIsUploading(false); setIsUploading(false);
isInitiatorRef.current = false;
if (activeBlobUrlRef.current) { if (activeBlobUrlRef.current) {
URL.revokeObjectURL(activeBlobUrlRef.current); URL.revokeObjectURL(activeBlobUrlRef.current);
activeBlobUrlRef.current = null; activeBlobUrlRef.current = null;
@ -92,15 +106,11 @@ export function QuestionPhoto({
if (fileInputRef.current) { if (fileInputRef.current) {
fileInputRef.current.value = ""; 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( setToastMessage(
t["Failed to load image. Please try uploading again."] ?? t["Failed to load image. Please try uploading again."] ??
"Failed to load image. Please try uploading again.", "Failed to load image. Please try uploading again.",
); );
}
}, [question, setAnswerValue, t, isUploading]);
}, [question, setAnswerValue, t]);
const uploadTmpMediaMutation = useUploadTmpMediaMutation({ const uploadTmpMediaMutation = useUploadTmpMediaMutation({
onSuccess: (response) => { onSuccess: (response) => {
@ -114,6 +124,7 @@ export function QuestionPhoto({
handleUploadFailure(); handleUploadFailure();
} }
setIsUploading(false); setIsUploading(false);
isInitiatorRef.current = false;
}, },
onError: (error) => { onError: (error) => {
console.error("Photo upload error:", error); console.error("Photo upload error:", error);
@ -140,57 +151,70 @@ export function QuestionPhoto({
setIsUploading(true); setIsUploading(true);
setHasImageError(false); setHasImageError(false);
setToastMessage(null); setToastMessage(null);
if (event.data?.files?.[0]?.base64) {
const b64 = event.data.files[0].base64;
setLocalPreviewUrl(b64);
}
break; break;
case "completed": { case "completed": {
setIsUploading(false);
isInitiatorRef.current = false;
const file = event.data?.files?.[0]; const file = event.data?.files?.[0];
const remoteUrl = const remoteUrl =
file?.path || file?.path ||
file?.url || file?.url ||
(file?.data?.path as string | undefined) || (file?.data?.path as string | undefined) ||
(file?.data?.url as string | undefined); (file?.data?.url as string | undefined);
if (remoteUrl) { if (remoteUrl) {
setIsUploading(false);
isInitiatorRef.current = false;
setAnswerValue(question, remoteUrl); setAnswerValue(question, remoteUrl);
setLocalPreviewUrl((prev) => prev || remoteUrl); setLocalPreviewUrl((prev) => prev || remoteUrl);
setHasImageError(false); setHasImageError(false);
pendingDeferredRef.current?.resolve(remoteUrl); pendingDeferredRef.current?.resolve(remoteUrl);
pendingDeferredRef.current = null; pendingDeferredRef.current = null;
} else if (file?.base64) { } 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);
})
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) => { .then((res) => {
if (res?.path) { if (res?.path) {
setAnswerValue(question, res.path); setAnswerValue(question, res.path);
setLocalPreviewUrl((prev) => prev || res.path);
setHasImageError(false); setHasImageError(false);
if (!activeBlobUrlRef.current) {
setLocalPreviewUrl(res.path);
}
pendingDeferredRef.current?.resolve(res.path); pendingDeferredRef.current?.resolve(res.path);
} else { } else {
handleUploadFailure(); handleUploadFailure();
pendingDeferredRef.current?.resolve(); pendingDeferredRef.current?.resolve();
} }
setIsUploading(false);
isInitiatorRef.current = false;
pendingDeferredRef.current = null; pendingDeferredRef.current = null;
}) })
.catch((err) => { .catch((err) => {
console.error("Flutter photo upload error:", err);
console.error("Flutter photo upload to server error:", err);
handleUploadFailure(); handleUploadFailure();
pendingDeferredRef.current?.reject(err); pendingDeferredRef.current?.reject(err);
pendingDeferredRef.current = null; pendingDeferredRef.current = null;
}); });
registerPendingUpload?.(question.id, uploadPromise); registerPendingUpload?.(question.id, uploadPromise);
} catch (convErr) {
console.error("Failed to decode base64 photo:", convErr);
handleUploadFailure();
pendingDeferredRef.current?.reject(convErr);
pendingDeferredRef.current = null;
}
} else { } else {
setIsUploading(false);
isInitiatorRef.current = false;
pendingDeferredRef.current?.resolve(); pendingDeferredRef.current?.resolve();
pendingDeferredRef.current = null; pendingDeferredRef.current = null;
} }
@ -208,7 +232,7 @@ export function QuestionPhoto({
case "failed": case "failed":
setIsUploading(false); setIsUploading(false);
isInitiatorRef.current = false; isInitiatorRef.current = false;
handleUploadFailure();
handleUploadFailure(event.message || event.data?.message);
pendingDeferredRef.current?.reject(new Error("Upload failed")); pendingDeferredRef.current?.reject(new Error("Upload failed"));
pendingDeferredRef.current = null; pendingDeferredRef.current = null;
break; break;
@ -265,6 +289,9 @@ export function QuestionPhoto({
allowedExtensions: allowedExtensions:
extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"],
maxBytes: 10_485_760, // 10 MB maxBytes: 10_485_760, // 10 MB
maxWidth: 1024,
maxHeight: 1024,
imageQuality: 85,
title: title:
typeof question.title === "string" ? question.title : "Upload Photo", typeof question.title === "string" ? question.title : "Upload Photo",
}); });
@ -374,7 +401,7 @@ export function QuestionPhoto({
className="sr-only" className="sr-only"
/> />
<label <label
htmlFor={inputId}
htmlFor={isInFlutterWebView() ? undefined : inputId}
onClick={handleLabelClick} onClick={handleLabelClick}
className="flex w-full cursor-pointer flex-col items-center" className="flex w-full cursor-pointer flex-col items-center"
> >

Loading…
Cancel
Save