diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index 624c821..14ce94a 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -5,7 +5,17 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { useHabibWebReady } from "@/hooks/use-habib-web-ready"; -import { FaLock } from "react-icons/fa6"; +import { + FaCalendarDays, + FaLocationDot, + FaGraduationCap, + FaBookOpen, + FaBriefcase, + FaStar, + FaLock, +} from "react-icons/fa6"; +import { LuEye } from "react-icons/lu"; +import { IoHeartOutline } from "react-icons/io5"; import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card"; import MarriageAdvisorsOverlay, { useMarriageAdvisorsOverlay, @@ -56,6 +66,24 @@ const fieldCandidateMatchers = { "q1_full_name", ], age: ["age", "date_of_birth", "birth_date", "dob", "birth_year"], + currentCountry: [ + "country_of_current_residence", + "current_country", + "residence_country", + "country", + "current_residence_country", + ], + currentCity: [ + "city_state_of_current_residence", + "city_of_current_residence", + "current_city", + "residence_city", + "city", + "city_state", + "state_city", + "state", + "province", + ], residence: [ "current_residence", "residence", @@ -65,8 +93,7 @@ const fieldCandidateMatchers = { "residence_city", "city", "country", - "birth_city", - "birthplace", + "current_country", ], educationLevel: [ "highest_level_of_education", @@ -79,38 +106,21 @@ const fieldCandidateMatchers = { "study_field", "study", ], - job: [ + jobTitle: [ "job_title", "job_title_and_description", + "job_position", + "employment_status", "job", "occupation", "profession", "career", - "employment_status", - "work", ], hobbies: [ "your_hobbies_and_main_interests", "hobbies_and_interests", "hobbies", "interests", - "your_personality_traits", - "personality_traits", - ], - maritalStatus: [ - "current_marital_status", - "marital_status", - "maritalstatus", - "relationship_status", - ], - cityPreference: [ - "willingness_to_relocate", - "city_preference", - "citypreference", - "preferred_city", - "preferred_location", - "future_residence", - "residence_preference_after_marriage", ], } as const; @@ -288,107 +298,188 @@ function useMatchSummaryDisplay( age = pickField(fields, fieldCandidateMatchers.age, usedIndexes, t); } - // 3. Country / City / Residence - const residence = pickField( + // 3. Country of Current Residence + let currentCountry = pickField( fields, - fieldCandidateMatchers.residence, + fieldCandidateMatchers.currentCountry, usedIndexes, t, ); - // 4. Highest level of education - const educationLevel = pickField( + // 4. City / State of Current Residence + let currentCity = pickField( fields, - fieldCandidateMatchers.educationLevel, + fieldCandidateMatchers.currentCity, usedIndexes, t, ); - // 5. Field of study - const fieldOfStudy = pickField( - fields, - fieldCandidateMatchers.fieldOfStudy, - usedIndexes, - t, - ); + // If either country or city was not matched as a standalone field, check composite residence + if (!currentCountry || !currentCity) { + const residenceIdx = fields.findIndex( + (f, idx) => + !usedIndexes.has(idx) && + matchesCandidate(f, fieldCandidateMatchers.residence), + ); - // 6. Job / Occupation - const job = pickField( + if (residenceIdx !== -1) { + const residenceField = fields[residenceIdx]; + if ( + typeof residenceField.value === "object" && + residenceField.value !== null && + !Array.isArray(residenceField.value) + ) { + const valObj = residenceField.value as { + country?: string; + city?: string; + state?: string; + }; + if (valObj.country && !currentCountry) { + currentCountry = { + id: `${residenceField.key}.country`, + label: + (t && + (t["Current Country of Residence"] || + t["Country of Residence"] || + t["Country"])) || + "Country of Residence", + value: formatOptionValue(valObj.country, t) || valObj.country, + }; + } + const cityVal = [valObj.city, valObj.state].filter(Boolean).join(", "); + if (cityVal && !currentCity) { + currentCity = { + id: `${residenceField.key}.city`, + label: + (t && + (t["Current City / State of Residence"] || + t["City / State of Residence"] || + t["City"])) || + "City / State of Residence", + value: formatOptionValue(cityVal, t) || cityVal, + }; + } + usedIndexes.add(residenceIdx); + } else if (!currentCountry && !currentCity) { + const disp = toDisplayField(residenceField, t); + if (disp) { + usedIndexes.add(residenceIdx); + currentCountry = disp; + } + } + } + } + + // 5. Highest level of education + const educationLevel = pickField( fields, - fieldCandidateMatchers.job, + fieldCandidateMatchers.educationLevel, usedIndexes, t, ); - // 7. Hobbies & Interests - const hobbies = pickField( + // 6. Field of study + const fieldOfStudy = pickField( fields, - fieldCandidateMatchers.hobbies, + fieldCandidateMatchers.fieldOfStudy, usedIndexes, t, ); - // 8. Marital Status - const maritalStatus = pickField( + // 7. Job Title + const jobTitle = pickField( fields, - fieldCandidateMatchers.maritalStatus, + fieldCandidateMatchers.jobTitle, usedIndexes, t, ); - // 9. City Preference / Relocation - const cityPreference = pickField( + // 8. Hobbies & Main Interests + const hobbies = pickField( fields, - fieldCandidateMatchers.cityPreference, + fieldCandidateMatchers.hobbies, usedIndexes, t, ); - const extraFields = fields - .filter((_, index) => !usedIndexes.has(index)) - .map((f) => toDisplayField(f, t)) - .filter((field): field is DisplayField => Boolean(field)) - .slice(0, 4); - - if (typeof window !== "undefined") { - console.log( - "🔍 [useMatchSummaryDisplay] computed 8 fields output:", - { - displayName, - age, - residence, - educationLevel, - fieldOfStudy, - job, - hobbies, - maritalStatus, - cityPreference, - extraFieldsCount: extraFields.length, - }, - ); - } - return { + name: displayName, age, - residence, + currentCountry, + currentCity, educationLevel, fieldOfStudy, - job, + jobTitle, hobbies, - maritalStatus, - cityPreference, - name: displayName, - extraFields, }; }, [matchSummary, t]); } -function FieldLine({ field }: { field: DisplayField }) { +function ProfileInfoItem({ + icon, + field, +}: { + icon: React.ReactNode; + field: DisplayField; +}) { return ( -

