You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

380 lines
13 KiB

"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { IoClose } from "react-icons/io5";
import { getSubmitPath } from "@/lib/get-submit-path";
import {
getLocalSectionProgress,
getStoredAge,
} from "@/components/Componentes/progress-helper";
import QuestionCard from "@/components/Componentes/question-card";
import RequiredStepsCard from "@/components/Componentes/required-steps-card";
import Button from "@/components/Componentes/button";
import InformationSheet from "@/components/Componentes/information-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import ErrorToast from "@/components/Componentes/error-toast";
import {
getQuestionListItems,
isQuestionListItemVisibleForProfile,
type QuestionListItem,
} from "@/data/question-data";
import { toFrontendSlug } from "@/data/section-slug-map";
import { useQueryClient } from "@tanstack/react-query";
import { useStartMarriageMatchMutation } from "@/hooks/marriage/use-match-start";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections";
import { useCloseServiceOnBack } from "@/hooks/use-close-service-on-back";
import {
clearMatchStartGrace,
markMatchStarted,
} from "@/lib/match-start-grace";
import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider";
import SectionsRequest from "./sections-request";
export default function QuestionsListPage() {
useCloseServiceOnBack();
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const queryClient = useQueryClient();
const { data: profile } = useMarriageProfileQuery();
const { data: sections } = useMarriageSectionsQuery({
refetchOnMount: "always",
});
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<QuestionListItem | null>(null);
const questionListItems = useMemo(
() =>
getQuestionListItems(locale).filter((item) =>
isQuestionListItemVisibleForProfile(item, {
gender: profile?.gender,
}),
),
[locale, profile?.gender],
);
const sectionProgressBySlug = useMemo(() => {
const progressBySlug = new Map<string, number>();
const age = getStoredAge();
sections?.forEach((section) => {
const frontendSlug = toFrontendSlug(section.slug);
const progress = Math.max(
0,
Math.min(100, Math.round(section.completion_percent)),
);
progressBySlug.set(frontendSlug, progress);
progressBySlug.set(section.slug, progress);
});
const fbSec = sections?.find((s) => s.slug === "family_background");
const mhSec = sections?.find((s) => s.slug === "marital_history");
if (fbSec || mhSec) {
const fbTotal = fbSec?.total_steps ?? 6;
const fbCurrent = fbSec
? Math.round((fbSec.completion_percent / 100) * fbTotal)
: 0;
const mhTotal = mhSec?.total_steps ?? 6;
const mhCurrent = mhSec
? Math.round((mhSec.completion_percent / 100) * mhTotal)
: 0;
const combinedProgress =
fbTotal + mhTotal > 0
? ((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100
: 0;
progressBySlug.set(
"family_marital_history",
Math.max(0, Math.min(100, Math.round(combinedProgress))),
);
}
questionListItems.forEach((item) => {
const localProgress = getLocalSectionProgress(item, profile, age);
if (localProgress !== null) {
progressBySlug.set(item.slug, localProgress);
}
});
return progressBySlug;
}, [sections, questionListItems, profile]);
const requiredQuestionListItems = useMemo(
() => questionListItems.filter((item) => Boolean(item.required)),
[questionListItems],
);
const allRequiredSectionsCompleted = useMemo(() => {
if (requiredQuestionListItems.length === 0) {
return false;
}
return requiredQuestionListItems.every((item) => {
const progress = sectionProgressBySlug.get(item.slug) ?? 0;
return progress >= 100;
});
}, [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<string | null>(null);
useEffect(() => {
if (startMatchMutation.isError || isSyncError) {
setToastMessage(t.questions.startMatchFailed);
}
}, [startMatchMutation.isError, isSyncError, t.questions.startMatchFailed]);
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 {
const slugsToCheck = [
"personal_info",
"contact_residence_family_communication",
"appearance_health_activity",
"education_career_economic_status",
"family_marital_history",
"beliefs_lifestyle_boundaries",
"future_spouse_criteria",
"identity_verification",
];
for (const slug of slugsToCheck) {
const rawValue = window.localStorage.getItem(
`marriage:sections:${slug}:answers`,
);
if (rawValue) {
const storedValue = JSON.parse(rawValue);
if (storedValue.pending_sync && storedValue.fields) {
const payload = {
current_step: storedValue.current_step,
fields: storedValue.fields,
};
const { updateMarriageSectionData } = await import(
"@/hooks/marriage/use-section-data"
);
await updateMarriageSectionData(slug, payload);
storedValue.pending_sync = false;
window.localStorage.setItem(
`marriage:sections:${slug}:answers`,
JSON.stringify(storedValue),
);
}
}
}
await queryClient.invalidateQueries();
startMatchMutation.mutate();
} catch (err) {
console.error("Failed to sync pending sections:", err);
setIsSyncError(true);
} finally {
setIsSyncing(false);
}
};
return (
<>
{toastMessage && (
<ErrorToast message={toastMessage} onClose={handleCloseToast} />
)}
{isOptionalInfoSheetOpen ? (
<InformationSheet
icon="warning"
title={t.questions.optionalInfoPromptTitle}
description={
<p className="px-0.5 text-sm leading-[1.45] text-[#2D2D2D]">
{t.questions.optionalInfoPromptDescription}
</p>
}
onClose={() => setIsOptionalInfoSheetOpen(false)}
buttons={({ close }) => (
<div className="grid w-full grid-cols-[1fr_2fr] gap-3">
<Button
className="rounded-[11px] border border-[#9B9B9B] bg-transparent py-[18px] text-[#8B8B8B]"
onClick={close}
variant="outlined"
>
{t.common.cancel}
</Button>
<Button
className="rounded-[11px] py-[18px]"
disabled={isStartMatchDisabled}
onClick={() => {
close();
handleStartMatch();
}}
>
{t.questions.findingMatch}
</Button>
</div>
)}
/>
) : null}
{selectedSection ? (
<InformationSheet
icon={null}
title={({ close }) => (
<span className="flex w-full items-start justify-between gap-3 text-left">
<span className="group-14 leading-5 font-bold tracking-normal text-[#8B8B8B]">
{selectedSection.title}
</span>
<button
type="button"
aria-label={`Close ${selectedSection.title} explanation`}
className="-mt-0.5 flex size-6 shrink-0 items-center justify-center text-[#8F8F8F]"
onClick={close}
>
<IoClose aria-hidden="true" className="text-[22px]" />
</button>
</span>
)}
description={
<p className="px-0.5 group-14 leading-[1.45] text-[#2D2D2D]">
{selectedSection.summary}
</p>
}
onClose={() => setSelectedSection(null)}
className="text-left"
/>
) : null}
<SectionsRequest />
<PageBackground disabled />
<main
style={{ paddingBottom: "calc(100px + var(--safe-bottom))" }}
className="-mx-[17px] relative min-h-screen overflow-x-clip bg-[#F5F5F5] px-[17px] pt-0"
>
<div className="pointer-events-none absolute inset-x-0 top-0 h-[240px] bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.98)_0%,rgba(255,255,255,0.7)_42%,rgba(255,255,255,0)_100%)]" />
<div className="pointer-events-none absolute -top-16 left-1/2 h-[220px] w-[220px] -translate-x-1/2 rounded-full bg-white/70 blur-3xl" />
<header
style={{ paddingTop: "calc(var(--safe-top) + 16px)" }}
className="sticky top-0 z-20 -mx-[17px] flex items-center justify-between bg-[#F5F5F5]/90 px-[17px] pb-3 backdrop-blur-md"
>
<NavigationButton
icon="back"
iconLabel={t.questions.closeQuestionsList}
onClick={(e) => {
e.preventDefault();
if (typeof window !== "undefined" && (window as any).HabibApp) {
(window as any).HabibApp.postMessage(
JSON.stringify({ action: "close_service" }),
);
} else {
router.push("/");
}
}}
/>
<h1 className="group-16 font-semibold text-[#151515]">
{t.questions.profileRegistration}
</h1>
<NavigationButton
icon="document"
iconLabel={t.common.support}
className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]"
/>
</header>
<div className="relative">
<div className="mt-4">
<RequiredStepsCard
items={questionListItems}
progressBySlug={sectionProgressBySlug}
/>
</div>
<section className="mt-5 space-y-3">
{questionListItems.map((item) => (
<QuestionCard
key={item.slug}
item={item}
progress={sectionProgressBySlug.get(item.slug) ?? null}
onInfoClick={(section) => setSelectedSection(section)}
/>
))}
</section>
</div>
<div
style={{ paddingBottom: "calc(16px + var(--safe-bottom))" }}
className="fixed inset-x-0 bottom-0 z-20 mx-auto w-full max-w-[375px] bg-[#F5F5F5]/95 px-[17px] pt-3 backdrop-blur-md"
>
<Button
aria-label={t.questions.findMatches}
disabled={isStartMatchDisabled}
isLoading={startMatchMutation.isPending || isSyncing}
onClick={() => {
if (hasIncompleteOptionalSections) {
setIsOptionalInfoSheetOpen(true);
return;
}
handleStartMatch();
}}
>
<span className="flex items-center justify-center gap-2.5">
<Image
src="/assets/images/noun-wedding-rings-6540466 1.svg"
alt=""
aria-hidden="true"
width={28}
height={28}
className="shrink-0"
/>
<span className="leading-none font-semibold">
{t.questions.findingMatch}
</span>
</span>
</Button>
</div>
</main>
</>
);
}