You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
693 lines
24 KiB
693 lines
24 KiB
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import type { QuestionField } from "@/lib/schema-adapter";
|
|
import {
|
|
uploadTmpMedia,
|
|
useUploadTmpMediaMutation,
|
|
} 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";
|
|
|
|
type QuestionFileProps = {
|
|
question: QuestionField;
|
|
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[],
|
|
): "image" | "video" | "audio" | "file" {
|
|
const lower = extensions.map((e) => e.replace(/^\./, "").toLowerCase());
|
|
const allImages = lower.every((e) =>
|
|
["jpg", "jpeg", "png", "webp", "gif", "bmp", "svg"].includes(e),
|
|
);
|
|
if (allImages) return "image";
|
|
|
|
const allVideo = lower.every((e) =>
|
|
["mp4", "mov", "avi", "mkv", "webm"].includes(e),
|
|
);
|
|
if (allVideo) return "video";
|
|
|
|
const allAudio = lower.every((e) =>
|
|
["mp3", "m4a", "wav", "aac", "ogg"].includes(e),
|
|
);
|
|
if (allAudio) return "audio";
|
|
|
|
return "file";
|
|
}
|
|
|
|
function isImageFile(
|
|
fileName?: string | null,
|
|
fileUrl?: string | null,
|
|
): boolean {
|
|
if (
|
|
fileUrl &&
|
|
(fileUrl.startsWith("data:image/") || fileUrl.startsWith("blob:"))
|
|
) {
|
|
if (fileUrl.startsWith("data:application/pdf")) return false;
|
|
if (fileName && /\.(pdf|doc|docx|xls|xlsx|txt|zip|rar)$/i.test(fileName)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
const nameToCheck = fileName || fileUrl || "";
|
|
return /\.(jpg|jpeg|png|webp|gif|svg|bmp|avif)$/i.test(nameToCheck);
|
|
}
|
|
|
|
export type UploadedDoc = {
|
|
url: string;
|
|
name: string;
|
|
previewUrl?: string;
|
|
};
|
|
|
|
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, registerPendingUpload } =
|
|
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 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 =
|
|
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),
|
|
);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
|
const isInitiatorRef = useRef(false);
|
|
const pendingDeferredRef = useRef<{
|
|
resolve: (val?: any) => void;
|
|
reject: (err?: any) => void;
|
|
} | null>(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 ?? [])
|
|
.map((option) => option.replace(/^\./, ""))
|
|
.join(", ");
|
|
|
|
const uploadTmpMediaMutation = useUploadTmpMediaMutation({
|
|
onSuccess: (response) => {
|
|
if (response.path) {
|
|
const resolved = response.path.startsWith("http")
|
|
? response.path
|
|
: getApiRequestUrl(response.path);
|
|
const newDoc: UploadedDoc = {
|
|
url: response.path,
|
|
name: response.name ?? response.path.split("/").pop() ?? t.defaultDoc,
|
|
previewUrl: resolved,
|
|
};
|
|
setUploadedDocs((prev) => {
|
|
const nextDocs = [...prev, newDoc];
|
|
setAnswerValue(
|
|
question,
|
|
nextDocs.length === 1 ? nextDocs[0].url : nextDocs.map((d) => d.url),
|
|
);
|
|
return nextDocs;
|
|
});
|
|
}
|
|
setIsUploading(false);
|
|
setUploadProgress(null);
|
|
},
|
|
onError: (error) => {
|
|
console.error("File upload error:", error);
|
|
setIsUploading(false);
|
|
setUploadProgress(null);
|
|
},
|
|
});
|
|
|
|
const isPending = uploadTmpMediaMutation.isPending || isUploading;
|
|
|
|
// Listen for upload_file responses from Flutter
|
|
useEffect(() => {
|
|
if (!isInFlutterWebView()) return;
|
|
|
|
const unsubscribe = window.addFlutterResponseListener?.((event) => {
|
|
if (event.action !== "upload_file") return;
|
|
if (event.requestId && String(event.requestId) !== String(question.id)) return;
|
|
if (!event.requestId && !isInitiatorRef.current) return;
|
|
|
|
switch (event.status) {
|
|
case "picking":
|
|
break;
|
|
case "picked":
|
|
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;
|
|
}
|
|
case "completed": {
|
|
setIsUploading(false);
|
|
setUploadProgress(null);
|
|
isInitiatorRef.current = false;
|
|
const incomingFiles = event.data?.files || [];
|
|
const newDocs: UploadedDoc[] = [];
|
|
|
|
for (const f of incomingFiles) {
|
|
const remoteUrl =
|
|
f?.path ||
|
|
f?.url ||
|
|
(f?.data?.path as string | undefined) ||
|
|
(f?.data?.url as string | undefined);
|
|
if (remoteUrl) {
|
|
const resolved =
|
|
remoteUrl.startsWith("http") ||
|
|
remoteUrl.startsWith("blob:") ||
|
|
remoteUrl.startsWith("data:")
|
|
? remoteUrl
|
|
: getApiRequestUrl(remoteUrl);
|
|
newDocs.push({
|
|
url: remoteUrl,
|
|
name: f?.name || remoteUrl.split("/").pop() || t.defaultDoc,
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (newDocs.length > 0) {
|
|
setUploadedDocs((prev) => {
|
|
const nextDocs = [...prev, ...newDocs];
|
|
setAnswerValue(
|
|
question,
|
|
nextDocs.length === 1
|
|
? nextDocs[0].url
|
|
: nextDocs.map((d) => d.url),
|
|
);
|
|
return nextDocs;
|
|
});
|
|
pendingDeferredRef.current?.resolve(newDocs);
|
|
pendingDeferredRef.current = null;
|
|
} else {
|
|
pendingDeferredRef.current?.resolve([]);
|
|
pendingDeferredRef.current = null;
|
|
}
|
|
break;
|
|
}
|
|
case "cancelled":
|
|
case "failed":
|
|
setIsUploading(false);
|
|
setUploadProgress(null);
|
|
isInitiatorRef.current = false;
|
|
pendingDeferredRef.current?.reject(new Error("File upload failed or cancelled"));
|
|
pendingDeferredRef.current = null;
|
|
break;
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
unsubscribe?.();
|
|
};
|
|
}, [question, setAnswerValue, t.defaultDoc, uploadTmpMediaMutation]);
|
|
|
|
/** Handle file pick in Flutter WebView via upload_file action. */
|
|
const handleFlutterPick = useCallback(() => {
|
|
const extensions = (question.extras?.options ?? []).map((o) =>
|
|
o.replace(/^\./, "").toLowerCase(),
|
|
);
|
|
const mediaType = resolveMediaType(extensions);
|
|
|
|
isInitiatorRef.current = true;
|
|
setIsUploading(true);
|
|
|
|
let resolveFn!: (val?: any) => void;
|
|
let rejectFn!: (err?: any) => void;
|
|
const uploadPromise = new Promise((resolve, reject) => {
|
|
resolveFn = resolve;
|
|
rejectFn = reject;
|
|
});
|
|
pendingDeferredRef.current = { resolve: resolveFn, reject: rejectFn };
|
|
registerPendingUpload?.(question.id, uploadPromise);
|
|
|
|
const pickerSource =
|
|
question.ui_config?.source ||
|
|
question.ui_config?.picker_type ||
|
|
"file_system";
|
|
const pickerType =
|
|
question.ui_config?.picker_type ||
|
|
question.ui_config?.source ||
|
|
"file_system";
|
|
|
|
uploadFile({
|
|
requestId: String(question.id),
|
|
mediaType,
|
|
multiple: true,
|
|
source: pickerSource as any,
|
|
picker_type: pickerType as any,
|
|
returnAs: "upload",
|
|
uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`,
|
|
fieldName: "file",
|
|
allowedExtensions: extensions,
|
|
maxBytes: 10_485_760, // 10 MB
|
|
title:
|
|
typeof question.title === "string" ? question.title : "Upload File",
|
|
});
|
|
}, [question, registerPendingUpload]);
|
|
|
|
/** Handle file pick via browser <input type="file" multiple> (fallback). */
|
|
function handleBrowserFileChange(files: FileList | null) {
|
|
if (!files || files.length === 0) return;
|
|
|
|
setIsUploading(true);
|
|
setUploadProgress(0);
|
|
|
|
const uploadTask = (async () => {
|
|
const newDocs: UploadedDoc[] = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
try {
|
|
const res = await uploadTmpMedia(file, (p) => setUploadProgress(p));
|
|
if (res.path) {
|
|
const resolved = res.path.startsWith("http")
|
|
? res.path
|
|
: getApiRequestUrl(res.path);
|
|
newDocs.push({
|
|
url: res.path,
|
|
name: res.name || file.name,
|
|
previewUrl: resolved,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to upload tmp file:", file.name, err);
|
|
}
|
|
}
|
|
|
|
setIsUploading(false);
|
|
setUploadProgress(null);
|
|
|
|
if (newDocs.length > 0) {
|
|
setUploadedDocs((prev) => {
|
|
const nextDocs = [...prev, ...newDocs];
|
|
setAnswerValue(
|
|
question,
|
|
nextDocs.length === 1 ? nextDocs[0].url : nextDocs.map((d) => d.url),
|
|
);
|
|
return nextDocs;
|
|
});
|
|
}
|
|
|
|
if (fileInputRef.current) {
|
|
fileInputRef.current.value = "";
|
|
}
|
|
})();
|
|
|
|
registerPendingUpload?.(question.id, uploadTask);
|
|
}
|
|
|
|
const handleRemoveDoc = (indexToRemove: number, e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
setUploadedDocs((prev) => {
|
|
const nextDocs = prev.filter((_, idx) => idx !== indexToRemove);
|
|
if (nextDocs.length === 0) {
|
|
setAnswerValue(question, null);
|
|
} else if (nextDocs.length === 1) {
|
|
setAnswerValue(question, nextDocs[0].url);
|
|
} else {
|
|
setAnswerValue(question, nextDocs.map((d) => d.url));
|
|
}
|
|
return nextDocs;
|
|
});
|
|
};
|
|
|
|
const handleTriggerPick = (e?: React.MouseEvent) => {
|
|
if (e) {
|
|
e.stopPropagation();
|
|
}
|
|
if (isInFlutterWebView()) {
|
|
handleFlutterPick();
|
|
} else {
|
|
fileInputRef.current?.click();
|
|
}
|
|
};
|
|
|
|
const inWebView = isInFlutterWebView();
|
|
const hasDocuments = uploadedDocs.length > 0;
|
|
|
|
return (
|
|
<div
|
|
data-question-answered={hasDocuments ? "true" : "false"}
|
|
className={[
|
|
"flex w-full flex-col gap-2 transition-opacity duration-200",
|
|
disabled ? "pointer-events-none opacity-30" : "",
|
|
].join(" ")}
|
|
>
|
|
<QuestionTitle question={question} />
|
|
|
|
{/* Hidden browser input for fallback */}
|
|
{!inWebView && (
|
|
<input
|
|
ref={fileInputRef}
|
|
type="file"
|
|
multiple
|
|
accept={acceptedFiles ? acceptedFiles : "*/*"}
|
|
disabled={disabled}
|
|
onChange={(event) => handleBrowserFileChange(event.target.files)}
|
|
className="sr-only"
|
|
/>
|
|
)}
|
|
|
|
<div
|
|
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-colors duration-200 hover:border-[#6F6F6F] overflow-hidden select-none",
|
|
hasDocuments
|
|
? "min-h-[156px] p-3.5"
|
|
: "aspect-[727/330] min-h-[156px] cursor-pointer p-4",
|
|
].join(" ")}
|
|
>
|
|
{hasDocuments ? (
|
|
/* ────── UPLOADED MULTI-DOCUMENT STATE ────── */
|
|
<div className="relative flex w-full flex-col items-center gap-3">
|
|
<div className="flex w-full flex-col gap-2">
|
|
{uploadedDocs.map((doc, index) => {
|
|
const isImg = isImageFile(doc.name, doc.previewUrl ?? doc.url);
|
|
return (
|
|
<div
|
|
key={`${doc.url}-${index}`}
|
|
className="relative flex w-full items-center justify-between gap-3 rounded-[16px] border border-[#E5E7EB] bg-white p-2.5 shadow-xs transition-shadow hover:shadow-sm"
|
|
>
|
|
{/* Left: Thumbnail or PDF Icon */}
|
|
<div className="flex items-center gap-3 overflow-hidden">
|
|
{isImg && doc.previewUrl ? (
|
|
<img
|
|
src={doc.previewUrl}
|
|
alt={doc.name}
|
|
className="h-12 w-12 shrink-0 rounded-[10px] object-cover border border-[#E5E7EB]"
|
|
/>
|
|
) : (
|
|
<div className="relative flex h-12 w-10 shrink-0 items-center justify-center rounded-[6px] border border-[#D1D5DB] bg-[#F9FAFB] shadow-xs">
|
|
<svg
|
|
width="24"
|
|
height="28"
|
|
viewBox="0 0 32 36"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M4 0C1.79086 0 0 1.79086 0 4V32C0 34.2091 1.79086 36 4 36H28C30.2091 36 32 34.2091 32 32V10L22 0H4Z"
|
|
fill="#E5E7EB"
|
|
/>
|
|
<path d="M22 0V10H32L22 0Z" fill="#9CA3AF" />
|
|
</svg>
|
|
<span className="absolute bottom-1 rounded-[3px] bg-[#F0445B] px-1 py-[1px] text-[7px] font-bold text-white leading-none">
|
|
PDF
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Middle: File Name */}
|
|
<span className="truncate text-start text-xs font-semibold text-[#1F2024] max-w-[200px]">
|
|
{doc.name}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Right: Individual Trash Button */}
|
|
<button
|
|
type="button"
|
|
onClick={(e) => handleRemoveDoc(index, e)}
|
|
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
|
|
width="15"
|
|
height="15"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M3 6h18" />
|
|
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
|
|
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Add Another Document Button */}
|
|
<button
|
|
type="button"
|
|
onClick={(e) => handleTriggerPick(e)}
|
|
className="mt-1 flex items-center justify-center gap-1.5 rounded-full border border-[#D1D5DB] bg-white px-4 py-2 text-xs font-bold text-[#374151] shadow-xs transition-colors hover:bg-[#F9FAFB] hover:border-[#9CA3AF] active:scale-98 cursor-pointer"
|
|
>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M12 5v14M5 12h14" />
|
|
</svg>
|
|
<span>{t.addMore}</span>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
/* ────── DEFAULT EMPTY STATE ────── */
|
|
<div className="flex w-full flex-col items-center justify-center py-4 pointer-events-none">
|
|
<Image
|
|
src="/assets/images/Image.svg"
|
|
alt="Upload"
|
|
width={28}
|
|
height={28}
|
|
/>
|
|
<span className="mt-3 block group-12 leading-none font-semibold text-[#111111]">
|
|
{t.uploadPrompt}
|
|
</span>
|
|
{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>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
|
|
{/* Smooth Circular Loading Spinner Overlay with Upload Progress */}
|
|
{isPending && (
|
|
<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">
|
|
{uploadProgress !== null && uploadProgress > 0
|
|
? `${t.uploading} (${uploadProgress}%)`
|
|
: t.uploading}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default QuestionFile;
|