"use client"; import Image from "next/image"; import { useRouter } from "next/navigation"; import type { ReactNode } from "react"; import { useCallback, useState } from "react"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionProgressTracker, { useQuestionProgress, } from "./question-progress-tracker"; import QuestionSnapList from "./question-snap-list"; import type { QuestionField } from "@/lib/schema-adapter"; import ErrorToast from "./error-toast"; import NoticeBox from "./notice-box"; import AnswerPaceSheet from "@/app/questions-list/[slug]/answer-pace-sheet"; import { FixToTheEnd } from "./fix-to-the-end"; import Button from "./button"; import { markFirstEntryCompleted } from "@/lib/first-entry-helper"; import DevTapInstrumentation from "./dev-tap-instrumentation"; type QuestionSectionFlowProps = { children: ReactNode; continueLabel: string; exitHref: string; onExit?: () => void; total: number; optionalQuestionIndexes: readonly number[]; questions?: readonly QuestionField[]; }; function SectionFlowContent({ children, continueLabel, exitHref, onExit, optionalQuestionIndexes, questions, }: { children: ReactNode; continueLabel: string; exitHref: string; onExit?: () => void; optionalQuestionIndexes: readonly number[]; questions?: readonly QuestionField[]; }) { const router = useRouter(); const { dictionary: t, locale } = useI18n(); const { flushAnswers, isUploadingMedia } = useQuestionAnswers(); const { markQuestionPassed, isCompleted } = useQuestionProgress(); const [activeQuestionIndex, setActiveQuestionIndex] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const handleQuestionExit = useCallback(() => { void flushAnswers({ force: true }); }, [flushAnswers]); const handleSubmit = useCallback(async () => { if (isSubmitting) { return; } setIsSubmitting(true); setErrorMessage(null); const MAX_RETRIES = 3; let success = false; let lastError: any = null; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { await flushAnswers({ force: true }); success = true; break; } catch (err: any) { lastError = err; console.warn( `[CONTINUE] flushAnswers attempt ${attempt}/${MAX_RETRIES} failed:`, err, ); // On 400 Bad Request / Validation error, do not retry silently const statusCode = err?.response?.status || err?.status; if (statusCode === 400) { break; } if (attempt < MAX_RETRIES) { // Silent delay between retries while maintaining loading spinner await new Promise((resolve) => setTimeout(resolve, 800)); } } } if (success) { markFirstEntryCompleted(); if (onExit) { onExit(); } else { const target = localizePath(exitHref || "/questions-list", locale); router.replace(target); } } else { setIsSubmitting(false); const statusCode = lastError?.response?.status || lastError?.status; const data = lastError?.response?.data; let msg = ""; // 1. Detailed field validation errors from backend if (data?.errors && typeof data.errors === "object") { const errorEntries = Object.entries(data.errors); if (errorEntries.length > 0) { const fieldNames: string[] = []; for (const [key] of errorEntries) { const matchingQuestion = questions?.find( (q) => q.id === key || q.id.endsWith(`.${key}`) || key.endsWith(`.${q.id}`) || q.id.split(".").pop() === key.split(".").pop(), ); if (matchingQuestion?.title) { fieldNames.push(`«${matchingQuestion.title}»`); } else { fieldNames.push(`«${key}»`); } } if (fieldNames.length > 0) { const uniqueFieldNames = Array.from(new Set(fieldNames)); const template = t["Please complete the required fields: {fields}"] || "Please complete the required fields: {fields}"; msg = template.replace("{fields}", uniqueFieldNames.join("، ")); } } } // 2. Direct message / detail / error from backend if (!msg) { const directMessage = data?.detail || data?.message || data?.error; if (typeof directMessage === "string" && directMessage.trim()) { msg = directMessage; } } // 3. Fallback for 400 Bad Request if (!msg && statusCode === 400) { msg = t["Some required fields are missing or invalid. Please check your answers."] || "Some required fields are missing or invalid. Please check your answers."; } // 4. Fallback for 401 Unauthorized if (!msg && statusCode === 401) { msg = t["Session expired. Please log in again."] || "Session expired. Please log in again."; } // 5. Fallback for Network / Server Error if (!msg) { msg = t["Internet connection error. Answers could not be synced with server. Please check your connection and tap Continue again."] || "Internet connection error. Answers could not be synced with server. Please check your connection and tap Continue again."; } setErrorMessage(msg); } }, [exitHref, onExit, flushAnswers, locale, router, isSubmitting, t, questions]); const markOptionalQuestionsPassed = useCallback( (currentIndex: number, nextIndex: number) => { [currentIndex, nextIndex].forEach((questionIndex) => { if (optionalQuestionIndexes.includes(questionIndex)) { markQuestionPassed(questionIndex); } }); }, [markQuestionPassed, optionalQuestionIndexes], ); const activeQuestion = questions?.[activeQuestionIndex]; const showNotice = activeQuestion?.showGuardianNotice; const isUnder27 = activeQuestion?.required ?? false; return ( <> {showNotice && (
{isUnder27 ? t[ "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence." ] : t[ "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family." ]}
)} {process.env.NODE_ENV === "development" ? ( ) : null} {errorMessage && ( setErrorMessage(null)} duration={5000} variant="error" /> )} ); } export function QuestionSectionFlow({ children, continueLabel, exitHref, onExit, total, optionalQuestionIndexes, questions, }: QuestionSectionFlowProps) { return ( {children} ); } export default QuestionSectionFlow;