From 130f518c454ce1f63e65c2fe91b25eb32bea843a Mon Sep 17 00:00:00 2001 From: ghorbani Date: Mon, 27 Jul 2026 14:10:26 +0330 Subject: [PATCH] feat: implement dynamic question components, slider functionality, and registration flow pages --- src/app/finding-match/page.tsx | 8 +- src/app/globals.css | 33 +++ src/app/intro/page.tsx | 18 +- .../[slug]/question-detail-client.tsx | 2 +- src/app/questions-list/page.tsx | 6 +- src/app/request-accepted/page.tsx | 20 +- src/app/request-sent/page.tsx | 10 +- src/components/questions/question-button.tsx | 2 +- src/components/questions/question-card.tsx | 10 +- .../questions/question-checkbox.tsx | 2 +- src/components/questions/question-date.tsx | 10 +- .../questions/question-dropdown.tsx | 10 +- src/components/questions/question-file.tsx | 233 ++++++++++++++---- src/components/questions/question-phone.tsx | 6 +- src/components/questions/question-photo.tsx | 231 ++++++++++++++--- .../questions/question-progress-tracker.tsx | 4 +- src/components/questions/question-radio.tsx | 2 +- src/components/questions/question-slider.tsx | 4 +- .../questions/question-snap-list.tsx | 4 +- src/components/questions/question-text.tsx | 10 +- src/components/questions/question-title.tsx | 8 +- .../questions/required-steps-card.tsx | 6 +- src/components/sliders/slider-slide-four.tsx | 10 +- src/components/sliders/slider-slide-one.tsx | 24 +- src/components/sliders/slider-slide-three.tsx | 8 +- src/components/sliders/slider-slide-two.tsx | 10 +- src/components/ui/advisor-actions-card.tsx | 6 +- src/components/ui/button.tsx | 6 +- src/components/ui/info-progress-card.tsx | 10 +- src/components/ui/information-sheet.tsx | 4 +- src/components/ui/sticky-header.tsx | 9 +- src/hooks/marriage/use-upload-tmp-media.ts | 1 - src/i18n/locales/en/questions.json | 100 ++------ src/i18n/locales/fa/questions.json | 100 ++------ 34 files changed, 572 insertions(+), 355 deletions(-) diff --git a/src/app/finding-match/page.tsx b/src/app/finding-match/page.tsx index 3cf8420..06c07a9 100644 --- a/src/app/finding-match/page.tsx +++ b/src/app/finding-match/page.tsx @@ -35,7 +35,7 @@ export default function FindingMatchPage() { >
-

{t.common.appName}

+

{t.common.appName}

@@ -51,11 +51,11 @@ export default function FindingMatchPage() { /> -

+

{copy.title}

-

+

{copy.description}

@@ -73,7 +73,7 @@ export default function FindingMatchPage() { className="inline-flex mt-3.5 w-full cursor-pointer items-center justify-center rounded-[9px] border-none bg-[#2B2C31] bg-none px-4 py-[17px] text-center text-white shadow-none transition-opacity" href={localizePath("/questions-list", locale)} > - + diff --git a/src/app/globals.css b/src/app/globals.css index 23f5425..118972e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -18,6 +18,39 @@ --font-faminela: var(--font-faminela-local); } +/* Typography Configuration - Centralized Design System Scale */ +@utility group-10 { + font-size: 10px; +} + +@utility group-12 { + font-size: 12px; +} + +@utility group-14 { + font-size: 14px; +} + +@utility group-16 { + font-size: 16px; +} + +.group-10 { + font-size: 10px; +} + +.group-12 { + font-size: 12px; +} + +.group-14 { + font-size: 14px; +} + +.group-16 { + font-size: 16px; +} + html { min-height: 100%; } diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx index b6d0c8a..1f3b845 100644 --- a/src/app/intro/page.tsx +++ b/src/app/intro/page.tsx @@ -144,7 +144,7 @@ export default function Intro() { )}
-

{t.common.appName}

+

{t.common.appName}

-

+

{t.intro.title}

-

+

{t.intro.description}

@@ -175,8 +175,8 @@ export default function Intro() { height={37} />
-

120

-

+

120

+

{t.intro.userProfile}

@@ -189,8 +189,8 @@ export default function Intro() { height={37} />
-

14

-

+

14

+

{t.intro.matches}

@@ -203,8 +203,8 @@ export default function Intro() { height={37} />
-

14

-

+

14

+

{t.intro.marriage}

diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 0a39fa7..7c32088 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -452,7 +452,7 @@ export default function QuestionDetailClient({ icon="close" iconLabel={closeLabel} /> -

+

{item.title}

( - + {selectedSection.title}
-

+

If they don’t contact you within 2 days, please inform us.

@@ -341,7 +341,7 @@ export default function RequestAcceptedPage() { alt="lock" /> -

+

Profile is locked

diff --git a/src/app/request-sent/page.tsx b/src/app/request-sent/page.tsx index c13f9f3..f1ccdee 100644 --- a/src/app/request-sent/page.tsx +++ b/src/app/request-sent/page.tsx @@ -27,7 +27,7 @@ export default function RequestSentPage() { >
-

Habib Marriage

+

Habib Marriage

@@ -44,16 +44,16 @@ export default function RequestSentPage() { /> -

+

Request Sent

-

+

We will propose marriage on your behalf. If they accept, their contact details will be shared with you.

-
+
Match Profile
@@ -78,7 +78,7 @@ export default function RequestSentPage() { alt="lock" /> -

+

Profile is locked

diff --git a/src/components/questions/question-button.tsx b/src/components/questions/question-button.tsx index 7698734..93f7fe9 100644 --- a/src/components/questions/question-button.tsx +++ b/src/components/questions/question-button.tsx @@ -42,7 +42,7 @@ export function QuestionButton({ type="button" disabled={disabled} onClick={() => setValue(true)} - className="min-h-[50px] rounded-[15px] bg-[#181818] px-5 py-3 text-[15px] font-semibold text-white" + className="min-h-[50px] rounded-[15px] bg-[#181818] px-5 py-3 group-14 font-semibold text-white" > {question.extras.options[0] || question.extras.placeHolder || diff --git a/src/components/questions/question-card.tsx b/src/components/questions/question-card.tsx index bc9e2f6..8aaf3cd 100644 --- a/src/components/questions/question-card.tsx +++ b/src/components/questions/question-card.tsx @@ -82,16 +82,16 @@ export function QuestionCard({
-

+

{item.title}

{item.required ? ( - + ({t.common.required}) ) : null}
-

+

{t.common.estimateTime}: {item.estimate}

@@ -151,7 +151,7 @@ export function QuestionCard({ /> )} - + {hasProgress ? `${normalizedProgress}%` : "..."} @@ -159,7 +159,7 @@ export function QuestionCard({ {item.note ? ( -

+

{item.note}

) : null} diff --git a/src/components/questions/question-checkbox.tsx b/src/components/questions/question-checkbox.tsx index 6078216..74f7d11 100644 --- a/src/components/questions/question-checkbox.tsx +++ b/src/components/questions/question-checkbox.tsx @@ -70,7 +70,7 @@ export function QuestionCheckbox({ htmlFor={optionId} className={[ "w-full cursor-pointer rounded-[12px] px-4 py-3.5", - "text-start text-[13px] leading-tight font-semibold transition-all duration-200", + "text-start group-14 leading-tight font-semibold transition-all duration-200", isSelected ? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" : "bg-[#F5F5F5] text-[#181818] hover:bg-[#EBEBEB]", diff --git a/src/components/questions/question-date.tsx b/src/components/questions/question-date.tsx index 88c2354..2181b99 100644 --- a/src/components/questions/question-date.tsx +++ b/src/components/questions/question-date.tsx @@ -102,7 +102,7 @@ export function QuestionDate({ onChange={(e) => handleSelectChange(year, month, e.target.value)} disabled={disabled} aria-label="روز" - className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" + className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" > {DAYS.map((d) => ( @@ -118,7 +118,7 @@ export function QuestionDate({ onChange={(e) => handleSelectChange(year, e.target.value, day)} disabled={disabled} aria-label="ماه" - className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" + className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" > {MONTHS.map((m) => ( @@ -134,7 +134,7 @@ export function QuestionDate({ onChange={(e) => handleSelectChange(e.target.value, month, day)} disabled={disabled} aria-label="سال" - className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" + className="h-[54px] w-full cursor-pointer rounded-[15px] border border-[#E7D8D5] bg-white px-3 group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" > {YEARS.map((y) => ( @@ -147,7 +147,7 @@ export function QuestionDate({ {/* Display Calculated Age */}
- + Age
diff --git a/src/components/questions/question-dropdown.tsx b/src/components/questions/question-dropdown.tsx index c421fc2..3bb7fab 100644 --- a/src/components/questions/question-dropdown.tsx +++ b/src/components/questions/question-dropdown.tsx @@ -94,7 +94,7 @@ export function QuestionDropdown({ type="button" disabled={disabled} onClick={() => setIsOpen(!isOpen)} - className="flex h-[55px] w-full items-center justify-between rounded-[11px] border border-[#BCBCBC] bg-white py-[10px] px-[10px] text-[14px] text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" + className="flex h-[55px] w-full items-center justify-between rounded-[11px] border border-[#BCBCBC] bg-white py-[10px] px-[10px] group-12 text-[#181818] outline-none transition-all focus:border-[#6F6F6F]" > {selectValue || question.extras.placeHolder || "Select"} @@ -148,13 +148,13 @@ export function QuestionDropdown({ value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} placeholder="Search..." - className="flex-1 bg-transparent text-[13px] text-[#181818] outline-none placeholder:text-[#9D8F8C]" + className="flex-1 bg-transparent group-12 text-[#181818] outline-none placeholder:text-[#9D8F8C]" /> {searchQuery ? ( @@ -192,14 +192,14 @@ export function QuestionDropdown({ : "border-[#4D4D4D] bg-transparent", ].join(" ")} /> - + {option} ); }) ) : ( - No options found + No options found )} diff --git a/src/components/questions/question-file.tsx b/src/components/questions/question-file.tsx index db58ffd..5580387 100644 --- a/src/components/questions/question-file.tsx +++ b/src/components/questions/question-file.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import { useCallback, useEffect, useState } from "react"; import type { QuestionField } from "@/data/question-data"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; +import { getApiRequestUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; @@ -37,23 +38,60 @@ function resolveMediaType( 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 function QuestionFile({ question, questionIndex, disabled, }: QuestionFileProps) { - const [selectedFileName, setSelectedFileName] = useState(null); + const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); + const storedValue = getAnswerValue(question, questionIndex); + + const initialFileName = + typeof storedValue === "string" && storedValue.trim().length > 0 + ? storedValue.split("/").pop() ?? storedValue + : null; + + const initialFileUrl = + typeof storedValue === "string" && storedValue.trim().length > 0 + ? storedValue.startsWith("http") || + storedValue.startsWith("blob:") || + storedValue.startsWith("data:") + ? storedValue + : getApiRequestUrl(storedValue) + : null; + + const [selectedFileName, setSelectedFileName] = useState( + initialFileName, + ); + const [filePreviewUrl, setFilePreviewUrl] = useState( + initialFileUrl, + ); const [isFlutterPicking, setIsFlutterPicking] = useState(false); - const acceptedFiles = question.extras.options + + const acceptedFiles = (question.extras?.options ?? []) .map((option) => option.replace(/^\./, "")) .join(", "); - const { setAnswerValue } = useQuestionAnswers(); + const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { - setAnswerValue(question, questionIndex, response.path); + if (response.path) { + setAnswerValue(question, questionIndex, response.path); + } }, - onError: () => { - setAnswerValue(question, questionIndex, null); + onError: (error) => { + console.error("File upload error:", error); }, }); @@ -72,28 +110,32 @@ export function QuestionFile({ break; case "picked": if (event.data?.files?.[0]) { - setSelectedFileName(event.data.files[0].name ?? null); + const fileName = event.data.files[0].name ?? null; + setSelectedFileName(fileName); + if (fileName) { + setAnswerValue(question, questionIndex, fileName); + } } break; case "progress": - // Could show progress bar – for now just keep pending state break; case "completed": { setIsFlutterPicking(false); const file = event.data?.files?.[0]; if (file?.url) { - // Flutter uploaded the file – use the returned URL setAnswerValue(question, questionIndex, file.url); setSelectedFileName(file.name ?? "uploaded"); + setFilePreviewUrl(file.url); } else if (file?.base64) { - // base64 mode – convert to File and use existing mutation - fetch(file.base64) + const b64 = file.base64; + setSelectedFileName(file.name ?? "upload"); + setFilePreviewUrl(b64); + fetch(b64) .then((res) => res.blob()) .then((blob) => { const f = new File([blob], file.name ?? "upload", { type: blob.type, }); - setSelectedFileName(f.name); uploadTmpMediaMutation.mutate(f); }); } @@ -112,12 +154,11 @@ export function QuestionFile({ return () => { unsubscribe?.(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [question, questionIndex]); + }, [question, questionIndex, setAnswerValue, uploadTmpMediaMutation]); /** Handle file pick in Flutter WebView via upload_file action. */ const handleFlutterPick = useCallback(() => { - const extensions = question.extras.options.map((o) => + const extensions = (question.extras?.options ?? []).map((o) => o.replace(/^\./, "").toLowerCase(), ); const mediaType = resolveMediaType(extensions); @@ -140,18 +181,52 @@ export function QuestionFile({ if (!file) { setSelectedFileName(null); + setFilePreviewUrl(null); setAnswerValue(question, questionIndex, null); return; } setSelectedFileName(file.name); + setAnswerValue(question, questionIndex, file.name); + + if (file.type.startsWith("image/")) { + const objectUrl = URL.createObjectURL(file); + setFilePreviewUrl(objectUrl); + } else { + setFilePreviewUrl(null); + } + uploadTmpMediaMutation.mutate(file); } + const handleRemoveFile = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + setSelectedFileName(null); + setFilePreviewUrl(null); + setAnswerValue(question, questionIndex, null); + }; + const inWebView = isInFlutterWebView(); + const isUploaded = Boolean(selectedFileName || storedValue); + const currentFileName = + selectedFileName ?? + (typeof storedValue === "string" ? storedValue.split("/").pop() : null); + const currentFileUrl = + filePreviewUrl ?? + (typeof storedValue === "string" && storedValue.trim().length > 0 + ? storedValue.startsWith("http") || + storedValue.startsWith("blob:") || + storedValue.startsWith("data:") + ? storedValue + : getApiRequestUrl(storedValue) + : null); + + const isImg = isImageFile(currentFileName, currentFileUrl); return (
{ if (e.key === "Enter" || e.key === " ") handleFlutterPick(); } : undefined } > - {/* Fallback: browser file input (hidden in WebView) */} - {!inWebView && ( + {/* Fallback: browser file input (hidden in WebView or when uploaded) */} + {!inWebView && !isUploaded && ( handleBrowserFileChange(event.target.files)} className="absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0" /> )} - Upload - - {isPending - ? "uploading..." - : (selectedFileName ?? "upload certificates")} - - {uploadTmpMediaMutation.isError ? ( - - Upload failed. Please try again. - - ) : acceptedFiles ? ( - - {acceptedFiles} - - ) : null} + + {isUploaded ? ( + /* ────── UPLOADED STATE ────── */ +
+ {isImg && currentFileUrl ? ( + /* Image Preview (Left design in screenshot) */ + {currentFileName + ) : ( + /* Document / PDF Preview (Right design in screenshot) */ +
+
+ + + + + + PDF + +
+ + {currentFileName ?? "document"} + +
+ )} + + {/* Trash Button in Bottom-Right */} + + + {isPending && ( +
+
+
+ )} +
+ ) : ( + /* ────── DEFAULT EMPTY STATE ────── */ + <> + Upload + + {isPending + ? "uploading..." + : (selectedFileName ?? "upload certificates")} + + {uploadTmpMediaMutation.isError ? ( + + Upload failed. Please try again. + + ) : acceptedFiles ? ( + + {acceptedFiles} + + ) : null} + + )}
); @@ -208,3 +355,5 @@ export function QuestionFile({ export default QuestionFile; + + diff --git a/src/components/questions/question-phone.tsx b/src/components/questions/question-phone.tsx index ce224e8..bfcc350 100644 --- a/src/components/questions/question-phone.tsx +++ b/src/components/questions/question-phone.tsx @@ -273,7 +273,7 @@ export function QuestionPhone({ setCodeValue(nextCodeValue); updateStoredValue(nextCodeValue, phoneValue); }} - className="w-[33px] border-0 bg-transparent p-0 text-left text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]" + className="w-[33px] border-0 bg-transparent p-0 text-left group-12 leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]" />
@@ -291,12 +291,12 @@ export function QuestionPhone({ updateStoredValue(codeValue, nextPhoneValue); }} dir="ltr" - className="h-full w-full border-0 bg-transparent p-0 text-left text-[14px] leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]" + className="h-full w-full border-0 bg-transparent p-0 text-left group-12 leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#9D8F8C]" />
{showInvalidState ? ( - + Enter a valid phone number with country code. ) : null} diff --git a/src/components/questions/question-photo.tsx b/src/components/questions/question-photo.tsx index c7374e6..9aa62b3 100644 --- a/src/components/questions/question-photo.tsx +++ b/src/components/questions/question-photo.tsx @@ -1,8 +1,11 @@ "use client"; import Image from "next/image"; -import type { ReactNode } from "react"; +import { useCallback, useEffect, useId, useState, type ReactNode } from "react"; import type { QuestionField } from "@/data/question-data"; +import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; +import { getApiRequestUrl } from "@/lib/http"; +import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; import { useQuestionAnswers } from "./question-answer-storage"; type QuestionPhotoProps = { @@ -12,65 +15,231 @@ type QuestionPhotoProps = { disabled?: boolean; }; -function getFileInputValue(files: FileList | null) { - const fileNames = Array.from(files ?? []).map((file) => file.name); - - return fileNames.length > 0 ? fileNames.join(", ") : null; -} - export function QuestionPhoto({ question, questionIndex, description, disabled, }: QuestionPhotoProps) { - const acceptedFiles = question.extras.options.join(","); + const inputId = useId(); + const [localPreviewUrl, setLocalPreviewUrl] = useState(null); + const [isFlutterPicking, setIsFlutterPicking] = useState(false); + + const acceptedFiles = + question.extras?.options && question.extras.options.length > 0 + ? question.extras.options.join(",") + : "image/*"; + const descriptionContent = description ?? question.description; - const { setAnswerValue } = useQuestionAnswers(); + const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); + const storedValue = getAnswerValue(question, questionIndex); + + const uploadTmpMediaMutation = useUploadTmpMediaMutation({ + onSuccess: (response) => { + if (response.path) { + setAnswerValue(question, questionIndex, response.path); + } + }, + onError: (error) => { + console.error("Photo upload error:", error); + }, + }); + + const isPending = uploadTmpMediaMutation.isPending || isFlutterPicking; + + // Listen for upload_file responses from Flutter WebView if active + useEffect(() => { + if (!isInFlutterWebView()) return; + + const unsubscribe = window.addFlutterResponseListener?.((event) => { + if (event.action !== "upload_file") return; + + switch (event.status) { + case "picking": + setIsFlutterPicking(true); + break; + case "picked": + if (event.data?.files?.[0]?.base64) { + const b64 = event.data.files[0].base64; + setLocalPreviewUrl(b64); + setAnswerValue(question, questionIndex, b64); + } + break; + case "completed": { + setIsFlutterPicking(false); + const file = event.data?.files?.[0]; + if (file?.url) { + setAnswerValue(question, questionIndex, file.url); + setLocalPreviewUrl(file.url); + } else if (file?.base64) { + const b64 = file.base64; + setLocalPreviewUrl(b64); + setAnswerValue(question, questionIndex, b64); + fetch(b64) + .then((res) => res.blob()) + .then((blob) => { + const f = new File([blob], file.name ?? "photo.jpg", { + type: blob.type || "image/jpeg", + }); + uploadTmpMediaMutation.mutate(f); + }); + } + break; + } + case "cancelled": + case "failed": + setIsFlutterPicking(false); + break; + } + }); + + return () => { + unsubscribe?.(); + }; + }, [question, questionIndex, setAnswerValue, uploadTmpMediaMutation]); + + const handleFlutterPick = useCallback(() => { + const extensions = (question.extras?.options ?? []).map((o) => + o.replace(/^\./, "").toLowerCase(), + ); + + uploadFile({ + mediaType: "image", + source: "gallery", + returnAs: "upload", + uploadUrl: `${process.env.NEXT_PUBLIC_API_BASE_URL ?? ""}/upload-tmp-media/`, + fieldName: "file", + allowedExtensions: extensions.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], + maxBytes: 10_485_760, // 10 MB + title: typeof question.title === "string" ? question.title : "Upload Photo", + }); + }, [question]); + + const handleBrowserFileChange = (files: FileList | null) => { + const file = files?.[0]; + if (!file) return; + + // Create synchronous object URL for instant preview & immediate state update + const objectUrl = URL.createObjectURL(file); + setLocalPreviewUrl(objectUrl); + setAnswerValue(question, questionIndex, objectUrl); + + // Trigger background upload + uploadTmpMediaMutation.mutate(file); + }; + + const handleLabelClick = (e: React.MouseEvent) => { + if (isInFlutterWebView()) { + e.preventDefault(); + handleFlutterPick(); + } + }; + + // Determine photo URL to display + let displayUrl: string | null = localPreviewUrl; + + if (!displayUrl && 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); + } + } + + const hasAnswer = Boolean(displayUrl); return (
setAnswerValue(question, questionIndex, getFileInputValue(event.target.files))} + onChange={(event) => handleBrowserFileChange(event.target.files)} disabled={disabled} className="sr-only" - id={`photo-input-${questionIndex}`} />