+
{matchHeadingTitle}
-
+
{matchHeadingDescription}
+
Unable to load match summary.
) : matchSummary ? ( <> -- {matchDisplay.name} -
- -- {pairedPersonalFields.map((field, index) => ( - - {index > 0 ? ( - | - ) : null} - - {field.label}:{" "} - - {field.value} - - ))} -
- ) : null} - - {pairedEduFields.length ? ( -- {pairedEduFields.map((field, index) => ( - - {index > 0 ? ( - | - ) : null} - - {field.label}:{" "} - - {field.value} - - ))} -
- ) : null} - - {matchDisplay.job ? ( -+ {matchDisplay.name} +
+ ++
No match summary is available yet.
)} diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 96feef0..1ec396f 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -21,6 +21,7 @@ import { parseValue as parseBirthplaceValue } from "@/components/Componentes/que import QuestionSectionFlow from "@/components/Componentes/question-section-flow"; import StickyHeader from "@/components/Componentes/sticky-header"; import TestIntroPage from "@/components/Componentes/test-intro-page"; +import TestCompletedSheet from "@/components/Componentes/test-completed-sheet"; import TestQuestionsFlow, { type TestQuestion, } from "@/components/Componentes/test-questions-flow"; @@ -307,10 +308,38 @@ export default function QuestionDetailClient({ const isGlasserSlug = itemSlug === "glasser_5_needs_test" || itemSlug === "glasser_test"; const isAssessment = isCattellSlug || isGlasserSlug; + const [isCompletedSheetOpen, setIsCompletedSheetOpen] = useState(false); + const { data: overview, isLoading: isOverviewLoading, isError: isOverviewError, refetch: refetchOverview } = useFormOverviewQuery( "profile", locale, ); + + const isAssessmentCompleted = useMemo(() => { + if (!isAssessment) return false; + if ( + overview?.progress?.sections_progress?.[itemSlug]?.completion_percent === + 100 + ) { + return true; + } + const currentOverviewItem = overview?.sections?.find( + (s) => s.id === itemSlug, + ); + if (currentOverviewItem?.progress?.completion_percent === 100) { + return true; + } + try { + const completionKey = getQuestionStorageKey(itemSlug, profileId); + if (completionKey && typeof window !== "undefined") { + const raw = window.localStorage.getItem(completionKey); + if (raw && JSON.parse(raw)?.completed === true) { + return true; + } + } + } catch {} + return false; + }, [isAssessment, overview, itemSlug, profileId]); const { data: sectionResponse, isLoading: isSectionLoading, isError: isSectionError, refetch: refetchSection } = useFormSectionQuery( "profile", @@ -715,7 +744,10 @@ export default function QuestionDetailClient({ questions={activeTestQuestions} closeLabel={closeLabel} informationLabel={informationLabel} - onClose={() => setIsTestStarted(false)} + onClose={() => { + setIsTestStarted(false); + handleExit(); + }} onFinish={handleTestFinish} draftStorageKey={getTestDraftStorageKey(item.slug, profileId)} /> @@ -792,8 +824,18 @@ export default function QuestionDetailClient({ "All provided information is held in strict confidence and will not be disclosed to third parties. This data is utilized exclusively to ensure optimal matchmaking accuracy. Consequently, it is imperative that this assessment be completed with the utmost diligence, precision, and integrity." ] } - startLabel={hasTestProgress ? t["Continue"] : t["Start"]} + startLabel={ + isAssessmentCompleted + ? (t as Record-
+
- {item} +
- {(t as Record
)[item] || item}
))}
-
{FIRST_ENTRY_TERMS.map((item, index) => (
-
+
+
+
{title}
-
+
{description}
-
-
+
+
{isLoading
? /* ── Shimmer circles while API loads ── */
Array.from({ length: 3 }).map((_, i) => (
))
: displayAvatars.map((avatar) => (
))}
{!isLoading && displayExtraCount > 0 && (
-
+
+{displayExtraCount}
)}
+ }
+ buttons={({ close }) => (
+
+ )}
+ closeOnOutside={closeOnOutside}
+ onClose={onClose}
+ />
+ );
+}
+
+export default TestExitSheet;
diff --git a/src/components/Componentes/test-questions-flow.tsx b/src/components/Componentes/test-questions-flow.tsx
index 6a4e3e3..59ebb77 100644
--- a/src/components/Componentes/test-questions-flow.tsx
+++ b/src/components/Componentes/test-questions-flow.tsx
@@ -1,8 +1,9 @@
"use client";
import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { GoArrowLeft, GoArrowRight } from "react-icons/go";
+import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
import { useI18n } from "@/translations/provider";
import Button from "./button";
@@ -10,6 +11,7 @@ import { ExplanationUiFont } from "./explanation-ui-font";
import NavigationButton from "./navigation-button";
import { PageBackground } from "./page-background";
import StickyHeader from "./sticky-header";
+import TestExitSheet from "./test-exit-sheet";
import TestLoadingScreen from "./test-loading-screen";
export type QuestionOption = {
@@ -37,36 +39,6 @@ type TestQuestionsFlowProps = {
onClose?: () => void;
draftStorageKey?: string | null;
};
-type StoredTestDraft = {
- answers?: Record;
- currentIndex?: number;
- totalQuestions?: number;
-};
-
-function getStoredDraft(
- storageKey: string | null | undefined,
- totalQuestions: number,
-) {
- if (!storageKey || typeof window === "undefined")
- return { answers: {}, currentIndex: 0 };
- try {
- const rawDraft = window.localStorage.getItem(storageKey);
- if (!rawDraft) return { answers: {}, currentIndex: 0 };
- const draft = JSON.parse(rawDraft) as StoredTestDraft;
- return {
- answers:
- draft.answers && typeof draft.answers === "object" ? draft.answers : {},
- currentIndex: Number.isInteger(draft.currentIndex)
- ? Math.min(
- Math.max(draft.currentIndex ?? 0, 0),
- Math.max(totalQuestions - 1, 0),
- )
- : 0,
- };
- } catch {
- return { answers: {}, currentIndex: 0 };
- }
-}
function formatOptionLabel(str: string): string {
if (!str) return str;
@@ -117,40 +89,17 @@ export default function TestQuestionsFlow({
}: TestQuestionsFlowProps) {
const router = useRouter();
const { locale } = useI18n();
- const [currentIndex, setCurrentIndex] = useState(
- () => getStoredDraft(draftStorageKey, questions.length).currentIndex,
- );
- const [answers, setAnswers] = useState>(
- () => getStoredDraft(draftStorageKey, questions.length).answers,
- );
+ const [currentIndex, setCurrentIndex] = useState(0);
+ const [answers, setAnswers] = useState>({});
const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isExitSheetOpen, setIsExitSheetOpen] = useState(false);
const isTargetTest =
!!draftStorageKey &&
(draftStorageKey.includes("personality_test") ||
draftStorageKey.includes("glasser_5_needs_test"));
- const [maxVisitedIndex, setMaxVisitedIndex] = useState(() => {
- const initialIndex = getStoredDraft(
- draftStorageKey,
- questions.length,
- ).currentIndex;
- const initialAnswers = getStoredDraft(
- draftStorageKey,
- questions.length,
- ).answers;
- let highestAnswered = -1;
- for (let i = 0; i < questions.length; i++) {
- if (initialAnswers[questions[i].id] !== undefined) {
- highestAnswered = i;
- }
- }
- const furthestReached =
- highestAnswered !== -1
- ? Math.min(highestAnswered + 1, questions.length - 1)
- : 0;
- return Math.max(initialIndex, furthestReached);
- });
+ const [maxVisitedIndex, setMaxVisitedIndex] = useState(0);
useEffect(() => {
if (Object.keys(answers).length === 0) {
@@ -160,35 +109,47 @@ export default function TestQuestionsFlow({
}
}, [currentIndex, answers, maxVisitedIndex]);
+ const handleRequestClose = useCallback(() => {
+ setIsExitSheetOpen(true);
+ }, []);
+
+ useHardwareBackHandler(() => {
+ if (isExitSheetOpen) {
+ setIsExitSheetOpen(false);
+ return true;
+ }
+ setIsExitSheetOpen(true);
+ return true;
+ }, true);
+
+ const handleConfirmExit = useCallback(() => {
+ if (draftStorageKey) {
+ try {
+ window.localStorage.removeItem(draftStorageKey);
+ } catch {}
+ }
+ if (typeof window !== "undefined") {
+ try {
+ window.localStorage.removeItem("marriage:tests:personality_test:draft");
+ window.localStorage.removeItem("marriage:tests:glasser_5_needs_test:draft");
+ } catch {}
+ }
+ setAnswers({});
+ setCurrentIndex(0);
+ setIsExitSheetOpen(false);
+ if (onClose) {
+ onClose();
+ } else {
+ router.back();
+ }
+ }, [draftStorageKey, onClose, router]);
+
const currentQuestion = questions[currentIndex] ?? questions[0];
const totalQuestions = questions.length;
const isLastQuestion = currentIndex === totalQuestions - 1;
const selectedValue = currentQuestion
? answers[currentQuestion.id]
: undefined;
- useEffect(() => {
- if (!draftStorageKey) return;
- try {
- const match = draftStorageKey.match(
- /^marriage:user:(\d+):tests:([^:]+):draft:v(\d+)$/,
- );
- const ownerProfileId = match ? Number(match[1]) : undefined;
- const version = match ? Number(match[3]) : undefined;
- const slug = match ? match[2] : undefined;
-
- window.localStorage.setItem(
- draftStorageKey,
- JSON.stringify({
- answers,
- currentIndex,
- totalQuestions,
- ...(ownerProfileId !== undefined ? { ownerProfileId } : {}),
- ...(version !== undefined ? { version } : {}),
- ...(slug !== undefined ? { slug } : {}),
- }),
- );
- } catch {}
- }, [answers, currentIndex, draftStorageKey, totalQuestions]);
const handleOptionSelect = (value: string | number) => {
if (!currentQuestion) return;
@@ -222,8 +183,22 @@ export default function TestQuestionsFlow({
if (onFinish) {
await onFinish(answers);
}
- if (draftStorageKey) window.localStorage.removeItem(draftStorageKey);
- router.back();
+ if (draftStorageKey) {
+ try {
+ window.localStorage.removeItem(draftStorageKey);
+ } catch {}
+ }
+ if (typeof window !== "undefined") {
+ try {
+ window.localStorage.removeItem("marriage:tests:personality_test:draft");
+ window.localStorage.removeItem("marriage:tests:glasser_5_needs_test:draft");
+ } catch {}
+ }
+ if (onClose) {
+ onClose();
+ } else {
+ router.back();
+ }
} catch {
// ignore
} finally {
@@ -263,7 +238,7 @@ export default function TestQuestionsFlow({
variant="transparent"
icon="close"
iconLabel={closeLabel}
- onClick={onClose}
+ onClick={handleRequestClose}
/>
{title}
@@ -312,7 +287,7 @@ export default function TestQuestionsFlow({
key={q.id}
aria-hidden={offset !== 0}
className={[
- "absolute inset-0 flex flex-col justify-start overflow-y-auto pt-9 pb-4 transition-transform duration-250 ease-[cubic-bezier(0.25,1,0.5,1)]",
+ "absolute inset-0 flex flex-col overflow-y-auto pt-4 pb-2 transition-transform duration-250 ease-[cubic-bezier(0.25,1,0.5,1)]",
offset === 0 ? "pointer-events-auto" : "pointer-events-none",
].join(" ")}
style={{
@@ -321,13 +296,13 @@ export default function TestQuestionsFlow({
>
{/* Question Title */}
{q.text}
{/* Answer Options Stack */}
-
+
{options.map((option) => {
const isSelected = qSelectedValue === option.value;
@@ -438,6 +413,13 @@ export default function TestQuestionsFlow({
+
+ setIsExitSheetOpen(false)}
+ onConfirmExit={handleConfirmExit}
+ />
>
);
}
+
diff --git a/src/lib/schema-adapter.ts b/src/lib/schema-adapter.ts
index 6fbbe76..e4ad2a3 100644
--- a/src/lib/schema-adapter.ts
+++ b/src/lib/schema-adapter.ts
@@ -46,6 +46,49 @@ export const sectionSlugIconMap: Record = {
glasser_test: "glasser",
};
+export const sectionEstimatedMinutesMap: Record = {
+ personal_info: 2,
+ personal_identity: 2,
+ contact_residence_family_communication: 2,
+ contact_residence: 2,
+ appearance_health_activity: 2,
+ appearance_health: 2,
+ education_career_economic_status: 3,
+ education_career: 3,
+ family_background: 3,
+ marital_history_children: 2,
+ marital_history: 2,
+ beliefs_lifestyle_boundaries: 4,
+ beliefs_lifestyle: 4,
+ personality_test: 16,
+ cattell_test: 16,
+ glasser_5_needs_test: 5,
+ glasser_test: 5,
+ future_spouse_criteria: 6,
+ spouse_criteria: 6,
+ identity_verification: 2,
+ documents_verification: 2,
+};
+
+export function resolveSectionEstimatedMinutes(
+ slug: string,
+ backendEstimatedMinutes?: number | null,
+): number {
+ if (
+ (slug === "personality_test" || slug === "cattell_test") &&
+ (!backendEstimatedMinutes || backendEstimatedMinutes === 8)
+ ) {
+ return 16;
+ }
+ if (typeof backendEstimatedMinutes === "number" && backendEstimatedMinutes > 0) {
+ return backendEstimatedMinutes;
+ }
+ if (slug && sectionEstimatedMinutesMap[slug]) {
+ return sectionEstimatedMinutesMap[slug];
+ }
+ return 3;
+}
+
export function resolveSectionIcon(
slug: string,
backendIcon?: string,
@@ -231,12 +274,15 @@ export function mapBackendSectionToFrontend(
});
});
+ const estMinutes = resolveSectionEstimatedMinutes(
+ section.id,
+ section.estimated_minutes,
+ );
+
return {
slug: section.id,
title: section.title,
- estimate: section.estimated_minutes
- ? `${section.estimated_minutes} min`
- : "5 min",
+ estimate: `${estMinutes} min`,
progress: progress,
icon: resolveSectionIcon(section.id, section.icon),
required: section.is_required,
@@ -273,19 +319,23 @@ export function convertOverviewToFrontendItems(
if (!overview) return [];
return [...overview.sections]
.sort((a, b) => a.order - b.order)
- .map((section) => ({
- slug: section.id,
- title: section.title,
- estimate: section.estimated_minutes
- ? `${section.estimated_minutes} min`
- : "5 min",
- progress: section.progress?.completion_percent ?? 0,
- icon: resolveSectionIcon(section.id, section.icon),
- required: section.is_required,
- showInfoBadge: false,
- summary: "",
- checkpoints: [],
- tooltip: "",
- questions: [],
- }));
+ .map((section) => {
+ const estMinutes = resolveSectionEstimatedMinutes(
+ section.id,
+ section.estimated_minutes,
+ );
+ return {
+ slug: section.id,
+ title: section.title,
+ estimate: `${estMinutes} min`,
+ progress: section.progress?.completion_percent ?? 0,
+ icon: resolveSectionIcon(section.id, section.icon),
+ required: section.is_required,
+ showInfoBadge: false,
+ summary: "",
+ checkpoints: [],
+ tooltip: "",
+ questions: [],
+ };
+ });
}
diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json
index 369f10a..e801963 100644
--- a/src/translations/locales/en.json
+++ b/src/translations/locales/en.json
@@ -690,10 +690,10 @@
"Very religious and committed": "Very religious and committed",
"View Contact": "View Contact",
"View Contact Details": "View Contact Details",
- "View More Details": "View More Details",
+ "View More Details": "View Full Profile & Proceed",
"View Profile": "View Profile",
"View contact number": "View contact number",
- "View more details": "View more details",
+ "View more details": "View Full Profile & Proceed",
"View profile": "View profile",
"Watch Video": "Watch Video",
"We are in the acquaintance/proposal process and nothing is finalized yet": "We are in the acquaintance/proposal process and nothing is finalized yet",
diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json
index 66f1725..6f76bef 100644
--- a/src/translations/locales/fa.json
+++ b/src/translations/locales/fa.json
@@ -690,10 +690,10 @@
"Very religious and committed": "بسیار مذهبی و مقید",
"View Contact": "مشاهده تماس",
"View Contact Details": "مشاهده شماره تماس",
- "View More Details": "مشاهده جزئیات بیشتر",
+ "View More Details": "مشاهده مشخصات کامل و ادامه فرایند",
"View Profile": "مشاهده پروفایل",
"View contact number": "مشاهده شماره تماس",
- "View more details": "مشاهده جزئیات بیشتر",
+ "View more details": "مشاهده مشخصات کامل و ادامه فرایند",
"View profile": "مشاهده پروفایل",
"Watch Video": "مشاهده ویدیو",
"We are in the acquaintance/proposal process and nothing is finalized yet": "در مسیر آشنایی و خواستگاری هستیم و هنوز هیچی نهایی نشده",
{title}
-+
{description}
-