- {field.label}: - {field.value} -

+
+
+ {icon} +
+ +
+ + {field.label} + + + + {field.value} + +
+
); } @@ -522,57 +613,59 @@ export default function NewMatchClient() {
-
+
-
+
{/* 1. Header Section Skeleton */}
- - - - + + +
{/* 2. Match Card Skeleton */} -
+
{/* Name line */} - + {/* Subtitle / Details lines */} -
+
{/* Button skeleton */} - +
{/* 3. Advisor Card Skeleton */}
-
- -
- - +
+ +
+ +
-
+
- - - - + + +
- +
@@ -582,16 +675,6 @@ export default function NewMatchClient() { ); } - const pairedPersonalFields = [ - matchDisplay.age, - matchDisplay.residence, - ].filter((field): field is DisplayField => Boolean(field)); - - const pairedEduFields = [ - matchDisplay.educationLevel, - matchDisplay.fieldOfStudy, - ].filter((field): field is DisplayField => Boolean(field)); - const isFemaleProfile = profile?.gender === "female"; const matchHeadingTitle = isFemaleProfile ? t["New Marriage Proposal"] @@ -617,33 +700,37 @@ export default function NewMatchClient() {
-
+
-
+
{/* 1. Header Section */}
-

+

{matchHeadingTitle}

-

+

{matchHeadingDescription}

@@ -651,98 +738,173 @@ export default function NewMatchClient() { {/* 2. Match Summary Card Section */}
{isLoading ? ( -
-
- {/* Name line */} - - - {/* Subtitle / Details lines */} -
- - -
+
+
+ +
+ +
- {/* Button skeleton */} - +
+ + +
+ +
+ {[1, 2, 3, 4, 5, 6, 7].map((i) => ( +
+ +
+ + +
+
+ ))} +
) : ( -
+
{isError ? ( -

+

Unable to load match summary.

) : matchSummary ? ( <> -

- {matchDisplay.name} -

- -
- {pairedPersonalFields.length ? ( -

- {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 ? ( - - ) : null} - - {matchDisplay.hobbies ? ( - - ) : null} - - {matchDisplay.maritalStatus ? ( - - ) : null} - {matchDisplay.cityPreference ? ( - - ) : null} + {/* Top Curved Banner */} +
+ {/* Bottom Curve */} + + +
- + {/* Circular Overlapping Avatar */} +
+
+ candidate avatar +
+
+ + {/* Candidate Name & Heart Divider */} +
+

+ {matchDisplay.name} +

+ +
+ + + +
+
+ + {/* Info Items List */} +
+ {matchDisplay.age && ( + } + /> + )} + + {matchDisplay.currentCountry && ( + } + /> + )} + + {matchDisplay.currentCity && ( + } + /> + )} + + {matchDisplay.educationLevel && ( + } + /> + )} + + {matchDisplay.fieldOfStudy && ( + } + /> + )} + + {matchDisplay.jobTitle && ( + } + /> + )} + + {matchDisplay.hobbies && ( + } + /> + )} + + {/* Button */} + +
) : ( -

+

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)["Completed"] || (locale === "fa" ? "تکمیل شده" : "Completed") + : hasTestProgress + ? t["Continue"] + : t["Start"] + } onStart={() => { + if (isAssessmentCompleted) { + setIsCompletedSheetOpen(true); + return; + } setIsTestStarted(true); }} > @@ -908,6 +950,15 @@ export default function QuestionDetailClient({
+ + { + setIsCompletedSheetOpen(false); + handleExit(); + }} + /> ); } diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx index ffb38ea..d8e08c2 100644 --- a/src/app/questions-list/questions-list-client.tsx +++ b/src/app/questions-list/questions-list-client.tsx @@ -56,13 +56,28 @@ import { fetchGeoCountryCode } from "@/components/Componentes/question-phone"; import SectionsRequest from "./sections-request"; import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; import QuestionDetailClient from "@/app/questions-list/[slug]/question-detail-client"; +import TestCompletedSheet from "@/components/Componentes/test-completed-sheet"; export default function QuestionsListClient() { + const [isTermsSheetOpen, setIsTermsSheetOpen] = useState(false); + const [completedTestSheet, setCompletedTestSheet] = useState<{ + isOpen: boolean; + title?: string; + }>({ isOpen: false, title: undefined }); + // Hardware back on the root questions list = close the Flutter service. // Unlike the old useCloseServiceOnBack, this does NOT push fake history // entries. Flutter calls __habibHandleHardwareBack() and we return false // (meaning "I didn't handle it — you should close"). useHardwareBackHandler(() => { + if (isTermsSheetOpen) { + setIsTermsSheetOpen(false); + return true; + } + if (completedTestSheet.isOpen) { + setCompletedTestSheet({ isOpen: false }); + return true; + } if (isOptionalInfoSheetOpen) { setIsOptionalInfoSheetOpen(false); return true; // Handled: closed the tips sheet @@ -235,6 +250,31 @@ export default function QuestionsListClient() { return progressBySlug; }, [overview, questionListItems, localAssessmentProgress]); + const isAssessmentSlug = useCallback( + (slug: string) => + slug === "personality_test" || + slug === "glasser_5_needs_test" || + slug === "personality" || + slug === "glasser" || + slug === "cattell", + [], + ); + + const handleCardSelect = useCallback( + (item: QuestionListItem) => { + const progress = sectionProgressBySlug.get(item.slug) ?? 0; + if (isAssessmentSlug(item.slug) && progress >= 100) { + setCompletedTestSheet({ + isOpen: true, + title: item.title, + }); + return; + } + handleOpenSection(item.slug); + }, + [handleOpenSection, isAssessmentSlug, sectionProgressBySlug], + ); + const requiredQuestionListItems = useMemo( () => questionListItems.filter((item) => Boolean(item.required)), [questionListItems], @@ -784,7 +824,11 @@ export default function QuestionsListClient() { className="text-left" /> ) : null} - + setIsTermsSheetOpen(false)} + /> {process.env.NODE_ENV === "development" ? : null} @@ -820,7 +864,9 @@ export default function QuestionsListClient() { setIsTermsSheetOpen(true)} className="shadow-[0_10px_26px_rgba(15,23,42,0.05)]" /> @@ -842,7 +888,7 @@ export default function QuestionsListClient() { onInfoClick={(section) => setSelectedSection(section)} onPrefetch={prefetchSection} onNearViewport={prefetchSection} - onSelect={(item) => handleOpenSection(item.slug)} + onSelect={handleCardSelect} /> ))}
@@ -899,6 +945,12 @@ export default function QuestionsListClient() { /> ) : null} + + setCompletedTestSheet({ isOpen: false })} + /> ); } diff --git a/src/app/questions-list/sections-request.tsx b/src/app/questions-list/sections-request.tsx index e559495..398f075 100644 --- a/src/app/questions-list/sections-request.tsx +++ b/src/app/questions-list/sections-request.tsx @@ -5,6 +5,7 @@ import { IoClose } from "react-icons/io5"; import Button from "@/components/Componentes/button"; import InformationSheet from "@/components/Componentes/information-sheet"; import type { FormOverviewSection } from "@/hooks/marriage/use-form-schema"; +import { useI18n } from "@/translations/provider"; const bookingTerms = [ "All provided information is held in strict confidence.", @@ -32,6 +33,7 @@ export default function SectionsRequest({ isOpen?: boolean; onClose?: () => void; }) { + const { dictionary: t } = useI18n(); const [hasSeenSheet, setHasSeenSheet] = useState(true); const [isAutoOpenDismissed, setIsAutoOpenDismissed] = useState(false); @@ -62,8 +64,7 @@ export default function SectionsRequest({ const isAutoOpen = Boolean(sections) && hasNoProgression && !hasSeenSheet && !isAutoOpenDismissed; - const isSheetOpen = - controlledIsOpen !== undefined ? controlledIsOpen : isAutoOpen; + const isSheetOpen = Boolean(controlledIsOpen) || isAutoOpen; const handleClose = () => { setIsAutoOpenDismissed(true); @@ -80,18 +81,21 @@ export default function SectionsRequest({ return null; } + const termsTitle = t["terms & conditions"] || "Terms & Conditions"; + const gotItLabel = t["Got it"] || "Got it"; + return ( ( - + - Terms & Conditions + {termsTitle} )} onClose={handleClose} - className="text-left" + className="text-start" /> ); } - diff --git a/src/components/Componentes/advisor-actions-card.tsx b/src/components/Componentes/advisor-actions-card.tsx index f1722a6..518453b 100644 --- a/src/components/Componentes/advisor-actions-card.tsx +++ b/src/components/Componentes/advisor-actions-card.tsx @@ -55,49 +55,49 @@ export function AdvisorActionsCard({ : fallbackExtraCount; return ( -
-
-

+
+
+

{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": "در مسیر آشنایی و خواستگاری هستیم و هنوز هیچی نهایی نشده",