diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index af9a9a8..7bce970 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -7,9 +7,10 @@ import { uploadTmpMedia, useUploadTmpMediaMutation, } from "@/hooks/marriage/use-upload-tmp-media"; -import { getApiRequestUrl } from "@/lib/http"; +import { getApiRequestUrl, resolveMediaUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; import { useI18n } from "@/translations/provider"; +import ErrorToast from "./error-toast"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; import { LoadingSkeleton } from "./loading-skeleton"; @@ -191,12 +192,7 @@ export function QuestionFile({ .filter((item): item is string | Record => Boolean(item)) .map((item) => { if (typeof item === "string") { - const resolvedUrl = - item.startsWith("http") || - item.startsWith("blob:") || - item.startsWith("data:") - ? item - : getApiRequestUrl(item); + const resolvedUrl = resolveMediaUrl(item) ?? item; return { url: item, previewUrl: resolvedUrl, @@ -206,22 +202,12 @@ export function QuestionFile({ 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); + const resolvedUrl = resolveMediaUrl(url) ?? 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); + const resolvedUrl = resolveMediaUrl(stored) ?? stored; return [ { url: stored, @@ -242,6 +228,9 @@ export function QuestionFile({ const [uploadProgress, setUploadProgress] = useState( debugProgress ?? null, ); + const [toastMessage, setToastMessage] = useState(null); + const [imgErrorSet, setImgErrorSet] = useState>(() => new Set()); + const activeBlobUrlsRef = useRef>(new Map()); const isInitiatorRef = useRef(false); const pendingDeferredRef = useRef<{ resolve: (val?: any) => void; @@ -249,6 +238,16 @@ export function QuestionFile({ } | null>(null); const fileInputRef = useRef(null); + // Revoke blob URLs on unmount + useEffect(() => { + return () => { + for (const blobUrl of activeBlobUrlsRef.current.values()) { + URL.revokeObjectURL(blobUrl); + } + activeBlobUrlsRef.current.clear(); + }; + }, []); + // Sync state when answers load asynchronously from server/cache useEffect(() => { if (storedValue !== undefined && storedValue !== null) { @@ -263,9 +262,7 @@ export function QuestionFile({ const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { if (response.path) { - const resolved = response.path.startsWith("http") - ? response.path - : getApiRequestUrl(response.path); + const resolved = resolveMediaUrl(response.path) ?? response.path; const newDoc: UploadedDoc = { url: response.path, name: response.name ?? response.path.split("/").pop() ?? t.defaultDoc, @@ -287,6 +284,7 @@ export function QuestionFile({ console.error("File upload error:", error); setIsUploading(false); setUploadProgress(null); + setToastMessage(t.uploadFailed); }, }); @@ -330,12 +328,7 @@ export function QuestionFile({ (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); + const resolved = resolveMediaUrl(remoteUrl) ?? remoteUrl; newDocs.push({ url: remoteUrl, name: f?.name || remoteUrl.split("/").pop() || t.defaultDoc, @@ -439,32 +432,59 @@ export function QuestionFile({ setIsUploading(true); setUploadProgress(0); + setToastMessage(null); + + // Create instant blob previews for image files + const blobPreviews: { file: File; blobUrl: string | null }[] = []; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const isImg = file.type.startsWith("image/"); + if (isImg) { + const blobUrl = URL.createObjectURL(file); + blobPreviews.push({ file, blobUrl }); + } else { + blobPreviews.push({ file, blobUrl: null }); + } + } const uploadTask = (async () => { const newDocs: UploadedDoc[] = []; + let hasError = false; - for (let i = 0; i < files.length; i++) { - const file = files[i]; + for (let i = 0; i < blobPreviews.length; i++) { + const { file, blobUrl } = blobPreviews[i]; try { const res = await uploadTmpMedia(file, (p) => setUploadProgress(p)); if (res.path) { - const resolved = res.path.startsWith("http") - ? res.path - : getApiRequestUrl(res.path); + const resolved = resolveMediaUrl(res.path) ?? res.path; + // Use blob preview URL if available (instant), otherwise use resolved backend URL + const previewUrl = blobUrl ?? resolved; + if (blobUrl) { + activeBlobUrlsRef.current.set(res.path, blobUrl); + } newDocs.push({ url: res.path, name: res.name || file.name, - previewUrl: resolved, + previewUrl, }); } } catch (err) { console.error("Failed to upload tmp file:", file.name, err); + // Revoke blob URL on failure + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + } + hasError = true; } } setIsUploading(false); setUploadProgress(null); + if (hasError && newDocs.length === 0) { + setToastMessage(t.uploadFailed); + } + if (newDocs.length > 0) { setUploadedDocs((prev) => { const nextDocs = [...prev, ...newDocs]; @@ -488,7 +508,25 @@ export function QuestionFile({ e.stopPropagation(); e.preventDefault(); setUploadedDocs((prev) => { + const removedDoc = prev[indexToRemove]; + // Revoke blob URL if exists + if (removedDoc) { + const blobUrl = activeBlobUrlsRef.current.get(removedDoc.url); + if (blobUrl) { + URL.revokeObjectURL(blobUrl); + activeBlobUrlsRef.current.delete(removedDoc.url); + } + } const nextDocs = prev.filter((_, idx) => idx !== indexToRemove); + // Clear image error tracking for removed index + setImgErrorSet((prevSet) => { + const newSet = new Set(); + for (const errIdx of prevSet) { + if (errIdx < indexToRemove) newSet.add(errIdx); + else if (errIdx > indexToRemove) newSet.add(errIdx - 1); + } + return newSet; + }); if (nextDocs.length === 0) { setAnswerValue(question, null); } else if (nextDocs.length === 1) { @@ -522,6 +560,13 @@ export function QuestionFile({ disabled ? "pointer-events-none opacity-30" : "", ].join(" ")} > + {toastMessage && ( + setToastMessage(null)} + /> + )} + {/* Hidden browser input for fallback */} @@ -561,17 +606,22 @@ export function QuestionFile({
{uploadedDocs.map((doc, index) => { const isImg = isImageFile(doc.name, doc.previewUrl ?? doc.url); + const hasThumbError = imgErrorSet.has(index); + const ext = doc.name?.split(".").pop()?.toUpperCase() || "FILE"; return (
- {/* Left: Thumbnail or PDF Icon */} + {/* Left: Thumbnail or File Icon */}
- {isImg && doc.previewUrl ? ( + {isImg && doc.previewUrl && !hasThumbError ? ( {doc.name} { + setImgErrorSet((prev) => new Set(prev).add(index)); + }} className="h-12 w-12 shrink-0 rounded-[10px] object-cover border border-[#E5E7EB]" /> ) : ( @@ -590,7 +640,7 @@ export function QuestionFile({ - PDF + {ext.length <= 4 ? ext : "FILE"}
)} diff --git a/src/components/Componentes/question-photo.test.tsx b/src/components/Componentes/question-photo.test.tsx new file mode 100644 index 0000000..bdc8c96 --- /dev/null +++ b/src/components/Componentes/question-photo.test.tsx @@ -0,0 +1,213 @@ +import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { QuestionField } from "@/lib/schema-adapter"; +import { QuestionPhoto } from "./question-photo"; + +let answerMap: Record = {}; +const mockSetAnswerValue = vi.fn((q: QuestionField, val: unknown) => { + answerMap[q.id] = val; +}); +const mockRegisterPendingUpload = vi.fn(); + +const mockUploadTmpMedia = vi.fn(async (file: File) => ({ + path: `/media/tmp/${file.name}`, + name: file.name, +})); + +vi.mock("@/hooks/marriage/use-upload-tmp-media", () => ({ + useUploadTmpMediaMutation: ({ + onSuccess, + onError, + }: { + onSuccess?: (data: any) => void; + onError?: (err: any) => void; + }) => ({ + mutate: (file: File) => { + mockUploadTmpMedia(file) + .then((res) => onSuccess?.(res)) + .catch((err) => onError?.(err)); + }, + mutateAsync: async (file: File) => { + try { + const res = await mockUploadTmpMedia(file); + onSuccess?.(res); + return res; + } catch (err) { + onError?.(err); + throw err; + } + }, + isPending: false, + isError: false, + }), +})); + +vi.mock("./question-answer-storage", () => ({ + useQuestionAnswers: () => ({ + getAnswerValue: (q: QuestionField) => answerMap[q.id] ?? null, + setAnswerValue: mockSetAnswerValue, + registerPendingUpload: mockRegisterPendingUpload, + isLoading: false, + }), +})); + +vi.mock("@/translations/provider", () => ({ + useI18n: () => ({ + locale: "en", + dictionary: { + "Photo upload failed. Please try again.": "Photo upload failed. Please try again.", + "Failed to load image. Please try uploading again.": "Failed to load image. Please try uploading again.", + }, + }), +})); + +const mockPhotoQuestion: QuestionField = { + id: "documents_verification.face_photo", + title: "Face Photo", + type: "photo", + order: 1, + required: true, + baseRequired: true, + isVisible: true, + description: "Recent clear face photo", + tooltip: "", + extras: { + options: [".jpg", ".jpeg", ".png"], + }, + options: [], +}; + +describe("QuestionPhoto Component", () => { + beforeEach(() => { + answerMap = {}; + mockSetAnswerValue.mockClear(); + mockUploadTmpMedia.mockClear(); + mockRegisterPendingUpload.mockClear(); + vi.clearAllMocks(); + }); + + afterEach(() => { + cleanup(); + }); + + it("renders default avatar placeholder when no photo uploaded", () => { + const { container } = render(); + + expect(screen.getByText(/Face/)).toBeInTheDocument(); + expect(screen.getByText(/Photo/)).toBeInTheDocument(); + expect(screen.getByText("Recent clear face photo")).toBeInTheDocument(); + // Default placeholder SVG is rendered + expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); + }); + + it("renders uploaded image and camera badge when answer is provided", () => { + answerMap[mockPhotoQuestion.id] = "https://example.com/avatar.jpg"; + const { container } = render(); + + const img = container.querySelector("img[src='https://example.com/avatar.jpg']"); + expect(img).toBeInTheDocument(); + expect(img).toHaveClass("rounded-full"); + // Camera badge rendered + expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeInTheDocument(); + }); + + it("triggers upload and renders circular spinner without black background overlay during file selection", async () => { + let resolveUpload!: (val: any) => void; + mockUploadTmpMedia.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveUpload = resolve; + }), + ); + + const { container } = render(); + + const fileInput = container.querySelector("input[type='file']") as HTMLInputElement; + const testFile = new File(["dummy content"], "my-photo.jpg", { type: "image/jpeg" }); + + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = vi.fn(() => "blob:http://localhost/test-blob"); + + try { + fireEvent.change(fileInput, { target: { files: [testFile] } }); + + await waitFor(() => { + const spinner = container.querySelector(".animate-spin"); + expect(spinner).toBeInTheDocument(); + expect(spinner).toHaveClass("border-[#F0445B]/25"); + }); + + expect(container.querySelector(".bg-black\\/40")).toBeNull(); + expect(container.querySelector(".bg-black")).toBeNull(); + + resolveUpload({ path: "/media/uploaded.jpg" }); + + await waitFor(() => { + expect(container.querySelector(".animate-spin")).toBeNull(); + }); + } finally { + URL.createObjectURL = originalCreateObjectURL; + } + }); + + it("shows error toast, reverts to default avatar placeholder, and clears answer when upload fails", async () => { + mockUploadTmpMedia.mockImplementationOnce( + () => Promise.reject(new Error("Network Error")), + ); + + const { container } = render(); + const fileInput = container.querySelector("input[type='file']") as HTMLInputElement; + const testFile = new File(["dummy content"], "bad-photo.jpg", { type: "image/jpeg" }); + + const originalCreateObjectURL = URL.createObjectURL; + const originalRevokeObjectURL = URL.revokeObjectURL; + URL.createObjectURL = vi.fn(() => "blob:http://localhost/bad-blob"); + URL.revokeObjectURL = vi.fn(); + + try { + fireEvent.change(fileInput, { target: { files: [testFile] } }); + + // Error toast should appear + await waitFor(() => { + expect(screen.getByText("Photo upload failed. Please try again.")).toBeInTheDocument(); + }); + + // Answer value is cleared + expect(mockSetAnswerValue).toHaveBeenCalledWith(mockPhotoQuestion, null); + + // Default avatar SVG placeholder is restored + expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); + + // Camera badge is not visible + expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeNull(); + + // File input is reset allowing re-upload + expect(fileInput.value).toBe(""); + } finally { + URL.createObjectURL = originalCreateObjectURL; + URL.revokeObjectURL = originalRevokeObjectURL; + } + }); + + it("shows error toast and reverts to default avatar when rendered image fails to load", async () => { + answerMap[mockPhotoQuestion.id] = "https://example.com/broken-avatar.jpg"; + const { container } = render(); + + const img = container.querySelector("img[src='https://example.com/broken-avatar.jpg']") as HTMLImageElement; + expect(img).toBeInTheDocument(); + + // Trigger image error (e.g. 404 / ERR_CONNECTION_REFUSED) + fireEvent.error(img); + + await waitFor(() => { + expect(screen.getByText("Failed to load image. Please try uploading again.")).toBeInTheDocument(); + }); + + // Answer value cleared + expect(mockSetAnswerValue).toHaveBeenCalledWith(mockPhotoQuestion, null); + + // Default avatar placeholder rendered instead of broken image + expect(container.querySelector("img[src*='Frame 2095586679.svg']")).toBeInTheDocument(); + expect(container.querySelector(".bg-\\[\\#F0445B\\]")).toBeNull(); + }); +}); diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index 582452b..bad1619 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -4,11 +4,12 @@ import Image from "next/image"; import { type ReactNode, useCallback, useEffect, useId, useRef, useState } from "react"; import type { QuestionField } from "@/lib/schema-adapter"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; -import { getApiRequestUrl } from "@/lib/http"; +import { resolveMediaUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; +import { useI18n } from "@/translations/provider"; +import ErrorToast from "./error-toast"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; -import { LoadingSkeleton } from "./loading-skeleton"; type QuestionPhotoProps = { question: QuestionField; @@ -22,15 +23,21 @@ export function QuestionPhoto({ disabled, }: QuestionPhotoProps) { const inputId = useId(); + const fileInputRef = useRef(null); + const activeBlobUrlRef = useRef(null); + + const { dictionary: t } = useI18n(); const [localPreviewUrl, setLocalPreviewUrl] = useState(null); const [isUploading, setIsUploading] = useState(false); + const [toastMessage, setToastMessage] = useState(null); + const [hasImageError, setHasImageError] = useState(false); const acceptedFiles = question.extras?.options && question.extras.options.length > 0 ? question.extras.options.join(",") : "image/*"; - const descriptionContent = description ?? question.description; + const { getAnswerValue, setAnswerValue, registerPendingUpload } = useQuestionAnswers(); const storedValue = getAnswerValue(question); @@ -41,17 +48,72 @@ export function QuestionPhoto({ reject: (err?: any) => void; } | null>(null); + // Revoke blob URL on unmount + useEffect(() => { + return () => { + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + activeBlobUrlRef.current = null; + } + }; + }, []); + + const handleUploadFailure = useCallback( + (errorMessage?: string) => { + setIsUploading(false); + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + activeBlobUrlRef.current = null; + } + setLocalPreviewUrl(null); + setAnswerValue(question, null); + setHasImageError(true); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setToastMessage( + errorMessage ?? + t["Photo upload failed. Please try again."] ?? + "Photo upload failed. Please try again.", + ); + }, + [question, setAnswerValue, t], + ); + + const handleImageError = useCallback(() => { + console.error("Photo preview failed to load"); + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + activeBlobUrlRef.current = null; + } + setLocalPreviewUrl(null); + setAnswerValue(question, null); + setHasImageError(true); + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setToastMessage( + t["Failed to load image. Please try uploading again."] ?? + "Failed to load image. Please try uploading again.", + ); + }, [question, setAnswerValue, t]); + const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { - if (response.path) { + if (response?.path) { setAnswerValue(question, response.path); - setLocalPreviewUrl(response.path); + setHasImageError(false); + if (!activeBlobUrlRef.current) { + setLocalPreviewUrl(response.path); + } + } else { + handleUploadFailure(); } setIsUploading(false); }, onError: (error) => { console.error("Photo upload error:", error); - setIsUploading(false); + handleUploadFailure(); }, }); @@ -72,6 +134,8 @@ export function QuestionPhoto({ case "picked": case "progress": setIsUploading(true); + setHasImageError(false); + setToastMessage(null); if (event.data?.files?.[0]?.base64) { const b64 = event.data.files[0].base64; setLocalPreviewUrl(b64); @@ -88,7 +152,8 @@ export function QuestionPhoto({ (file?.data?.url as string | undefined); if (remoteUrl) { setAnswerValue(question, remoteUrl); - setLocalPreviewUrl(remoteUrl); + setLocalPreviewUrl((prev) => prev || remoteUrl); + setHasImageError(false); pendingDeferredRef.current?.resolve(remoteUrl); pendingDeferredRef.current = null; } else if (file?.base64) { @@ -105,14 +170,18 @@ export function QuestionPhoto({ .then((res) => { if (res?.path) { setAnswerValue(question, res.path); - setLocalPreviewUrl(res.path); + setLocalPreviewUrl((prev) => prev || res.path); + setHasImageError(false); pendingDeferredRef.current?.resolve(res.path); } else { + handleUploadFailure(); pendingDeferredRef.current?.resolve(); } pendingDeferredRef.current = null; }) .catch((err) => { + console.error("Flutter photo upload error:", err); + handleUploadFailure(); pendingDeferredRef.current?.reject(err); pendingDeferredRef.current = null; }); @@ -124,10 +193,19 @@ export function QuestionPhoto({ break; } case "cancelled": + setIsUploading(false); + isInitiatorRef.current = false; + if (!storedValue) { + setLocalPreviewUrl(null); + } + pendingDeferredRef.current?.reject(new Error("Upload cancelled")); + pendingDeferredRef.current = null; + break; case "failed": setIsUploading(false); isInitiatorRef.current = false; - pendingDeferredRef.current?.reject(new Error("Upload failed or cancelled")); + handleUploadFailure(); + pendingDeferredRef.current?.reject(new Error("Upload failed")); pendingDeferredRef.current = null; break; } @@ -136,7 +214,14 @@ export function QuestionPhoto({ return () => { unsubscribe?.(); }; - }, [question, setAnswerValue, registerPendingUpload, uploadTmpMediaMutation]); + }, [ + question, + setAnswerValue, + registerPendingUpload, + uploadTmpMediaMutation, + handleUploadFailure, + storedValue, + ]); const handleFlutterPick = useCallback(() => { const extensions = (question.extras?.options ?? []).map((o) => @@ -145,6 +230,8 @@ export function QuestionPhoto({ isInitiatorRef.current = true; setIsUploading(true); + setHasImageError(false); + setToastMessage(null); let resolveFn!: (val?: any) => void; let rejectFn!: (err?: any) => void; @@ -184,8 +271,28 @@ export function QuestionPhoto({ const file = files?.[0]; if (!file) return; - // Create synchronous object URL for instant UI preview only + // Validate size (10 MB limit) + if (file.size > 10_485_760) { + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + setToastMessage( + t["File size exceeds the 10 MB limit."] ?? + "File size exceeds the 10 MB limit.", + ); + return; + } + + // Reset previous errors + setHasImageError(false); + setToastMessage(null); + + // Create synchronous object URL for instant UI preview + if (activeBlobUrlRef.current) { + URL.revokeObjectURL(activeBlobUrlRef.current); + } const objectUrl = URL.createObjectURL(file); + activeBlobUrlRef.current = objectUrl; setLocalPreviewUrl(objectUrl); // Trigger background upload and register promise with central storage @@ -195,13 +302,18 @@ export function QuestionPhoto({ .then((res) => { if (res?.path) { setAnswerValue(question, res.path); - setLocalPreviewUrl(res.path); + setHasImageError(false); + if (!activeBlobUrlRef.current) { + setLocalPreviewUrl(res.path); + } + } else { + handleUploadFailure(); } setIsUploading(false); }) .catch((err) => { console.error("Photo upload error:", err); - setIsUploading(false); + handleUploadFailure(); }); registerPendingUpload?.(question.id, uploadPromise); @@ -215,27 +327,19 @@ export function QuestionPhoto({ }; // Determine photo URL to display - let displayUrl: string | null = localPreviewUrl; + let candidateUrl: string | null = localPreviewUrl; if ( - !displayUrl && + !candidateUrl && + !hasImageError && typeof storedValue === "string" && storedValue.trim().length > 0 ) { - const val = storedValue.trim(); - if ( - val.startsWith("http://") || - val.startsWith("https://") || - val.startsWith("blob:") || - val.startsWith("data:") - ) { - displayUrl = val; - } else { - displayUrl = getApiRequestUrl(val); - } + candidateUrl = storedValue.trim(); } - const hasAnswer = Boolean(displayUrl); + const displayUrl = hasImageError ? null : resolveMediaUrl(candidateUrl); + const hasAnswer = Boolean(displayUrl && !hasImageError); return (
+ {toastMessage && ( + setToastMessage(null)} + /> + )} + -
- {displayUrl ? ( +
+ {hasAnswer && displayUrl ? ( {typeof ) : ( @@ -283,8 +396,8 @@ export function QuestionPhoto({ )} {/* Red camera badge when image is uploaded */} - {displayUrl && ( - + {hasAnswer && !isPending && ( + )} - {/* Loading spinner during upload matching Flutter ProfileAvatarWidget */} + {/* Circular loading spinner during upload without black overlay */} {isPending && ( -
-
+
+
)}
- {descriptionContent ? ( - - {descriptionContent} - - ) : null}
diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index 4319497..327be2b 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -298,7 +298,7 @@ function SectionFlowContent({