diff --git a/src/app/[lang]/marriage-advisors/page.tsx b/src/app/[lang]/marriage-advisors/page.tsx new file mode 100644 index 0000000..a81d1e3 --- /dev/null +++ b/src/app/[lang]/marriage-advisors/page.tsx @@ -0,0 +1 @@ +export { default } from "@/app/marriage-advisors/page"; diff --git a/src/app/candidate-contact/page.tsx b/src/app/candidate-contact/page.tsx index aefa130..48ba63d 100644 --- a/src/app/candidate-contact/page.tsx +++ b/src/app/candidate-contact/page.tsx @@ -119,7 +119,9 @@ export default function CandidateContactPage() {
@@ -133,7 +135,11 @@ export default function CandidateContactPage() { {t["Congratulations! 🎉"]}

- {t["Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed."]} + { + t[ + "Wishing you a lifetime of love, joy, and happiness. Your profile has been successfully closed." + ] + }

) : ( @@ -151,7 +157,11 @@ export default function CandidateContactPage() { {/* Title / Status message */}

- {t["The selected candidate will contact your family shortly."]} + { + t[ + "The selected candidate will contact your family shortly." + ] + }

{caseStatus === "contacted" ? ( @@ -159,7 +169,11 @@ export default function CandidateContactPage() { {/* Outcome instructions and button */}

- {t["Thank you for giving us feedback, we would be very happy if you also let us know the final result."]} + { + t[ + "Thank you for giving us feedback, we would be very happy if you also let us know the final result." + ] + }

@@ -230,7 +244,11 @@ export default function CandidateContactPage() { {/* Red warning box */}

- {t["To keep the process moving smoothly, the other party has a 48-hour (2-day) window to make initial contact with you or your family. If no contact is established after 2 days, you have the option to decline his request or notify us that he hasn't reached out."]} + { + t[ + "To keep the process moving smoothly, the other party has a 48-hour (2-day) window to make initial contact with you or your family. If no contact is established after 2 days, you have the option to decline his request or notify us that he hasn't reached out." + ] + }

@@ -243,11 +261,15 @@ export default function CandidateContactPage() { {/* Advisor section */} {/* Profile Locked banner */} diff --git a/src/app/finding-match/page.tsx b/src/app/finding-match/page.tsx index ab1f991..7e7b1f0 100644 --- a/src/app/finding-match/page.tsx +++ b/src/app/finding-match/page.tsx @@ -153,7 +153,7 @@ export default function FindingMatchPage() { avatars={advisorAvatars} extraCount={7} getAdvisorLabel={copy.getAdvisor} - getAdvisorHref="/questions-list" + getAdvisorHref="/marriage-advisors" /> diff --git a/src/app/globals.css b/src/app/globals.css index 3333b20..e5c92f1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -288,31 +288,40 @@ body[data-page-background="custom"] .app-shell { /* ─── Premium Shimmer Animations ─── */ @keyframes shimmer { 0% { - background-position: 200% 0; + background-position: -200% 0; } 100% { - background-position: -200% 0; + background-position: 200% 0; } } .shimmer-bg { - background-color: rgba(0, 0, 0, 0.05); - background-image: linear-gradient( + background: linear-gradient( 90deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.4) 50%, - rgba(255, 255, 255, 0) 100% + rgba(0, 0, 0, 0.05) 25%, + rgba(0, 0, 0, 0.1) 37%, + rgba(0, 0, 0, 0.05) 63% ); background-size: 200% 100%; - background-repeat: no-repeat; animation: shimmer 1.5s infinite linear; } .dark .shimmer-bg { - background-color: rgba(255, 255, 255, 0.04); - background-image: linear-gradient( + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.04) 25%, + rgba(255, 255, 255, 0.09) 37%, + rgba(255, 255, 255, 0.04) 63% + ); + background-size: 200% 100%; +} + +.shimmer-white-bg { + background: linear-gradient( 90deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.06) 50%, - rgba(255, 255, 255, 0) 100% + rgba(255, 255, 255, 0.12) 25%, + rgba(255, 255, 255, 0.24) 37%, + rgba(255, 255, 255, 0.12) 63% ); background-size: 200% 100%; + animation: shimmer 1.5s infinite linear; } + diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx index a7357ca..6a42bac 100644 --- a/src/app/intro/page.tsx +++ b/src/app/intro/page.tsx @@ -167,7 +167,11 @@ export default function Intro() { {t["A Path to Heavenly Marriage"]}

- {t["We have come together with the goal of creating a secure and confidential path for \"permanent marriage\" among Muslims"]} + { + t[ + 'We have come together with the goal of creating a secure and confidential path for "permanent marriage" among Muslims' + ] + }

@@ -180,7 +184,7 @@ export default function Intro() { />

120

-

+

{t["user profiles"]}

@@ -191,10 +195,11 @@ export default function Intro() { alt={t["matches"]} width={37} height={37} + priority />

14

-

+

{t["matches"]}

@@ -205,10 +210,11 @@ export default function Intro() { alt={t["marriages"]} width={37} height={37} + priority />

14

-

+

{t["marriages"]}

diff --git a/src/app/marriage-advisors/page.tsx b/src/app/marriage-advisors/page.tsx new file mode 100644 index 0000000..46c42dc --- /dev/null +++ b/src/app/marriage-advisors/page.tsx @@ -0,0 +1,234 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import { FiMessageSquare, FiPhone } from "react-icons/fi"; +import { FaStar } from "react-icons/fa"; +import StickyHeader from "@/components/Componentes/sticky-header"; +import NavigationButton from "@/components/Componentes/navigation-button"; +import { PageBackground } from "@/components/Componentes/page-background"; +import SupportSheet from "@/components/Componentes/support-sheet"; +import { useI18n } from "@/translations/provider"; + +export default function MarriageAdvisorsPage() { + const { dictionary: t } = useI18n(); + const [isSupportOpen, setIsSupportOpen] = useState(false); + + const mainStyle = { + backgroundImage: `url("/assets/images/islamic_pattern_2_2892_3864.svg")`, + backgroundColor: "#F5F5F5", + backgroundRepeat: "repeat-y", + backgroundSize: "100% auto", + }; + + const advisors = [ + { + id: "hasti-masoudi", + name: t["Dr. Hasti Masoudi"] || "Dr. Hasti Masoudi", + specialty: + t["Specialist in clinical psychology"] || + "Specialist in clinical psychology", + avatar: "/assets/images/female_avatar.svg", + rating: "3.2", + tagsRow1: [ + t["Personal Development"] || "Personal Development", + t["Team Manager"] || "Team Manager", + t["Science"] || "Science", + ], + tagsRow2: [ + t["Manager"] || "Manager", + t["Team Manager"] || "Team Manager", + t["Science"] || "Science", + ], + online: true, + }, + { + id: "ali-rezaei", + name: t["Dr. Ali Rezaei"] || "Dr. Ali Rezaei", + specialty: + t["Family & marriage counselor"] || "Family & marriage counselor", + avatar: "/assets/images/Avatar Image.png", + rating: "4.9", + tagsRow1: [ + t["Premarital Counselling"] || "Premarital Counselling", + t["Conflict Resolution"] || "Conflict Resolution", + ], + tagsRow2: [ + t["Behavioral Therapy"] || "Behavioral Therapy", + t["Self-Awareness"] || "Self-Awareness", + ], + online: true, + }, + { + id: "sara-alavi", + name: t["Dr. Sara Alavi"] || "Dr. Sara Alavi", + specialty: + t["Relationship & intimacy coach"] || "Relationship & intimacy coach", + avatar: "/assets/images/Ellipse 370.png", + rating: "4.7", + tagsRow1: [ + t["Emotional Health"] || "Emotional Health", + t["Communication Skills"] || "Communication Skills", + ], + tagsRow2: [ + t["Relationship Coach"] || "Relationship Coach", + t["Personal Development"] || "Personal Development", + ], + online: false, + }, + ]; + + return ( + <> + +
+ +
+ +

+ {t["Marriage advisors"]} +

+
+
+ + +
+ {advisors.map((advisor) => ( +
+ {/* Top Frame containing Left (Avatar + Info) and Right (Rating) and Actions */} +
+ {/* Header Row */} +
+ {/* Left part: Avatar + Info */} +
+ {/* Avatar Container */} +
+ {advisor.name} + {/* Status indicator dot */} + {advisor.online && ( +
+ + +
+ )} +
+ + {/* Name + Title Info */} +
+

+ {advisor.name} +

+

+ {advisor.specialty} +

+
+
+ + {/* Right part: Rating */} +
+ + + {advisor.rating} + +
+
+ + {/* Actions Row */} +
+ {/* Text Contact Button */} + + + {/* Voice Call Contact Button */} + +
+
+ + {/* Divider */} +
+ + {/* Tags Section */} +
+ {/* Row 1 tags */} +
+ {advisor.tagsRow1.map((tag, index) => ( + + {tag} + + ))} +
+ + {/* Row 2 tags */} +
+ {advisor.tagsRow2.map((tag, index) => ( + + {tag} + + ))} +
+
+
+ ))} +
+
+ + setIsSupportOpen(false)} + /> + + ); +} diff --git a/src/app/new-match/page.tsx b/src/app/new-match/page.tsx index 2844ad1..ec46360 100644 --- a/src/app/new-match/page.tsx +++ b/src/app/new-match/page.tsx @@ -355,8 +355,12 @@ export default function NewMatchPage() { ? t["New Marriage Proposal"] : t["YOU HAVE A NEW MATCH!"]; const matchHeadingDescription = isFemaleProfile - ? t["A suitable match has been found for you. If approved, your profile will be evaluated to proceed with the introduction process."] - : t["If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information."]; + ? t[ + "A suitable match has been found for you. If approved, your profile will be evaluated to proceed with the introduction process." + ] + : t[ + "If you proceed, we will notify the other party, and upon their approval, you can view each other's contact information." + ]; const isMale = profile?.gender === "male"; const hasActiveSub = !!profile?.active_subscription; @@ -458,7 +462,9 @@ export default function NewMatchPage() { if (isMale && !hasActiveSub) { setIsPaymentSheetOpen(true); } else { - router.push(localizePath("/new-match/profile", locale)); + router.push( + localizePath("/new-match/profile", locale), + ); } }} className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white h-[40px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors cursor-pointer" @@ -477,11 +483,15 @@ export default function NewMatchPage() {
@@ -539,7 +549,9 @@ export default function NewMatchPage() {

- {t["This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."] || + {t[ + "This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins." + ] || "This payment acts as a verification process for your account and activates a 3-month subscription to receive new case introductions. The cost of this subscription is 50 Habib Coins."}

@@ -553,7 +565,9 @@ export default function NewMatchPage() {

- {t["Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."] || + {t[ + "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users." + ] || "Please note that there is no guarantee for a specific number of cases, and the volume of incoming cases is solely subject to profile compatibility with other users."}

@@ -576,7 +590,9 @@ export default function NewMatchPage() { {respondMutation.isPending ? ( ) : ( - {t["Decline"] || "Decline"} + + {t["Decline"] || "Decline"} + )} @@ -594,7 +610,9 @@ export default function NewMatchPage() { ) : ( <> - {t["Pay"] || "Pay"} + + {t["Pay"] || "Pay"} + { - if (typeof window !== "undefined") { - (window as any).__queryClient = queryClient; - } - - let lastReloadTime = 0; - const handleReload = () => { - const now = Date.now(); - if (now - lastReloadTime < 5000) { - return; - } - lastReloadTime = now; - // Invalidate all active queries so fresh data is reloaded from backend - queryClient.invalidateQueries(); - }; - - const handleVisibilityChange = () => { - if (document.visibilityState === "visible") { - handleReload(); - } - }; - - const handlePageShow = () => { - handleReload(); - }; - - window.addEventListener("focus", handleReload); - window.addEventListener("pageshow", handlePageShow); - document.addEventListener("visibilitychange", handleVisibilityChange); - - // Listen for Flutter response events if available - const win = window as any; - const unbindFlutter = - typeof win.addFlutterResponseListener === "function" - ? win.addFlutterResponseListener(() => handleReload()) - : null; - - return () => { - window.removeEventListener("focus", handleReload); - window.removeEventListener("pageshow", handlePageShow); - document.removeEventListener("visibilitychange", handleVisibilityChange); - if (typeof unbindFlutter === "function") { - unbindFlutter(); - } - }; - }, [queryClient]); - - return <>{children}; -} +// AppFocusReloader was extracted to SilentReloader in silent-reloader.tsx type ProvidersProps = { children: ReactNode; @@ -72,12 +21,19 @@ export default function Providers({ children }: ProvidersProps) { new QueryClient({ defaultOptions: { queries: { + // Keep the last successful response available while a fresh + // request runs. `isLoading` is therefore only true on the first + // visit to a query; later visits render cached data immediately. refetchOnWindowFocus: "always", refetchOnMount: "always", refetchOnReconnect: "always", retry: 1, - staleTime: 0, // Always consider queries stale to ensure fresh data from backend on every page load - gcTime: 10 * 60 * 1000, // 10 minutes + staleTime: 0, + // Retain page data long enough for back-navigation to be instant. + gcTime: 10 * 60 * 1000, + // React Query replaces only the changed branches of a response, + // preventing needless UI updates when a silent refresh is equal. + structuralSharing: true, }, }, }), @@ -85,11 +41,11 @@ export default function Providers({ children }: ProvidersProps) { return ( - + {children} - + ); } diff --git a/src/app/questions-list/[slug]/page.tsx b/src/app/questions-list/[slug]/page.tsx index d4d88e6..bf80a28 100644 --- a/src/app/questions-list/[slug]/page.tsx +++ b/src/app/questions-list/[slug]/page.tsx @@ -47,7 +47,11 @@ export default async function QuestionDetailPage({ locale={locale} questionsListHref={questionsListHref} title={t["Answer at Your Own Pace"]} - description={t["You can pause the survey anytime and resume later. Your progress is saved automatically."]} + description={ + t[ + "You can pause the survey anytime and resume later. Your progress is saved automatically." + ] + } closeLabel={t["Close questions list"]} informationLabel={t["Information sheet"]} continueLabel={t["Submit"]} diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 66c92d6..474bd88 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -63,6 +63,10 @@ type StoredAnswers = { fields?: StoredQuestionField[]; }; +function getTestDraftStorageKey(slug: string) { + return `marriage:tests:${slug}:draft`; +} + function getQuestionStorageKey(slug: string) { return `marriage:sections:${slug}:answers`; } @@ -124,7 +128,7 @@ function getStoredAge() { return ( f.type === "number" || f.label === "Age" || - f.label === "سن" || + f.label === "ط³ظ†" || (typeof f.key === "string" && (f.key.endsWith("_age") || f.key.endsWith("_sn"))) ); @@ -139,7 +143,7 @@ function getStoredAge() { return ( f.type === "date" || f.label === "Date of Birth" || - f.label === "تاریخ تولد" || + f.label === "طھط§ط±غŒط® طھظˆظ„ط¯" || (typeof f.key === "string" && (f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))) ); @@ -498,6 +502,30 @@ export default function QuestionDetailClient({ const router = useRouter(); const { dictionary: t } = useI18n(); const [isTestStarted, setIsTestStarted] = useState(false); + const [hasTestProgress, setHasTestProgress] = useState(false); + + useEffect(() => { + if (typeof window !== "undefined") { + const draftKey = `marriage:tests:${itemSlug}:draft`; + const draftRaw = window.localStorage.getItem(draftKey); + if (draftRaw) { + try { + const parsed = JSON.parse(draftRaw); + if ( + parsed && + typeof parsed.answers === "object" && + parsed.answers !== null && + Object.keys(parsed.answers).length > 0 + ) { + setHasTestProgress(true); + return; + } + } catch {} + } + setHasTestProgress(false); + } + }, [itemSlug, isTestStarted]); + const { data: profile, isLoading: isProfileLoading } = useMarriageProfileQuery(); const profileGender = profile?.gender; @@ -538,7 +566,7 @@ export default function QuestionDetailClient({ q.options && q.options.length > 0 ? q.options : locale === "fa" - ? ["بله", "به اندازه کافی واضح نیست", "نه"] + ? ["ط¨ظ„ظ‡", "ط¨ظ‡ ط§ظ†ط¯ط§ط²ظ‡ ع©ط§ظپغŒ ظˆط§ط¶ط­ ظ†غŒط³طھ", "ظ†ظ‡"] : ["Yes", "Not clear enough", "No"]; const mappedOptions = rawOptions.map((optText, idx) => ({ label: optText, @@ -559,11 +587,17 @@ export default function QuestionDetailClient({ : glasserFallbackQuestions; const defaultGlasserOptions = [ - { label: locale === "fa" ? "خیلی کم (۱)" : "Very Low (1)", value: 1 }, - { label: locale === "fa" ? "کم (۲)" : "Low (2)", value: 2 }, - { label: locale === "fa" ? "متوسط (۳)" : "Moderate (3)", value: 3 }, - { label: locale === "fa" ? "زیاد (۴)" : "High (4)", value: 4 }, - { label: locale === "fa" ? "خیلی زیاد (۵)" : "Very High (5)", value: 5 }, + { + label: locale === "fa" ? "خیلی کم" : "Very Low", + value: 1, + }, + { label: locale === "fa" ? "کم" : "Low", value: 2 }, + { label: locale === "fa" ? "متوسط" : "Moderate", value: 3 }, + { label: locale === "fa" ? "زیاد" : "High", value: 4 }, + { + label: locale === "fa" ? "خیلی زیاد" : "Very High", + value: 5, + }, ]; return questionsList.map((q) => ({ @@ -585,14 +619,14 @@ export default function QuestionDetailClient({ } const hasDobQuestion = item.questions.some( - (q) => q.title === "Date of Birth" || q.title === "تاریخ تولد", + (q) => q.title === "Date of Birth" || q.title === "طھط§ط±غŒط® طھظˆظ„ط¯", ); return item.questions .filter((question) => { if ( hasDobQuestion && - (question.title === "Age" || question.title === "سن") + (question.title === "Age" || question.title === "ط³ظ†") ) { return false; } @@ -622,7 +656,12 @@ export default function QuestionDetailClient({ }, [isProfileLoading, item, profileContext, questionsListHref, router]); if (isProfileLoading && item) { - return ; + return ( + + ); } else if ( !item || !isQuestionListItemVisibleForProfile(item, profileContext) @@ -633,13 +672,13 @@ export default function QuestionDetailClient({ if (item && item.questions.length === 0) { if (isTestStarted) { const isQuestionsLoading = isCattellSlug - ? cattellQuery.isLoading || cattellQuery.isFetching + ? cattellQuery.isLoading : isGlasserSlug - ? glasserQuery.isLoading || glasserQuery.isFetching + ? glasserQuery.isLoading : false; if (isQuestionsLoading) { - return ; + return ; } const activeTestQuestions = isCattellSlug @@ -665,10 +704,10 @@ export default function QuestionDetailClient({

{isError ? locale === "fa" - ? "خطا در دریافت سوالات از سرور. لطفاً از اتصال اینترنت یا ورود به حساب کاربری اطمینان حاصل کنید." + ? "ط®ط·ط§ ط¯ط± ط¯ط±غŒط§ظپطھ ط³ظˆط§ظ„ط§طھ ط§ط² ط³ط±ظˆط±. ظ„ط·ظپط§ظ‹ ط§ط² ط§طھطµط§ظ„ ط§غŒظ†طھط±ظ†طھ غŒط§ ظˆط±ظˆط¯ ط¨ظ‡ ط­ط³ط§ط¨ ع©ط§ط±ط¨ط±غŒ ط§ط·ظ…غŒظ†ط§ظ† ط­ط§طµظ„ ع©ظ†غŒط¯." : "Failed to load questions from server. Please check your connection or login status." : locale === "fa" - ? "سوالاتی برای این آزمون یافت نشد." + ? "ط³ظˆط§ظ„ط§طھغŒ ط¨ط±ط§غŒ ط§غŒظ† ط¢ط²ظ…ظˆظ† غŒط§ظپطھ ظ†ط´ط¯." : "No questions found for this test."}

@@ -684,7 +723,7 @@ export default function QuestionDetailClient({ onClick={() => refetch()} className="rounded-xl bg-[#F2465F] px-4 py-2 text-sm font-semibold text-white shadow-md" > - {locale === "fa" ? "تلاش مجدد" : "Retry"} + {locale === "fa" ? "طھظ„ط§ط´ ظ…ط¬ط¯ط¯" : "Retry"}
@@ -700,11 +739,7 @@ export default function QuestionDetailClient({ question_number: Number(qNum), option: String(option), })); - try { - await submitCattellMutation.mutateAsync({ responses }); - } catch { - // Ignore if already submitted or API returned error - } + await submitCattellMutation.mutateAsync({ responses }); try { window.localStorage.setItem( getQuestionStorageKey(item.slug), @@ -716,11 +751,7 @@ export default function QuestionDetailClient({ question_number: Number(qNum), score: Number(score), })); - try { - await submitGlasserMutation.mutateAsync({ responses }); - } catch { - // Ignore if already submitted - } + await submitGlasserMutation.mutateAsync({ responses }); try { window.localStorage.setItem( getQuestionStorageKey(item.slug), @@ -739,21 +770,37 @@ export default function QuestionDetailClient({ informationLabel={informationLabel} onClose={() => setIsTestStarted(false)} onFinish={handleTestFinish} + draftStorageKey={getTestDraftStorageKey(item.slug)} /> ); } const bulletKey = item.slug === "glasser_5_needs_test" ? "glasser" : "personality"; - const bullets = bulletKey === "glasser" ? [ - t["Understanding your five basic needs helps you recognize what truly drives your behavior in relationships."], - t["Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse."], - t["By identifying your strongest needs, you can better communicate your expectations and build healthier relationships."] - ] : [ - t["Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?"], - t["Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse."], - t["Do you operate based on superficial behavioral adaptations, or are you aware of the deep \"source traits\" that fundamentally control your decision-making processes?"] - ]; + const bullets = + bulletKey === "glasser" + ? [ + t[ + "Understanding your five basic needs helps you recognize what truly drives your behavior in relationships." + ], + t[ + "Taking this test is not mandatory, but it will help you better understand your priorities and find a more compatible spouse." + ], + t[ + "By identifying your strongest needs, you can better communicate your expectations and build healthier relationships." + ], + ] + : [ + t[ + "Do you struggle to understand the underlying psychological drivers that dictate your interpersonal connections and communication barriers?" + ], + t[ + "Taking this test is not mandatory, but it will help you better search for a spouse. The Personality test is a test for self-knowledge and better understanding of your spouse." + ], + t[ + 'Do you operate based on superficial behavioral adaptations, or are you aware of the deep "source traits" that fundamentally control your decision-making processes?' + ], + ]; return ( <> @@ -786,13 +833,128 @@ export default function QuestionDetailClient({ title={item.title} estimateTime={item.estimate} description={t["Estimate time"]} - bulletPoints={bullets} - disclaimerText={t["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={t["Start"]} + bulletPoints={ + isCattellSlug || isGlasserSlug ? undefined : bullets + } + disclaimerText={ + t[ + "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"]} onStart={() => { setIsTestStarted(true); }} - /> + > + {isCattellSlug ? ( +
+ +
+ ) : isGlasserSlug ? ( +
+ +
+ ) : null} + diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index 2083d3f..32ddc56 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -146,9 +146,19 @@ export default function QuestionsListPage() { useEffect(() => { if (startMatchMutation.isError || isSyncError) { - setToastMessage(t["Sending the match request failed. Please check your connection and try again."]); + 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."]]); + }, [ + startMatchMutation.isError, + isSyncError, + t[ + "Sending the match request failed. Please check your connection and try again." + ], + ]); const handleCloseToast = () => { setToastMessage(null); @@ -304,7 +314,11 @@ export default function QuestionsListPage() { title={t["Important Note"]} description={

- {t["You've completed all required fields. However, filling in all sections will help us find better matches for you"]} + { + t[ + "You've completed all required fields. However, filling in all sections will help us find better matches for you" + ] + }

} onClose={() => setIsOptionalInfoSheetOpen(false)} @@ -439,9 +453,7 @@ export default function QuestionsListPage() { height={28} className="shrink-0" /> - - {t["Submit"]} - + {t["Submit"]} diff --git a/src/app/request-accepted/page.tsx b/src/app/request-accepted/page.tsx index edc3ff0..8724d5f 100644 --- a/src/app/request-accepted/page.tsx +++ b/src/app/request-accepted/page.tsx @@ -204,8 +204,6 @@ export default function RequestAcceptedPage() { } }, [profile, router, locale, noContactReportedSuccess]); - - const isRedirecting = useMemo(() => { if (!profile) return false; return getSubmitPath(profile) !== "/request-accepted"; @@ -497,7 +495,7 @@ export default function RequestAcceptedPage() {
-
+
{isFinalized ? (
@@ -566,127 +564,128 @@ export default function RequestAcceptedPage() { )} <> - {caseStatus === "contacted" || - isFemaleContactConfirmed || - (isFemaleProfile && contactStatusMutation.isPending) ? ( -
- {isFemaleProfile && - contactStatusMutation.isPending ? null : isFemaleProfile ? ( + {caseStatus === "contacted" || + isFemaleContactConfirmed || + (isFemaleProfile && contactStatusMutation.isPending) ? ( +
+ {isFemaleProfile && + contactStatusMutation.isPending ? null : isFemaleProfile ? ( + + ) : ( + <> - ) : ( - <> - - - - - )} -
- ) : ( -
- {isFemaleProfile ? ( + - ) : ( - -
- {primaryActionText} -
- - )} - + + )} +
+ ) : ( +
+ {isFemaleProfile ? ( -
- )} - - {caseStatus !== "contacted" && - !isFemaleContactConfirmed && - !noContactReportedSuccess && - !(isFemaleProfile && contactStatusMutation.isPending) ? ( -
-

- { - t[ - "Please be informed that from the time of this introduction, you have 48 hours (2 days) to contact the person or their respected family to declare your readiness and begin the acquaintance process. At this stage, merely an initial call to announce your presence is sufficient, and planning for further steps (such as an in-person meeting) depends entirely on your subsequent mutual agreements. Since failing to make contact within the specified time might be considered socially disrespectful, if no action is taken within these 2 days, the introduced match will be removed according to the platform's rules. We also remind you that this issue may lead to restrictions such as delays in future introductions and financial penalties." - ] - } -

-
- ) : null} - + ) : ( + +
+ {primaryActionText} +
+ + )} + + +
+ )} + + {caseStatus !== "contacted" && + !isFemaleContactConfirmed && + !noContactReportedSuccess && + !(isFemaleProfile && contactStatusMutation.isPending) ? ( +
+

+ { + t[ + "Please be informed that from the time of this introduction, you have 48 hours (2 days) to contact the person or their respected family to declare your readiness and begin the acquaintance process. At this stage, merely an initial call to announce your presence is sufficient, and planning for further steps (such as an in-person meeting) depends entirely on your subsequent mutual agreements. Since failing to make contact within the specified time might be considered socially disrespectful, if no action is taken within these 2 days, the introduced match will be removed according to the platform's rules. We also remind you that this issue may lead to restrictions such as delays in future introductions and financial penalties." + ] + } +

+
+ ) : null} + )}
@@ -703,7 +702,7 @@ export default function RequestAcceptedPage() { avatars={advisorAvatars} extraCount={7} getAdvisorLabel={t["Get Advisor"]} - getAdvisorHref="/questions-list" + getAdvisorHref="/marriage-advisors" />
diff --git a/src/app/request-sent/page.tsx b/src/app/request-sent/page.tsx index 74e7f13..4c6c934 100644 --- a/src/app/request-sent/page.tsx +++ b/src/app/request-sent/page.tsx @@ -47,12 +47,18 @@ export default function RequestSentPage() { const copy = { advisorTitle: t["Get an advisor"], - advisorDescription: t["Not sure what to do next? Our psychology section is here to guide you at every step."], + advisorDescription: + t[ + "Not sure what to do next? Our psychology section is here to guide you at every step." + ], getAdvisor: t["Get Advisor"], }; const requestSentCopy = { title: t["Request Sent"], - description: t["Your request has been sent. Once the lady reviews your request, you will be notified."], + description: + t[ + "Your request has been sent. Once the lady reviews your request, you will be notified." + ], matchProfile: t["View More Details"], profileLocked: t["Profile is locked"], }; @@ -105,7 +111,7 @@ export default function RequestSentPage() { avatars={advisorAvatars} extraCount={7} getAdvisorLabel={copy.getAdvisor} - getAdvisorHref="/questions-list" + getAdvisorHref="/marriage-advisors" />
diff --git a/src/components/Componentes/call-result-sheet.tsx b/src/components/Componentes/call-result-sheet.tsx index 5062423..feacb1c 100644 --- a/src/components/Componentes/call-result-sheet.tsx +++ b/src/components/Componentes/call-result-sheet.tsx @@ -31,7 +31,7 @@ export function CallResultSheet({ t["Different expectations"], t["No connection felt"], t["Location not suitable"], - t["Other reasons"] + t["Other reasons"], ]; const groupId = useId(); const [isVisible, setIsVisible] = useState(true); diff --git a/src/components/Componentes/dismiss-reason-sheet.tsx b/src/components/Componentes/dismiss-reason-sheet.tsx index 29e746b..6cb10ec 100644 --- a/src/components/Componentes/dismiss-reason-sheet.tsx +++ b/src/components/Componentes/dismiss-reason-sheet.tsx @@ -30,7 +30,7 @@ export function DismissReasonSheet({ t["Different expectations"], t["No connection felt"], t["Location not suitable"], - t["Other reasons"] + t["Other reasons"], ]; const groupId = useId(); const [isVisible, setIsVisible] = useState(true); @@ -154,7 +154,11 @@ export function DismissReasonSheet({

- {t["Please provide the full reason for rejecting the submitted item"]} + { + t[ + "Please provide the full reason for rejecting the submitted item" + ] + }

setReasonText(event.target.value) diff --git a/src/components/Componentes/female-outcome-sheet.test.tsx b/src/components/Componentes/female-outcome-sheet.test.tsx index 2e959e7..d2a325f 100644 --- a/src/components/Componentes/female-outcome-sheet.test.tsx +++ b/src/components/Componentes/female-outcome-sheet.test.tsx @@ -40,9 +40,7 @@ describe("FemaleOutcomeSheet", () => { it("keeps the profile unchanged when the acquaintance process is still ongoing", async () => { const onSubmit = renderSheet(); - await userEvent.click( - screen.getByRole("button", { name: "Continue" }), - ); + await userEvent.click(screen.getByRole("button", { name: "Continue" })); expect(onSubmit).toHaveBeenCalledWith("success"); }); diff --git a/src/components/Componentes/help-modal.tsx b/src/components/Componentes/help-modal.tsx index 0ee36a1..8f363bb 100644 --- a/src/components/Componentes/help-modal.tsx +++ b/src/components/Componentes/help-modal.tsx @@ -36,12 +36,11 @@ export function HelpModal({ const resolvedTitle = "Tips"; const resolvedDescription = description ?? - t["Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."] ?? + t[ + "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance." + ] ?? "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."; - const resolvedButtonText = - buttonText ?? - t["Got it"] ?? - "Got it"; + const resolvedButtonText = buttonText ?? t["Got it"] ?? "Got it"; const closeSheet = useCallback(() => { if (isClosing) return; diff --git a/src/components/Componentes/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx index 6f9846b..89f4fb2 100644 --- a/src/components/Componentes/information-sheet.tsx +++ b/src/components/Componentes/information-sheet.tsx @@ -288,7 +288,9 @@ export function InformationSheet({ {description ? ( -
{description}
+
+ {description} +
) : null} {resolvedButtons ? ( diff --git a/src/components/Componentes/loading-border-spinner.tsx b/src/components/Componentes/loading-border-spinner.tsx index 337ac23..c3d5af5 100644 --- a/src/components/Componentes/loading-border-spinner.tsx +++ b/src/components/Componentes/loading-border-spinner.tsx @@ -1,12 +1,19 @@ import type { ComponentProps } from "react"; -export function LoadingBorderSpinner({ className = "", ...props }: ComponentProps<"span">) { +export function LoadingBorderSpinner({ + className = "", + ...props +}: ComponentProps<"span">) { const hasBorder = className.split(" ").some((c) => c.startsWith("border-")); const borderClasses = hasBorder ? "" : "border-2 border-rose-500/25 border-t-rose-500 dark:border-rose-400/20 dark:border-t-rose-400"; - const hasSize = className.split(" ").some((c) => c.startsWith("size-") || c.startsWith("w-") || c.startsWith("h-")); + const hasSize = className + .split(" ") + .some( + (c) => c.startsWith("size-") || c.startsWith("w-") || c.startsWith("h-"), + ); const sizeClasses = hasSize ? "" : "size-5"; return ( diff --git a/src/components/Componentes/loading-icon-spinner.tsx b/src/components/Componentes/loading-icon-spinner.tsx index ec88fbe..d17d3d2 100644 --- a/src/components/Componentes/loading-icon-spinner.tsx +++ b/src/components/Componentes/loading-icon-spinner.tsx @@ -1,6 +1,9 @@ import type { ComponentProps } from "react"; -export function LoadingIconSpinner({ className = "", ...props }: ComponentProps<"svg">) { +export function LoadingIconSpinner({ + className = "", + ...props +}: ComponentProps<"svg">) { return ( - - + + ); } diff --git a/src/components/Componentes/loading-select-spinner.tsx b/src/components/Componentes/loading-select-spinner.tsx index c07333e..0b99311 100644 --- a/src/components/Componentes/loading-select-spinner.tsx +++ b/src/components/Componentes/loading-select-spinner.tsx @@ -1,4 +1,8 @@ -export function LoadingSelectSpinner({ className = "" }: { className?: string }) { +export function LoadingSelectSpinner({ + className = "", +}: { + className?: string; +}) { return ( c.startsWith("rounded-")); + const hasRoundedClass = className + .split(" ") + .some((c) => c.startsWith("rounded-")); const roundedClass = hasRoundedClass ? "" : "rounded-md"; return ( diff --git a/src/components/Componentes/loading-three-dot.tsx b/src/components/Componentes/loading-three-dot.tsx index 0ffba4b..c4e9dad 100644 --- a/src/components/Componentes/loading-three-dot.tsx +++ b/src/components/Componentes/loading-three-dot.tsx @@ -2,7 +2,10 @@ import type { ComponentProps } from "react"; interface LoadingThreeDotProps extends ComponentProps<"span"> {} -export function LoadingThreeDot({ className = "", ...props }: LoadingThreeDotProps) { +export function LoadingThreeDot({ + className = "", + ...props +}: LoadingThreeDotProps) { return (
- {isRenewing - ? t["Renewing..."] - : t["Renew Subscription"]} + {isRenewing ? t["Renewing..."] : t["Renew Subscription"]} {!isRenewing && ( @@ -186,7 +187,11 @@ export function NavigationButton({ setIsSubscriptionInfoOpen(false)} /> @@ -210,14 +215,13 @@ export function NavigationButton({ return () => document.removeEventListener("mousedown", handleOutsideClick); }, [isDropdownOpen]); - // پشتیبانی برای هم خانم‌ها و هم آقایان در هر وضعیتی فعال و در دسترس است - const hasSupportAccess = true; + const canAccessSupport = hasSupportAccess(profile); if (icon === "subscription" && isFemale) { return
; } - if (icon === "support" && !hasSupportAccess) { + if (icon === "support" && !canAccessSupport) { return
; } @@ -301,7 +305,9 @@ export function NavigationButton({ const resolvedHelpDescription = helpDescription ?? (icon === "document" - ? (t as any)["Taking this test is not mandatory, but it will help you better search for a spouse."] + ? (t as any)[ + "Taking this test is not mandatory, but it will help you better search for a spouse." + ] : undefined); if (icon === "more") { @@ -331,23 +337,25 @@ export function NavigationButton({ {isDropdownOpen && (
- + {canAccessSupport && ( + + )}
)}
+ ) : /* ────── DEFAULT EMPTY STATE ────── */ + isPending ? ( + ) : ( - /* ────── DEFAULT EMPTY STATE ────── */ - isPending ? ( - - ) : ( - <> - Upload - - {selectedFileName ?? "upload certificates"} + <> + Upload + + {selectedFileName ?? "upload certificates"} + + {uploadTmpMediaMutation.isError ? ( + + Upload failed. Please try again. - {uploadTmpMediaMutation.isError ? ( - - Upload failed. Please try again. - - ) : acceptedFiles ? ( - - {acceptedFiles} - - ) : null} - - ) + ) : acceptedFiles ? ( + + {acceptedFiles} + + ) : null} + )}
diff --git a/src/components/Componentes/question-number.tsx b/src/components/Componentes/question-number.tsx index 6df31a9..990f972 100644 --- a/src/components/Componentes/question-number.tsx +++ b/src/components/Componentes/question-number.tsx @@ -365,8 +365,9 @@ export default function QuestionNumber({
{isOutOfRange ? ( - {t["The value entered seems incorrect. Please provide a realistic value."] || - `Please enter a value between ${min} and ${max}`} + {t[ + "The value entered seems incorrect. Please provide a realistic value." + ] || `Please enter a value between ${min} and ${max}`} ) : null}
@@ -416,8 +417,9 @@ export default function QuestionNumber({ /> {isOutOfRange ? ( - {t["The value entered seems incorrect. Please provide a realistic value."] || - `Please enter a value between ${min} and ${max}`} + {t[ + "The value entered seems incorrect. Please provide a realistic value." + ] || `Please enter a value between ${min} and ${max}`} ) : null}
diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index 4811fab..adc1dd4 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -172,8 +172,12 @@ function SectionFlowContent({
{isUnder27 - ? t["To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence."] - : t["Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family."]} + ? t[ + "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence." + ] + : t[ + "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family." + ]}
)} diff --git a/src/components/Componentes/question-text.tsx b/src/components/Componentes/question-text.tsx index 0864b56..c145d66 100644 --- a/src/components/Componentes/question-text.tsx +++ b/src/components/Componentes/question-text.tsx @@ -133,7 +133,8 @@ export default function QuestionText({ className="h-[18px] w-[18px] shrink-0 rounded border-[#D0D5DD] text-[#F2465F] focus:ring-[#F2465F] accent-[#F2465F] cursor-pointer" /> - {t["This is not a priority for me"] ?? "This is not a priority for me"} + {t["This is not a priority for me"] ?? + "This is not a priority for me"} diff --git a/src/components/Componentes/question-title.tsx b/src/components/Componentes/question-title.tsx index f0ac019..5a671c4 100644 --- a/src/components/Componentes/question-title.tsx +++ b/src/components/Componentes/question-title.tsx @@ -80,8 +80,12 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) { onClose={() => setIsHelpOpen(false)} description={(() => { const fullPath = "questions." + question.tooltip; - const englishVal = (pathMap as Record)[fullPath]; - return englishVal ? (t as any)[englishVal] || englishVal : question.tooltip; + const englishVal = (pathMap as Record)[ + fullPath + ]; + return englishVal + ? (t as any)[englishVal] || englishVal + : question.tooltip; })()} /> diff --git a/src/components/Componentes/required-steps-card.tsx b/src/components/Componentes/required-steps-card.tsx index f1899c1..bfea451 100644 --- a/src/components/Componentes/required-steps-card.tsx +++ b/src/components/Componentes/required-steps-card.tsx @@ -142,8 +142,12 @@ export default function RequiredStepsCard({

{isCompleted - ? t["You can now submit your request so we can start finding the right match for you"] - : t["Please complete the required information so we can find suitable matches for you"]} + ? t[ + "You can now submit your request so we can start finding the right match for you" + ] + : t[ + "Please complete the required information so we can find suitable matches for you" + ]}

diff --git a/src/components/Componentes/silent-reloader.tsx b/src/components/Componentes/silent-reloader.tsx new file mode 100644 index 0000000..71fe8aa --- /dev/null +++ b/src/components/Componentes/silent-reloader.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { useQueryClient } from "@tanstack/react-query"; +import { type ReactNode, useEffect } from "react"; + +type SilentReloaderProps = { + children: ReactNode; +}; + +export default function SilentReloader({ children }: SilentReloaderProps) { + const queryClient = useQueryClient(); + + useEffect(() => { + if (typeof window !== "undefined") { + (window as any).__queryClient = queryClient; + } + + let lastReloadTime = 0; + const handleReload = () => { + const now = Date.now(); + if (now - lastReloadTime < 5000) { + return; + } + lastReloadTime = now; + // Invalidate all active queries so fresh data is reloaded from backend + queryClient.invalidateQueries(); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") { + handleReload(); + } + }; + + const handlePageShow = () => { + handleReload(); + }; + + window.addEventListener("focus", handleReload); + window.addEventListener("pageshow", handlePageShow); + document.addEventListener("visibilitychange", handleVisibilityChange); + + // Listen for Flutter response events if available + const win = window as any; + const unbindFlutter = + typeof win.addFlutterResponseListener === "function" + ? win.addFlutterResponseListener(() => handleReload()) + : null; + + return () => { + window.removeEventListener("focus", handleReload); + window.removeEventListener("pageshow", handlePageShow); + document.removeEventListener("visibilitychange", handleVisibilityChange); + if (typeof unbindFlutter === "function") { + unbindFlutter(); + } + }; + }, [queryClient]); + + return <>{children}; +} diff --git a/src/components/Componentes/support-access.ts b/src/components/Componentes/support-access.ts new file mode 100644 index 0000000..1ad455e --- /dev/null +++ b/src/components/Componentes/support-access.ts @@ -0,0 +1,23 @@ +import type { MarriageProfile } from "@/hooks/marriage/types"; + +/** + * Support is unavailable until a user has finished their profile. Women gain + * access on the waiting screen; men additionally need a currently valid + * subscription. + */ +export function hasSupportAccess(profile?: MarriageProfile): boolean { + if (!profile || !["waiting", "in_case", "matched"].includes(profile.status)) { + return false; + } + + if (profile.gender === "female") { + return true; + } + + const subscription = profile.active_subscription; + return ( + profile.gender === "male" && + subscription?.is_active === true && + subscription.is_valid === true + ); +} diff --git a/src/components/Componentes/support-sheet.tsx b/src/components/Componentes/support-sheet.tsx index cfec2ea..a490c2f 100644 --- a/src/components/Componentes/support-sheet.tsx +++ b/src/components/Componentes/support-sheet.tsx @@ -137,7 +137,11 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) {

- {t["If you encounter any issues, please feel free to contact our support specialists in WhatsApp"]} + { + t[ + "If you encounter any issues, please feel free to contact our support specialists in WhatsApp" + ] + }

diff --git a/src/components/Componentes/swipe-button.tsx b/src/components/Componentes/swipe-button.tsx index 50c9f03..424c0fd 100644 --- a/src/components/Componentes/swipe-button.tsx +++ b/src/components/Componentes/swipe-button.tsx @@ -63,11 +63,7 @@ export function SwipeButton({ } // Single Button full width - return ( -
- {actionButton} -
- ); + return
{actionButton}
; } export default SwipeButton; diff --git a/src/components/Componentes/test-intro-page.tsx b/src/components/Componentes/test-intro-page.tsx index 1b787de..656cfe1 100644 --- a/src/components/Componentes/test-intro-page.tsx +++ b/src/components/Componentes/test-intro-page.tsx @@ -1,6 +1,6 @@ "use client"; -import Image from "next/image"; +import type { ReactNode } from "react"; import { IoTimeOutline } from "react-icons/io5"; import Button from "./button"; @@ -8,39 +8,38 @@ type TestIntroPageProps = { title: string; estimateTime: string; description: string; - bulletPoints: readonly string[]; + bulletPoints?: readonly string[]; disclaimerText: string; startLabel: string; onStart: () => void; + children?: ReactNode; }; export default function TestIntroPage({ title, estimateTime, description, - bulletPoints, + bulletPoints = [], disclaimerText, startLabel, onStart, + children, }: TestIntroPageProps) { return (
{/* Hero Image */} -
- +
{/* Disclaimer Card */}
-

+

{disclaimerText}

@@ -60,7 +59,9 @@ export default function TestIntroPage({
{/* Bullet Points */} - {bulletPoints.length > 0 ? ( + {children ? ( +
{children}
+ ) : bulletPoints.length > 0 ? (
    {bulletPoints.map((point, index) => (
  • - + ); diff --git a/src/components/Componentes/test-questions-flow.tsx b/src/components/Componentes/test-questions-flow.tsx index b3042b9..0f9b188 100644 --- a/src/components/Componentes/test-questions-flow.tsx +++ b/src/components/Componentes/test-questions-flow.tsx @@ -1,13 +1,14 @@ "use client"; import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { GoArrowLeft, GoCheck } from "react-icons/go"; -import { IoInformation } from "react-icons/io5"; +import { useEffect, useState } from "react"; +import { GoArrowLeft } from "react-icons/go"; + import Button from "./button"; +import { ExplanationUiFont } from "./explanation-ui-font"; import NavigationButton from "./navigation-button"; -import StickyHeader from "./sticky-header"; import { PageBackground } from "./page-background"; +import StickyHeader from "./sticky-header"; import TestLoadingScreen from "./test-loading-screen"; export type QuestionOption = { @@ -32,8 +33,73 @@ type TestQuestionsFlowProps = { stepsLabel?: string; onFinish?: (answers: Record) => void; onClose?: () => void; + draftStorageKey?: string; +}; +type StoredTestDraft = { + answers?: Record; + currentIndex?: number; }; +function getStoredDraft( + storageKey: string | 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; + + let result = str.trim(); + + // Remove trailing numbers in parentheses, e.g., (1) or (۱) + result = result.replace(/\s*\([\d۱۲۳۴۵۶۷۸۹۰]+\)\s*$/, ""); + + if (result.startsWith(')')) { + result = result.slice(1).trim(); + } + if (result.endsWith('(')) { + result = result.slice(0, -1).trim(); + } + + let openCount = 0; + let closeCount = 0; + for (let i = 0; i < result.length; i++) { + if (result[i] === '(') openCount++; + else if (result[i] === ')') closeCount++; + } + + if (openCount > closeCount) { + result = result + ')'.repeat(openCount - closeCount); + } else if (closeCount > openCount) { + result = '('.repeat(closeCount - openCount) + result; + } + + if (result.length > 0) { + result = result.charAt(0).toUpperCase() + result.slice(1); + } + + return result; +} + export default function TestQuestionsFlow({ title, questions, @@ -44,10 +110,15 @@ export default function TestQuestionsFlow({ stepsLabel = "Steps", onFinish, onClose, + draftStorageKey, }: TestQuestionsFlowProps) { const router = useRouter(); - const [currentIndex, setCurrentIndex] = useState(0); - const [answers, setAnswers] = useState>({}); + const [currentIndex, setCurrentIndex] = useState( + () => getStoredDraft(draftStorageKey, questions.length).currentIndex, + ); + const [answers, setAnswers] = useState>( + () => getStoredDraft(draftStorageKey, questions.length).answers, + ); const [isSubmitting, setIsSubmitting] = useState(false); const currentQuestion = questions[currentIndex] ?? questions[0]; @@ -56,6 +127,15 @@ export default function TestQuestionsFlow({ const selectedValue = currentQuestion ? answers[currentQuestion.id] : undefined; + useEffect(() => { + if (!draftStorageKey) return; + try { + window.localStorage.setItem( + draftStorageKey, + JSON.stringify({ answers, currentIndex }), + ); + } catch {} + }, [answers, currentIndex, draftStorageKey]); const handleOptionSelect = (value: string | number) => { if (!currentQuestion) return; @@ -85,6 +165,7 @@ export default function TestQuestionsFlow({ if (onFinish) { await onFinish(answers); } + if (draftStorageKey) window.localStorage.removeItem(draftStorageKey); router.back(); } catch { // ignore @@ -107,6 +188,9 @@ export default function TestQuestionsFlow({ } const progressPercent = ((currentIndex + 1) / totalQuestions) * 100; + const areAllQuestionsAnswered = questions.every( + (question) => answers[question.id] !== undefined, + ); return ( <> @@ -156,52 +240,77 @@ export default function TestQuestionsFlow({ {/* Question Section */}
    {/* Question Title */} -

    - {currentQuestion.text}{" "} - - - +

    + {currentQuestion.text}

    {/* Answer Options Stack */} -
    - {currentQuestion.options.map((option, idx) => { - const isSelected = selectedValue === option.value; - - return ( - - ); - })} -
    + {(() => { + const options = currentQuestion.options || []; + + return ( +
    + {options.map((option) => { + const isSelected = selectedValue === option.value; + + return ( + + ); + })} +
    + ); + })()}
    {/* Bottom Actions Bar */} @@ -220,8 +329,12 @@ export default function TestQuestionsFlow({ {/* Finish Button on Last Question */} {isLastQuestion ? (