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.
433 lines
16 KiB
433 lines
16 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 { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
|
|
import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton";
|
|
import { PageBackground } from "@/components/Componentes/page-background";
|
|
import ErrorToast from "@/components/Componentes/error-toast";
|
|
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 { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema";
|
|
import { convertSchemaToFrontendItems } from "@/lib/schema-adapter";
|
|
import type { QuestionListItem } from "@/data/question-data";
|
|
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 { triggerSilentReload } from "@/components/Componentes/silent-reloader";
|
|
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 } =
|
|
useMarriageProfileQuery();
|
|
const { data: sections, isLoading: isSectionsLoading } =
|
|
useMarriageSectionsQuery();
|
|
const { data: schema, isLoading: isSchemaLoading } =
|
|
useFormSchemaQuery("profile", locale);
|
|
|
|
useEffect(() => {
|
|
triggerSilentReload(queryClient);
|
|
}, [queryClient]);
|
|
|
|
|
|
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(
|
|
() => convertSchemaToFrontendItems(schema, locale),
|
|
[schema, locale],
|
|
);
|
|
|
|
const sectionProgressBySlug = useMemo(() => {
|
|
const progressBySlug = new Map<string, number>();
|
|
|
|
if (schema?.progress?.sections_progress) {
|
|
Object.entries(schema.progress.sections_progress).forEach(([slug, prog]) => {
|
|
progressBySlug.set(slug, Math.max(0, Math.min(100, Math.round(prog.completion_percent))));
|
|
});
|
|
}
|
|
|
|
// Add fallback for combined section from the schema adapter which attaches it to questionListItems directly
|
|
questionListItems.forEach((item) => {
|
|
progressBySlug.set(item.slug, item.progress);
|
|
});
|
|
|
|
return progressBySlug;
|
|
}, [schema, questionListItems]);
|
|
|
|
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[
|
|
"Sending the match request failed. Please check your connection and try again."
|
|
],
|
|
);
|
|
}
|
|
}, [
|
|
startMatchMutation.isError,
|
|
isSyncError,
|
|
t[
|
|
"Sending the match request failed. Please check your connection and try again."
|
|
],
|
|
]);
|
|
|
|
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);
|
|
}
|
|
};
|
|
|
|
if (isProfileLoading || isSectionsLoading || isSchemaLoading) {
|
|
return (
|
|
<>
|
|
<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["Close questions list"]}
|
|
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["Profile registration"]}
|
|
</h1>
|
|
<NavigationButton
|
|
icon="document"
|
|
iconLabel={t["Support"]}
|
|
className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]"
|
|
/>
|
|
</header>
|
|
|
|
<div className="relative mt-4 space-y-5">
|
|
{/* Required Steps Card Skeleton */}
|
|
<LoadingSkeleton className="h-[96px] w-full rounded-[15px]" />
|
|
|
|
{/* Section Cards Skeletons */}
|
|
<div className="space-y-3">
|
|
{Array.from({ length: 6 }).map((_, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center gap-3 rounded-[20px] border border-white/80 bg-white p-3 shadow-[0_12px_28px_rgba(15,23,42,0.05)]"
|
|
>
|
|
<LoadingSkeleton className="h-[44px] w-[44px] rounded-[12px]" />
|
|
<div className="flex-1 space-y-2">
|
|
<LoadingSkeleton className="h-4 w-[60%] rounded-md" />
|
|
<LoadingSkeleton className="h-3 w-[30%] rounded-md" />
|
|
</div>
|
|
<div className="flex flex-col items-end gap-3">
|
|
<LoadingSkeleton className="h-5 w-14 rounded-full" />
|
|
<LoadingSkeleton className="h-[22px] w-[22px] rounded-full" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<FixToTheEnd>
|
|
<LoadingSkeleton className="h-[52px] w-full rounded-[11px]" />
|
|
</FixToTheEnd>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{toastMessage && (
|
|
<ErrorToast message={toastMessage} onClose={handleCloseToast} />
|
|
)}
|
|
{isOptionalInfoSheetOpen ? (
|
|
<InformationSheet
|
|
icon="warning"
|
|
title={t["Important Note"]}
|
|
description={
|
|
<p className="px-0.5 text-sm leading-[1.45] text-[#2D2D2D]">
|
|
{
|
|
t[
|
|
"You've completed all required fields. However, filling in all sections will help us find better matches for you"
|
|
]
|
|
}
|
|
</p>
|
|
}
|
|
onClose={() => setIsOptionalInfoSheetOpen(false)}
|
|
buttons={({ close }) => (
|
|
<div className="grid w-full grid-cols-[33fr_67fr] gap-3">
|
|
<Button
|
|
className="rounded-[11px] border border-[#9B9B9B] bg-transparent text-[#8B8B8B] min-w-0"
|
|
onClick={close}
|
|
variant="outlined"
|
|
>
|
|
<span className="truncate">{t["Cancel"]}</span>
|
|
</Button>
|
|
<Button
|
|
className="rounded-[11px] min-w-0"
|
|
disabled={isStartMatchDisabled}
|
|
onClick={() => {
|
|
close();
|
|
handleStartMatch();
|
|
}}
|
|
>
|
|
<span className="truncate">{t["Submit"]}</span>
|
|
</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["Close questions list"]}
|
|
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["Profile registration"]}
|
|
</h1>
|
|
<NavigationButton
|
|
icon="document"
|
|
iconLabel={t["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>
|
|
|
|
<FixToTheEnd>
|
|
<Button
|
|
aria-label={t["Find Matches"]}
|
|
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["Submit"]}</span>
|
|
</span>
|
|
</Button>
|
|
</FixToTheEnd>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|