"use client"; 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 { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; import { LoadingSkeleton } from "./loading-skeleton"; type QuestionPhotoProps = { question: QuestionField; description?: ReactNode; disabled?: boolean; }; export function QuestionPhoto({ question, description, disabled, }: QuestionPhotoProps) { const inputId = useId(); const [localPreviewUrl, setLocalPreviewUrl] = useState(null); const [isUploading, setIsUploading] = 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); const isInitiatorRef = useRef(false); const pendingDeferredRef = useRef<{ resolve: (val?: any) => void; reject: (err?: any) => void; } | null>(null); const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { if (response.path) { setAnswerValue(question, response.path); setLocalPreviewUrl(response.path); } setIsUploading(false); }, onError: (error) => { console.error("Photo upload error:", error); setIsUploading(false); }, }); const isPending = uploadTmpMediaMutation.isPending || isUploading; // 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; if (event.requestId && String(event.requestId) !== String(question.id)) return; if (!event.requestId && !isInitiatorRef.current) return; switch (event.status) { case "picking": break; case "picked": case "progress": setIsUploading(true); if (event.data?.files?.[0]?.base64) { const b64 = event.data.files[0].base64; setLocalPreviewUrl(b64); } break; case "completed": { setIsUploading(false); isInitiatorRef.current = false; const file = event.data?.files?.[0]; const remoteUrl = file?.path || file?.url || (file?.data?.path as string | undefined) || (file?.data?.url as string | undefined); if (remoteUrl) { setAnswerValue(question, remoteUrl); setLocalPreviewUrl(remoteUrl); pendingDeferredRef.current?.resolve(remoteUrl); pendingDeferredRef.current = null; } else if (file?.base64) { const b64 = file.base64; setLocalPreviewUrl(b64); const uploadPromise = fetch(b64) .then((res) => res.blob()) .then((blob) => { const f = new File([blob], file.name ?? "photo.jpg", { type: blob.type || "image/jpeg", }); return uploadTmpMediaMutation.mutateAsync(f); }) .then((res) => { if (res?.path) { setAnswerValue(question, res.path); setLocalPreviewUrl(res.path); pendingDeferredRef.current?.resolve(res.path); } else { pendingDeferredRef.current?.resolve(); } pendingDeferredRef.current = null; }) .catch((err) => { pendingDeferredRef.current?.reject(err); pendingDeferredRef.current = null; }); registerPendingUpload?.(question.id, uploadPromise); } else { pendingDeferredRef.current?.resolve(); pendingDeferredRef.current = null; } break; } case "cancelled": case "failed": setIsUploading(false); isInitiatorRef.current = false; pendingDeferredRef.current?.reject(new Error("Upload failed or cancelled")); pendingDeferredRef.current = null; break; } }); return () => { unsubscribe?.(); }; }, [question, setAnswerValue, registerPendingUpload, uploadTmpMediaMutation]); const handleFlutterPick = useCallback(() => { const extensions = (question.extras?.options ?? []).map((o) => o.replace(/^\./, "").toLowerCase(), ); 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 || "gallery"; const pickerType = question.ui_config?.picker_type || question.ui_config?.source || "gallery"; uploadFile({ requestId: String(question.id), mediaType: "image", 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.length > 0 ? extensions : ["jpg", "jpeg", "png", "webp"], maxBytes: 10_485_760, // 10 MB title: typeof question.title === "string" ? question.title : "Upload Photo", }); }, [question, registerPendingUpload]); const handleBrowserFileChange = (files: FileList | null) => { const file = files?.[0]; if (!file) return; // Create synchronous object URL for instant UI preview only const objectUrl = URL.createObjectURL(file); setLocalPreviewUrl(objectUrl); // Trigger background upload and register promise with central storage setIsUploading(true); const uploadPromise = uploadTmpMediaMutation .mutateAsync(file) .then((res) => { if (res?.path) { setAnswerValue(question, res.path); setLocalPreviewUrl(res.path); } setIsUploading(false); }) .catch((err) => { console.error("Photo upload error:", err); setIsUploading(false); }); registerPendingUpload?.(question.id, uploadPromise); }; 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 (
handleBrowserFileChange(event.target.files)} disabled={disabled} className="sr-only" />