"use client"; import { useQueryClient } from "@tanstack/react-query"; import Image from "next/image"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { IoClose } from "react-icons/io5"; import Button from "@/components/Componentes/button"; import DataErrorState from "@/components/Componentes/data-error-state"; import ErrorToast from "@/components/Componentes/error-toast"; import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; import InformationSheet from "@/components/Componentes/information-sheet"; import NavigationButton from "@/components/Componentes/navigation-button"; import { PageBackground } from "@/components/Componentes/page-background"; import { getQuestionAnswersStorageKey } from "@/components/Componentes/question-answer-storage"; import QuestionCard from "@/components/Componentes/question-card"; import RequiredStepsCard from "@/components/Componentes/required-steps-card"; import type { MarriageField } from "@/hooks/marriage/types"; import { marriageQueryKeys } from "@/hooks/marriage/query-keys"; import { getFormSection, useFormOverviewQuery, } from "@/hooks/marriage/use-form-schema"; import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { applyProfilePatchResultToCache, updateMarriageSectionData, } from "@/hooks/marriage/use-section-data"; import { useCloseServiceOnBack } from "@/hooks/use-close-service-on-back"; import { getAssessmentLocalProgress } from "@/lib/assessment-progress"; import { getSubmitPath, hasCompletedMarriageProfileBasics, } from "@/lib/get-submit-path"; import { REQUIRED_PROFILE_SECTION_COUNT } from "@/lib/marriage-profile-contract"; import { clearMatchStartGrace, markMatchStarted, } from "@/lib/match-start-grace"; import type { QuestionListItem } from "@/lib/schema-adapter"; import { convertOverviewToFrontendItems } from "@/lib/schema-adapter"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import { prefetchSectionsWithBoundedConcurrency } from "./section-prefetch"; import SectionsRequest from "./sections-request"; export default function QuestionsListPage() { useCloseServiceOnBack(); const { dictionary: t, locale } = useI18n(); const router = useRouter(); const queryClient = useQueryClient(); const { data: profile, isLoading: isProfileLoading, isError: isProfileError, refetch: refetchProfile } = useMarriageProfileQuery(); const { data: overview, isLoading: isSchemaLoading, isError: isSchemaError, refetch: refetchOverview } = useFormOverviewQuery( "profile", locale, ); const isSectionsLoading = false; const profileTargetPath = useMemo( () => (profile ? getSubmitPath(profile) : null), [profile], ); const isProfileRedirecting = profileTargetPath !== null && profileTargetPath !== "/questions-list" && (!hasCompletedMarriageProfileBasics(profile) || profile?.can_edit_profile === false); useEffect(() => { if (isProfileRedirecting && profileTargetPath) { router.replace(localizePath(profileTargetPath, locale)); } }, [isProfileRedirecting, locale, profileTargetPath, router]); const startMatchMutation = useStartMarriageMatchMutation({ onSuccess: () => { markMatchStarted(); router.push(localizePath("/finding-match", locale)); }, onError: () => { // Never pretend the request went through – the user stays here and can // retry instead of being parked on the waiting screen forever. clearMatchStartGrace(); }, }); const [isOptionalInfoSheetOpen, setIsOptionalInfoSheetOpen] = useState(false); const [selectedSection, setSelectedSection] = useState(null); const questionListItems = useMemo( () => convertOverviewToFrontendItems(overview), [overview], ); const [localAssessmentProgress, setLocalAssessmentProgress] = useState< Map >(new Map()); useEffect(() => { const next = new Map(); for (const slug of ["personality_test", "glasser_5_needs_test"]) { try { const raw = window.localStorage.getItem(`marriage:tests:${slug}:draft`); const draft = raw ? JSON.parse(raw) : null; const progress = getAssessmentLocalProgress(draft, false); if (progress > 0) next.set(slug, progress); } catch { // Ignore malformed local drafts. } } setLocalAssessmentProgress(next); }, [overview]); const sectionProgressBySlug = useMemo(() => { const progressBySlug = new Map(); if (overview?.progress?.sections_progress) { Object.entries(overview.progress.sections_progress).forEach( ([slug, prog]) => { progressBySlug.set( slug, Math.max(0, Math.min(100, Math.round(prog.completion_percent))), ); }, ); } localAssessmentProgress.forEach((progress, slug) => { if ((progressBySlug.get(slug) ?? 0) < 100) progressBySlug.set(slug, progress); }); // Add fallback for combined section from the schema adapter which attaches it to questionListItems directly questionListItems.forEach((item) => { if (!progressBySlug.has(item.slug)) { progressBySlug.set(item.slug, item.progress); } }); return progressBySlug; }, [overview, questionListItems, localAssessmentProgress]); const requiredQuestionListItems = useMemo( () => questionListItems.filter((item) => Boolean(item.required)), [questionListItems], ); const completedRequiredSections = useMemo( () => requiredQuestionListItems.filter( (item) => (sectionProgressBySlug.get(item.slug) ?? 0) >= 100, ).length, [requiredQuestionListItems, sectionProgressBySlug], ); const [displayedRequiredSections, setDisplayedRequiredSections] = useState( () => completedRequiredSections, ); useEffect(() => { if (displayedRequiredSections === completedRequiredSections) return; if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { setDisplayedRequiredSections(completedRequiredSections); return; } const direction = completedRequiredSections > displayedRequiredSections ? 1 : -1; const distance = Math.abs( completedRequiredSections - displayedRequiredSections, ); const interval = window.setInterval( () => { setDisplayedRequiredSections((current) => { const next = current + direction; if (next === completedRequiredSections) window.clearInterval(interval); return next; }); }, Math.max(90, Math.floor(500 / distance)), ); return () => window.clearInterval(interval); }, [completedRequiredSections, displayedRequiredSections]); const hasValidRequiredContract = requiredQuestionListItems.length === REQUIRED_PROFILE_SECTION_COUNT; useEffect(() => { if (overview && !hasValidRequiredContract) { console.error("Required section contract mismatch", { expected: REQUIRED_PROFILE_SECTION_COUNT, actual: requiredQuestionListItems.length, }); } }, [hasValidRequiredContract, overview, requiredQuestionListItems.length]); const allRequiredSectionsCompleted = useMemo(() => { if (!hasValidRequiredContract) { return false; } return requiredQuestionListItems.every((item) => { const progress = sectionProgressBySlug.get(item.slug) ?? 0; return progress >= 100; }); }, [ hasValidRequiredContract, requiredQuestionListItems, sectionProgressBySlug, ]); const profileStatus = profile?.status; const isProfileSuspended = profileStatus === "suspended"; const isProfileBlocked = profileStatus === "in_case" || profileStatus === "matched"; const canStartMatch = !isProfileSuspended && !isProfileBlocked && allRequiredSectionsCompleted; const [isSyncError, setIsSyncError] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const [toastMessage, setToastMessage] = useState(null); const syncPromiseRef = useRef | null>(null); const syncPendingAnswers = useCallback(async () => { if (!overview) return; if (syncPromiseRef.current) { return syncPromiseRef.current; } const task = (async () => { const pendingSections: Array<{ storageKey: string; storedValue: { current_step: number; fields: MarriageField[]; pending_keys: string[]; pending_sync: boolean; }; fields: MarriageField[]; slug: string; }> = []; for (const item of questionListItems) { if ( item.slug === "personality_test" || item.slug === "glasser_5_needs_test" ) { continue; } const storageKey = getQuestionAnswersStorageKey(item.slug); const rawValue = window.localStorage.getItem(storageKey); if (!rawValue) continue; let storedValue: any; try { storedValue = JSON.parse(rawValue); } catch { continue; } if ( !storedValue || !storedValue.pending_sync || !Array.isArray(storedValue.fields) ) { continue; } const pendingKeys = new Set( Array.isArray(storedValue.pending_keys) ? storedValue.pending_keys : storedValue.fields.map((field: { key: string }) => field.key), ); const pendingFields = storedValue.fields.filter( (field: { key: string }) => pendingKeys.has(field.key), ); if (pendingFields.length === 0) continue; pendingSections.push({ storageKey, storedValue, fields: pendingFields, slug: item.slug, }); } if (pendingSections.length === 0) return; const result = await updateMarriageSectionData(pendingSections[0].slug, { current_step: pendingSections[0].storedValue.current_step, fields: pendingSections.flatMap((section) => section.fields), version: overview.version, }); applyProfilePatchResultToCache(queryClient, locale, result); const cleared = new Set(result.cleared_answer_ids ?? []); for (const { storageKey, storedValue } of pendingSections) { storedValue.fields = storedValue.fields.filter( (field: { key: string }) => !cleared.has(field.key), ); storedValue.pending_sync = false; storedValue.pending_keys = []; if (storedValue.fields.length === 0) { window.localStorage.removeItem(storageKey); } else { window.localStorage.setItem(storageKey, JSON.stringify(storedValue)); } } })(); syncPromiseRef.current = task; try { await task; setIsSyncError(false); } finally { syncPromiseRef.current = null; } }, [locale, overview, queryClient, questionListItems]); const prefetchSection = useCallback( (item: QuestionListItem) => { const href = localizePath(`/questions-list/${item.slug}`, locale); router.prefetch(href); if ( item.slug === "personality_test" || item.slug === "glasser_5_needs_test" ) return; void queryClient.prefetchQuery({ queryKey: marriageQueryKeys.formSection("profile", item.slug, locale), queryFn: () => getFormSection("profile", item.slug, locale), staleTime: 30 * 1000, }); }, [locale, queryClient, router], ); const viewportPrefetchChain = useRef(Promise.resolve()); const viewportPrefetchSlugs = useRef(new Set()); const enqueueViewportPrefetch = useCallback( (item: QuestionListItem) => { if ( item.slug === "personality_test" || item.slug === "glasser_5_needs_test" || viewportPrefetchSlugs.current.has(item.slug) ) { return; } viewportPrefetchSlugs.current.add(item.slug); viewportPrefetchChain.current = viewportPrefetchChain.current .catch(() => undefined) .then(() => queryClient.fetchQuery({ queryKey: marriageQueryKeys.formSection( "profile", item.slug, locale, ), queryFn: () => getFormSection("profile", item.slug, locale), staleTime: 30 * 1000, }), ) .then(() => undefined); }, [locale, queryClient], ); const prefetchQueueStarted = useRef(false); useEffect(() => { if (prefetchQueueStarted.current || !overview) return; const profileSections = questionListItems .filter( (item) => item.slug !== "personality_test" && item.slug !== "glasser_5_needs_test", ) .sort((first, second) => { const firstPriority = first.required && (sectionProgressBySlug.get(first.slug) ?? first.progress) < 100 ? 0 : 1; const secondPriority = second.required && (sectionProgressBySlug.get(second.slug) ?? second.progress) < 100 ? 0 : 1; return firstPriority - secondPriority; }); if (profileSections.length === 0) return; prefetchQueueStarted.current = true; let cancelled = false; void prefetchSectionsWithBoundedConcurrency( profileSections, (item) => queryClient.fetchQuery({ queryKey: marriageQueryKeys.formSection("profile", item.slug, locale), queryFn: () => getFormSection("profile", item.slug, locale), staleTime: 30 * 1000, }), () => cancelled, ); return () => { cancelled = true; }; }, [locale, overview, queryClient, questionListItems, sectionProgressBySlug]); useEffect(() => { void syncPendingAnswers().catch((err) => { console.warn("Background draft sync:", err); }); const handleOnline = () => { void syncPendingAnswers().catch((err) => { console.warn("Background draft sync on online:", err); }); }; window.addEventListener("online", handleOnline); return () => window.removeEventListener("online", handleOnline); }, [syncPendingAnswers]); useEffect(() => { if (startMatchMutation.isError) { setToastMessage( t[ "Sending the match request failed. Please check your connection and try again." ], ); } }, [startMatchMutation.isError, t]); const handleCloseToast = () => { setToastMessage(null); setIsSyncError(false); startMatchMutation.reset(); }; const isStartMatchDisabled = startMatchMutation.isPending || isSyncing || !canStartMatch; const hasIncompleteOptionalSections = useMemo(() => { return questionListItems.some( (item) => !item.required && (sectionProgressBySlug.get(item.slug) ?? 0) < 100, ); }, [questionListItems, sectionProgressBySlug]); const handleStartMatch = async () => { if (isStartMatchDisabled || isSyncing) { return; } setIsSyncing(true); setIsSyncError(false); try { await syncPendingAnswers(); startMatchMutation.mutate(); } catch (err) { console.error("Failed to sync pending sections:", err); setIsSyncError(true); setToastMessage( t[ "Sending the match request failed. Please check your connection and try again." ] ?? "Sending the match request failed. Please check your connection and try again." ); } finally { setIsSyncing(false); } }; if ( isProfileLoading || isSectionsLoading || isSchemaLoading || isProfileRedirecting ) { return ( <>
{ e.preventDefault(); if (typeof window !== "undefined" && (window as any).HabibApp) { (window as any).HabibApp.postMessage( JSON.stringify({ action: "close_service" }), ); } else { router.push("/"); } }} />

{t["Profile registration"]}

{/* Section Card Skeletons (solid blocks like the Meet/checkup AppShimmer loading — one sweep band runs across each card) */}
{Array.from({ length: 6 }).map((_, idx) => (
))}
); } if (isProfileError || isSchemaError) { return ( <>
{ e.preventDefault(); if (typeof window !== "undefined" && (window as any).HabibApp) { (window as any).HabibApp.postMessage( JSON.stringify({ action: "close_service" }), ); } else { router.push("/"); } }} />

{t["Profile registration"]}

{ if (isProfileError) refetchProfile(); if (isSchemaError) refetchOverview(); }} />
); } return ( <> {toastMessage && ( )} {isOptionalInfoSheetOpen ? ( { t[ "You've completed all required fields. However, filling in all sections will help us find better matches for you" ] }

} onClose={() => setIsOptionalInfoSheetOpen(false)} buttons={({ close }) => (
)} /> ) : null} {selectedSection ? ( ( {selectedSection.title} )} description={

{selectedSection.summary}

} onClose={() => setSelectedSection(null)} className="text-left" /> ) : null}
{ e.preventDefault(); if (typeof window !== "undefined" && (window as any).HabibApp) { (window as any).HabibApp.postMessage( JSON.stringify({ action: "close_service" }), ); } else { router.push("/"); } }} />

{t["Profile registration"]}

{questionListItems.map((item) => ( setSelectedSection(section)} onNearViewport={enqueueViewportPrefetch} onPrefetch={prefetchSection} /> ))}
); }