|
|
@ -9,6 +9,7 @@ import { |
|
|
} from "@/hooks/marriage/use-upload-tmp-media"; |
|
|
} from "@/hooks/marriage/use-upload-tmp-media"; |
|
|
import { getApiRequestUrl } from "@/lib/http"; |
|
|
import { getApiRequestUrl } from "@/lib/http"; |
|
|
import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; |
|
|
import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; |
|
|
|
|
|
import { useI18n } from "@/translations/provider"; |
|
|
import { useQuestionAnswers } from "./question-answer-storage"; |
|
|
import { useQuestionAnswers } from "./question-answer-storage"; |
|
|
import QuestionTitle from "./question-title"; |
|
|
import QuestionTitle from "./question-title"; |
|
|
import { LoadingSkeleton } from "./loading-skeleton"; |
|
|
import { LoadingSkeleton } from "./loading-skeleton"; |
|
|
@ -18,6 +19,107 @@ type QuestionFileProps = { |
|
|
disabled?: boolean; |
|
|
disabled?: boolean; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
const FILE_I18N: Record< |
|
|
|
|
|
string, |
|
|
|
|
|
{ |
|
|
|
|
|
uploadPrompt: string; |
|
|
|
|
|
addMore: string; |
|
|
|
|
|
uploading: string; |
|
|
|
|
|
remove: string; |
|
|
|
|
|
uploadFailed: string; |
|
|
|
|
|
defaultDoc: string; |
|
|
|
|
|
} |
|
|
|
|
|
> = { |
|
|
|
|
|
fa: { |
|
|
|
|
|
uploadPrompt: "بارگذاری مدارک (کارت شناسایی، پاسپورت و...)", |
|
|
|
|
|
addMore: "افزودن مدرک دیگر", |
|
|
|
|
|
uploading: "در حال آپلود در تمپ...", |
|
|
|
|
|
remove: "حذف مدرک", |
|
|
|
|
|
uploadFailed: "خطا در آپلود. لطفاً دوباره تلاش کنید.", |
|
|
|
|
|
defaultDoc: "مدرک", |
|
|
|
|
|
}, |
|
|
|
|
|
ar: { |
|
|
|
|
|
uploadPrompt: "تحميل المستندات (بطاقة الهوية، جواز السفر...)", |
|
|
|
|
|
addMore: "إضافة مستند آخر", |
|
|
|
|
|
uploading: "جاري التحميل...", |
|
|
|
|
|
remove: "حذف المستند", |
|
|
|
|
|
uploadFailed: "فشل التحميل. يرجى المحاولة مرة أخرى.", |
|
|
|
|
|
defaultDoc: "مستند", |
|
|
|
|
|
}, |
|
|
|
|
|
en: { |
|
|
|
|
|
uploadPrompt: "Upload documents (ID card, passport...)", |
|
|
|
|
|
addMore: "Add another document", |
|
|
|
|
|
uploading: "Uploading to temporary storage...", |
|
|
|
|
|
remove: "Remove document", |
|
|
|
|
|
uploadFailed: "Upload failed. Please try again.", |
|
|
|
|
|
defaultDoc: "document", |
|
|
|
|
|
}, |
|
|
|
|
|
tr: { |
|
|
|
|
|
uploadPrompt: "Belgeleri yükleyin (Kimlik kartı, pasaport...)", |
|
|
|
|
|
addMore: "Başka bir belge ekle", |
|
|
|
|
|
uploading: "Yükleniyor...", |
|
|
|
|
|
remove: "Belgeyi kaldır", |
|
|
|
|
|
uploadFailed: "Yükleme başarısız oldu. Lütfen tekrar deneyin.", |
|
|
|
|
|
defaultDoc: "belge", |
|
|
|
|
|
}, |
|
|
|
|
|
ur: { |
|
|
|
|
|
uploadPrompt: "دستاویزات اپ لوڈ کریں (شناختی کارڈ، پاسپورٹ...)", |
|
|
|
|
|
addMore: "ایک اور دستاویز شامل کریں", |
|
|
|
|
|
uploading: "اپ لوڈ ہو رہا ہے...", |
|
|
|
|
|
remove: "دستاویز حذف کریں", |
|
|
|
|
|
uploadFailed: "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔", |
|
|
|
|
|
defaultDoc: "دستاویز", |
|
|
|
|
|
}, |
|
|
|
|
|
ru: { |
|
|
|
|
|
uploadPrompt: "Загрузить документы (удостоверение личности, паспорт...)", |
|
|
|
|
|
addMore: "Добавить еще документ", |
|
|
|
|
|
uploading: "Загрузка...", |
|
|
|
|
|
remove: "Удалить документ", |
|
|
|
|
|
uploadFailed: "Ошибка загрузки. Пожалуйста, попробуйте снова.", |
|
|
|
|
|
defaultDoc: "документ", |
|
|
|
|
|
}, |
|
|
|
|
|
de: { |
|
|
|
|
|
uploadPrompt: "Dokumente hochladen (Personalausweis, Reisepass...)", |
|
|
|
|
|
addMore: "Weitere Datei hinzufügen", |
|
|
|
|
|
uploading: "Wird hochgeladen...", |
|
|
|
|
|
remove: "Dokument entfernen", |
|
|
|
|
|
uploadFailed: "Upload fehlgeschlagen. Bitte erneut versuchen.", |
|
|
|
|
|
defaultDoc: "Dokument", |
|
|
|
|
|
}, |
|
|
|
|
|
fr: { |
|
|
|
|
|
uploadPrompt: "Télécharger des documents (carte d'identité, passeport...)", |
|
|
|
|
|
addMore: "Ajouter un autre document", |
|
|
|
|
|
uploading: "Téléchargement en cours...", |
|
|
|
|
|
remove: "Supprimer le document", |
|
|
|
|
|
uploadFailed: "Échec du téléchargement. Veuillez réessayer.", |
|
|
|
|
|
defaultDoc: "document", |
|
|
|
|
|
}, |
|
|
|
|
|
es: { |
|
|
|
|
|
uploadPrompt: "Subir documentos (DNI, pasaporte...)", |
|
|
|
|
|
addMore: "Agregar otro documento", |
|
|
|
|
|
uploading: "Subiendo...", |
|
|
|
|
|
remove: "Eliminar documento", |
|
|
|
|
|
uploadFailed: "Error al subir. Inténtalo de nuevo.", |
|
|
|
|
|
defaultDoc: "documento", |
|
|
|
|
|
}, |
|
|
|
|
|
zh: { |
|
|
|
|
|
uploadPrompt: "上传证件(身份证、护照等)", |
|
|
|
|
|
addMore: "添加其他文件", |
|
|
|
|
|
uploading: "上传中...", |
|
|
|
|
|
remove: "删除文件", |
|
|
|
|
|
uploadFailed: "上传失败,请重试。", |
|
|
|
|
|
defaultDoc: "文件", |
|
|
|
|
|
}, |
|
|
|
|
|
id: { |
|
|
|
|
|
uploadPrompt: "Unggah dokumen (KTP, paspor...)", |
|
|
|
|
|
addMore: "Tambah dokumen lain", |
|
|
|
|
|
uploading: "Mengunggah...", |
|
|
|
|
|
remove: "Hapus dokumen", |
|
|
|
|
|
uploadFailed: "Unggah gagal. Silakan coba lagi.", |
|
|
|
|
|
defaultDoc: "dokumen", |
|
|
|
|
|
}, |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
/** Map question file extensions to upload_file mediaType. */ |
|
|
/** Map question file extensions to upload_file mediaType. */ |
|
|
function resolveMediaType( |
|
|
function resolveMediaType( |
|
|
extensions: string[], |
|
|
extensions: string[], |
|
|
@ -69,10 +171,15 @@ export function QuestionFile({ |
|
|
question, |
|
|
question, |
|
|
disabled, |
|
|
disabled, |
|
|
}: QuestionFileProps) { |
|
|
}: QuestionFileProps) { |
|
|
|
|
|
const { locale } = useI18n(); |
|
|
|
|
|
const t = |
|
|
|
|
|
FILE_I18N[locale] ?? FILE_I18N[locale?.slice(0, 2)] ?? FILE_I18N.fa; |
|
|
|
|
|
|
|
|
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); |
|
|
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); |
|
|
const storedValue = getAnswerValue(question); |
|
|
const storedValue = getAnswerValue(question); |
|
|
|
|
|
|
|
|
const parseInitialDocs = useCallback((stored: unknown): UploadedDoc[] => { |
|
|
|
|
|
|
|
|
const parseInitialDocs = useCallback( |
|
|
|
|
|
(stored: unknown): UploadedDoc[] => { |
|
|
if (!stored) return []; |
|
|
if (!stored) return []; |
|
|
if (Array.isArray(stored)) { |
|
|
if (Array.isArray(stored)) { |
|
|
return stored |
|
|
return stored |
|
|
@ -88,11 +195,12 @@ export function QuestionFile({ |
|
|
return { |
|
|
return { |
|
|
url: item, |
|
|
url: item, |
|
|
previewUrl: resolvedUrl, |
|
|
previewUrl: resolvedUrl, |
|
|
name: item.split("/").pop() || "document", |
|
|
|
|
|
|
|
|
name: item.split("/").pop() || t.defaultDoc, |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
const url = (item.url || item.path || "") as string; |
|
|
const url = (item.url || item.path || "") as string; |
|
|
const name = (item.name || url.split("/").pop() || "document") as string; |
|
|
|
|
|
|
|
|
const name = |
|
|
|
|
|
(item.name || url.split("/").pop() || t.defaultDoc) as string; |
|
|
const resolvedUrl = |
|
|
const resolvedUrl = |
|
|
url.startsWith("http") || |
|
|
url.startsWith("http") || |
|
|
url.startsWith("blob:") || |
|
|
url.startsWith("blob:") || |
|
|
@ -112,13 +220,15 @@ export function QuestionFile({ |
|
|
return [ |
|
|
return [ |
|
|
{ |
|
|
{ |
|
|
url: stored, |
|
|
url: stored, |
|
|
name: stored.split("/").pop() || "document", |
|
|
|
|
|
|
|
|
name: stored.split("/").pop() || t.defaultDoc, |
|
|
previewUrl: resolvedUrl, |
|
|
previewUrl: resolvedUrl, |
|
|
}, |
|
|
}, |
|
|
]; |
|
|
]; |
|
|
} |
|
|
} |
|
|
return []; |
|
|
return []; |
|
|
}, []); |
|
|
|
|
|
|
|
|
}, |
|
|
|
|
|
[t.defaultDoc], |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const [uploadedDocs, setUploadedDocs] = useState<UploadedDoc[]>(() => |
|
|
const [uploadedDocs, setUploadedDocs] = useState<UploadedDoc[]>(() => |
|
|
parseInitialDocs(storedValue), |
|
|
parseInitialDocs(storedValue), |
|
|
@ -139,7 +249,7 @@ export function QuestionFile({ |
|
|
: getApiRequestUrl(response.path); |
|
|
: getApiRequestUrl(response.path); |
|
|
const newDoc: UploadedDoc = { |
|
|
const newDoc: UploadedDoc = { |
|
|
url: response.path, |
|
|
url: response.path, |
|
|
name: response.name ?? response.path.split("/").pop() ?? "uploaded", |
|
|
|
|
|
|
|
|
name: response.name ?? response.path.split("/").pop() ?? t.defaultDoc, |
|
|
previewUrl: resolved, |
|
|
previewUrl: resolved, |
|
|
}; |
|
|
}; |
|
|
setUploadedDocs((prev) => { |
|
|
setUploadedDocs((prev) => { |
|
|
@ -198,7 +308,7 @@ export function QuestionFile({ |
|
|
: getApiRequestUrl(remoteUrl); |
|
|
: getApiRequestUrl(remoteUrl); |
|
|
newDocs.push({ |
|
|
newDocs.push({ |
|
|
url: remoteUrl, |
|
|
url: remoteUrl, |
|
|
name: f?.name || remoteUrl.split("/").pop() || "document", |
|
|
|
|
|
|
|
|
name: f?.name || remoteUrl.split("/").pop() || t.defaultDoc, |
|
|
previewUrl: resolved, |
|
|
previewUrl: resolved, |
|
|
}); |
|
|
}); |
|
|
} else if (f?.base64) { |
|
|
} else if (f?.base64) { |
|
|
@ -239,7 +349,7 @@ export function QuestionFile({ |
|
|
return () => { |
|
|
return () => { |
|
|
unsubscribe?.(); |
|
|
unsubscribe?.(); |
|
|
}; |
|
|
}; |
|
|
}, [question, setAnswerValue, uploadTmpMediaMutation]); |
|
|
|
|
|
|
|
|
}, [question, setAnswerValue, t.defaultDoc, uploadTmpMediaMutation]); |
|
|
|
|
|
|
|
|
/** Handle file pick in Flutter WebView via upload_file action. */ |
|
|
/** Handle file pick in Flutter WebView via upload_file action. */ |
|
|
const handleFlutterPick = useCallback(() => { |
|
|
const handleFlutterPick = useCallback(() => { |
|
|
@ -363,7 +473,22 @@ export function QuestionFile({ |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
<div |
|
|
<div |
|
|
className="relative flex min-h-[160px] w-full flex-col items-center justify-center rounded-[29px] border-2 border-dashed border-[#8D8D8D] bg-[#F7F7F7] text-center transition-colors duration-200 focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-[#6F6F6F] hover:border-[#777777] overflow-hidden p-4" |
|
|
|
|
|
|
|
|
role="button" |
|
|
|
|
|
tabIndex={0} |
|
|
|
|
|
onClick={!hasDocuments ? () => handleTriggerPick() : undefined} |
|
|
|
|
|
onKeyDown={ |
|
|
|
|
|
!hasDocuments |
|
|
|
|
|
? (e) => { |
|
|
|
|
|
if (e.key === "Enter" || e.key === " ") handleTriggerPick(); |
|
|
|
|
|
} |
|
|
|
|
|
: undefined |
|
|
|
|
|
} |
|
|
|
|
|
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", |
|
|
|
|
|
hasDocuments |
|
|
|
|
|
? "min-h-[156px]" |
|
|
|
|
|
: "aspect-[727/330] min-h-[156px] cursor-pointer", |
|
|
|
|
|
].join(" ")} |
|
|
> |
|
|
> |
|
|
{hasDocuments ? ( |
|
|
{hasDocuments ? ( |
|
|
/* ────── UPLOADED MULTI-DOCUMENT STATE ────── */ |
|
|
/* ────── UPLOADED MULTI-DOCUMENT STATE ────── */ |
|
|
@ -415,7 +540,7 @@ export function QuestionFile({ |
|
|
<button |
|
|
<button |
|
|
type="button" |
|
|
type="button" |
|
|
onClick={(e) => handleRemoveDoc(index, e)} |
|
|
onClick={(e) => handleRemoveDoc(index, e)} |
|
|
title="حذف مدرک" |
|
|
|
|
|
|
|
|
title={t.remove} |
|
|
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#F3F4F6] text-[#4B5563] transition-colors hover:bg-red-50 hover:text-[#EF4444] active:scale-95 cursor-pointer" |
|
|
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#F3F4F6] text-[#4B5563] transition-colors hover:bg-red-50 hover:text-[#EF4444] active:scale-95 cursor-pointer" |
|
|
> |
|
|
> |
|
|
<svg |
|
|
<svg |
|
|
@ -456,20 +581,12 @@ export function QuestionFile({ |
|
|
> |
|
|
> |
|
|
<path d="M12 5v14M5 12h14" /> |
|
|
<path d="M12 5v14M5 12h14" /> |
|
|
</svg> |
|
|
</svg> |
|
|
<span>افزودن مدرک دیگر</span> |
|
|
|
|
|
|
|
|
<span>{t.addMore}</span> |
|
|
</button> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
) : ( |
|
|
) : ( |
|
|
/* ────── DEFAULT EMPTY STATE ────── */ |
|
|
/* ────── DEFAULT EMPTY STATE ────── */ |
|
|
<div |
|
|
|
|
|
role="button" |
|
|
|
|
|
tabIndex={0} |
|
|
|
|
|
onClick={() => handleTriggerPick()} |
|
|
|
|
|
onKeyDown={(e) => { |
|
|
|
|
|
if (e.key === "Enter" || e.key === " ") handleTriggerPick(); |
|
|
|
|
|
}} |
|
|
|
|
|
className="flex w-full cursor-pointer flex-col items-center justify-center py-4" |
|
|
|
|
|
> |
|
|
|
|
|
|
|
|
<div className="flex w-full flex-col items-center justify-center py-4 pointer-events-none"> |
|
|
<Image |
|
|
<Image |
|
|
src="/assets/images/Image.svg" |
|
|
src="/assets/images/Image.svg" |
|
|
alt="Upload" |
|
|
alt="Upload" |
|
|
@ -477,9 +594,13 @@ export function QuestionFile({ |
|
|
height={28} |
|
|
height={28} |
|
|
/> |
|
|
/> |
|
|
<span className="mt-3 block group-12 leading-none font-semibold text-[#111111]"> |
|
|
<span className="mt-3 block group-12 leading-none font-semibold text-[#111111]"> |
|
|
بارگذاری مدارک (کارت شناسایی، پاسپورت و...) |
|
|
|
|
|
|
|
|
{t.uploadPrompt} |
|
|
</span> |
|
|
</span> |
|
|
{acceptedFiles ? ( |
|
|
|
|
|
|
|
|
{uploadTmpMediaMutation.isError ? ( |
|
|
|
|
|
<span className="mt-2 block group-10 leading-none font-bold text-[#D44747]"> |
|
|
|
|
|
{t.uploadFailed} |
|
|
|
|
|
</span> |
|
|
|
|
|
) : acceptedFiles ? ( |
|
|
<span className="mt-2 block group-10 leading-none font-bold text-[#8B8B8B]"> |
|
|
<span className="mt-2 block group-10 leading-none font-bold text-[#8B8B8B]"> |
|
|
{acceptedFiles} |
|
|
{acceptedFiles} |
|
|
</span> |
|
|
</span> |
|
|
@ -491,7 +612,9 @@ export function QuestionFile({ |
|
|
{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="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="h-8 w-8 animate-spin rounded-full border-[2.5px] border-white border-t-transparent shadow-sm" /> |
|
|
<span className="text-xs font-semibold text-white">در حال آپلود در تمپ...</span> |
|
|
|
|
|
|
|
|
<span className="text-xs font-semibold text-white"> |
|
|
|
|
|
{t.uploading} |
|
|
|
|
|
</span> |
|
|
</div> |
|
|
</div> |
|
|
)} |
|
|
)} |
|
|
</div> |
|
|
</div> |
|
|
|