Browse Source

fix(question-file): add full multi-language i18n support and fix border interaction states

master
mortezaei 2 days ago
parent
commit
192546a02a
  1. 251
      src/components/Componentes/question-file.tsx

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

@ -9,6 +9,7 @@ import {
} from "@/hooks/marriage/use-upload-tmp-media";
import { getApiRequestUrl } from "@/lib/http";
import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
import { LoadingSkeleton } from "./loading-skeleton";
@ -18,6 +19,107 @@ type QuestionFileProps = {
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. */
function resolveMediaType(
extensions: string[],
@ -69,56 +171,64 @@ export function QuestionFile({
question,
disabled,
}: QuestionFileProps) {
const { locale } = useI18n();
const t =
FILE_I18N[locale] ?? FILE_I18N[locale?.slice(0, 2)] ?? FILE_I18N.fa;
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question);
const parseInitialDocs = useCallback((stored: unknown): UploadedDoc[] => {
if (!stored) return [];
if (Array.isArray(stored)) {
return stored
.filter((item): item is string | Record<string, unknown> => Boolean(item))
.map((item) => {
if (typeof item === "string") {
const parseInitialDocs = useCallback(
(stored: unknown): UploadedDoc[] => {
if (!stored) return [];
if (Array.isArray(stored)) {
return stored
.filter((item): item is string | Record<string, unknown> => Boolean(item))
.map((item) => {
if (typeof item === "string") {
const resolvedUrl =
item.startsWith("http") ||
item.startsWith("blob:") ||
item.startsWith("data:")
? item
: getApiRequestUrl(item);
return {
url: item,
previewUrl: resolvedUrl,
name: item.split("/").pop() || t.defaultDoc,
};
}
const url = (item.url || item.path || "") as string;
const name =
(item.name || url.split("/").pop() || t.defaultDoc) as string;
const resolvedUrl =
item.startsWith("http") ||
item.startsWith("blob:") ||
item.startsWith("data:")
? item
: getApiRequestUrl(item);
return {
url: item,
previewUrl: resolvedUrl,
name: item.split("/").pop() || "document",
};
}
const url = (item.url || item.path || "") as string;
const name = (item.name || url.split("/").pop() || "document") as string;
const resolvedUrl =
url.startsWith("http") ||
url.startsWith("blob:") ||
url.startsWith("data:")
? url
: getApiRequestUrl(url);
return { url, name, previewUrl: resolvedUrl };
});
}
if (typeof stored === "string" && stored.trim().length > 0) {
const resolvedUrl =
stored.startsWith("http") ||
stored.startsWith("blob:") ||
stored.startsWith("data:")
? stored
: getApiRequestUrl(stored);
return [
{
url: stored,
name: stored.split("/").pop() || "document",
previewUrl: resolvedUrl,
},
];
}
return [];
}, []);
url.startsWith("http") ||
url.startsWith("blob:") ||
url.startsWith("data:")
? url
: getApiRequestUrl(url);
return { url, name, previewUrl: resolvedUrl };
});
}
if (typeof stored === "string" && stored.trim().length > 0) {
const resolvedUrl =
stored.startsWith("http") ||
stored.startsWith("blob:") ||
stored.startsWith("data:")
? stored
: getApiRequestUrl(stored);
return [
{
url: stored,
name: stored.split("/").pop() || t.defaultDoc,
previewUrl: resolvedUrl,
},
];
}
return [];
},
[t.defaultDoc],
);
const [uploadedDocs, setUploadedDocs] = useState<UploadedDoc[]>(() =>
parseInitialDocs(storedValue),
@ -139,7 +249,7 @@ export function QuestionFile({
: getApiRequestUrl(response.path);
const newDoc: UploadedDoc = {
url: response.path,
name: response.name ?? response.path.split("/").pop() ?? "uploaded",
name: response.name ?? response.path.split("/").pop() ?? t.defaultDoc,
previewUrl: resolved,
};
setUploadedDocs((prev) => {
@ -198,7 +308,7 @@ export function QuestionFile({
: getApiRequestUrl(remoteUrl);
newDocs.push({
url: remoteUrl,
name: f?.name || remoteUrl.split("/").pop() || "document",
name: f?.name || remoteUrl.split("/").pop() || t.defaultDoc,
previewUrl: resolved,
});
} else if (f?.base64) {
@ -239,7 +349,7 @@ export function QuestionFile({
return () => {
unsubscribe?.();
};
}, [question, setAnswerValue, uploadTmpMediaMutation]);
}, [question, setAnswerValue, t.defaultDoc, uploadTmpMediaMutation]);
/** Handle file pick in Flutter WebView via upload_file action. */
const handleFlutterPick = useCallback(() => {
@ -363,7 +473,22 @@ export function QuestionFile({
)}
<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 ? (
/* ────── UPLOADED MULTI-DOCUMENT STATE ────── */
@ -415,7 +540,7 @@ export function QuestionFile({
<button
type="button"
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"
>
<svg
@ -456,20 +581,12 @@ export function QuestionFile({
>
<path d="M12 5v14M5 12h14" />
</svg>
<span>افزودن مدرک دیگر</span>
<span>{t.addMore}</span>
</button>
</div>
) : (
/* ────── 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
src="/assets/images/Image.svg"
alt="Upload"
@ -477,9 +594,13 @@ export function QuestionFile({
height={28}
/>
<span className="mt-3 block group-12 leading-none font-semibold text-[#111111]">
بارگذاری مدارک (کارت شناسایی، پاسپورت و...)
{t.uploadPrompt}
</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]">
{acceptedFiles}
</span>
@ -491,7 +612,9 @@ export function QuestionFile({
{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" />
<span className="text-xs font-semibold text-white">در حال آپلود در تمپ...</span>
<span className="text-xs font-semibold text-white">
{t.uploading}
</span>
</div>
)}
</div>

Loading…
Cancel
Save