+
{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.
)} @@ -771,7 +950,7 @@ export default function NewMatchClient() { {profile?.can_edit_profile === false && (+ {t[ + "Are you sure you've fully reviewed the profile and want to reject this profile?" + ] || + (locale === "fa" + ? "آیا از رد این پیشنهاد مطمئن هستید؟ در صورت رد، این مورد دیگر در دسترس نخواهد بود." + : "Are you sure you want to decline this proposal? Once declined, this match will no longer be available.")} +
+-
+
- {item} +
- {(t as Record
)[item] || item}
))}
-
{FIRST_ENTRY_TERMS.map((item, index) => (
-
+
{titleText}
{caseStatus === "contacted" ||
isFemaleContactConfirmed ||
(isFemaleProfile && contactStatusMutation.isPending) ? (
-
+
{isFemaleProfile && contactStatusMutation.isPending ? (
) : (
-
+
{isFemaleProfile
? t[
"Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized."
@@ -583,7 +588,7 @@ export default function RequestAcceptedClient() {
)}
) : (
-
+
{noContactReportedSuccess
? t[
"Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you."
@@ -602,14 +607,14 @@ export default function RequestAcceptedClient() {
{caseStatus === "contacted" ||
isFemaleContactConfirmed ||
(isFemaleProfile && contactStatusMutation.isPending) ? (
-
+
{isFemaleProfile &&
contactStatusMutation.isPending ? null : isFemaleProfile ? (
) : (
-
+
{isFemaleProfile ? (
{contactStatusMutation.isPending ? (
@@ -663,9 +668,9 @@ export default function RequestAcceptedClient() {
) : (
-
+
{primaryActionText}
@@ -679,8 +684,8 @@ export default function RequestAcceptedClient() {
disabled={paymentMutation.isPending}
className={
isFemaleProfile
- ? "flex-1 h-[44px] rounded-[15px] bg-gradient-to-r from-[#FE6F82] to-[#E03950] text-white font-semibold group-14 flex items-center justify-center shadow-md shadow-[#FE6F82]/30 transition-all cursor-pointer hover:opacity-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none"
- : "max-w-[212px] flex-1 appearance-none border-0 bg-transparent p-0 text-left"
+ ? "flex-1 min-h-[48px] px-3.5 py-2.5 rounded-full bg-gradient-to-r from-[#FF6687] to-[#FF456C] text-white font-bold text-[13.5px] leading-tight flex items-center justify-center text-center whitespace-nowrap shadow-sm transition-all cursor-pointer hover:opacity-95 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
+ : "flex-1 max-w-[170px] min-h-[48px] appearance-none border-0 bg-transparent p-0 text-center cursor-pointer transition-transform active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
}
>
{isFemaleProfile ? (
@@ -690,9 +695,9 @@ export default function RequestAcceptedClient() {
secondaryActionText
)
) : (
-
+
{paymentMutation.isPending ? (
-
+
) : (
secondaryActionText
)}
@@ -706,8 +711,8 @@ export default function RequestAcceptedClient() {
!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."
diff --git a/src/app/request-sent/request-sent-client.tsx b/src/app/request-sent/request-sent-client.tsx
index 0396236..01fa028 100644
--- a/src/app/request-sent/request-sent-client.tsx
+++ b/src/app/request-sent/request-sent-client.tsx
@@ -67,12 +67,17 @@ export default function RequestSentClient() {
],
getAdvisor: t["Get Advisor"],
};
+ const isFemaleProfile = profile?.gender === "female";
const requestSentCopy = {
title: t["Request Sent"],
- description:
- t[
- "Your request has been sent. Once the lady reviews your request, you will be notified."
- ],
+ description: isFemaleProfile
+ ? t[
+ "Your request has been sent. Once the gentleman reviews your request, you will be notified."
+ ] ||
+ "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاعرسانی خواهد شد."
+ : 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"],
};
@@ -87,33 +92,41 @@ export default function RequestSentClient() {
>
-
+
-
-
+ {/* Illustration */}
+
+ {/* soft glow */}
+
+
+
+
+
-
+ {/* Title */}
+
{requestSentCopy.title}
-
+ {/* Description */}
+
{requestSentCopy.description}
+ {/* Button */}
-
+
{requestSentCopy.matchProfile}
diff --git a/src/components/Componentes/advisor-actions-card.tsx b/src/components/Componentes/advisor-actions-card.tsx
index f1722a6..1fde0ac 100644
--- a/src/components/Componentes/advisor-actions-card.tsx
+++ b/src/components/Componentes/advisor-actions-card.tsx
@@ -1,7 +1,9 @@
"use client";
+import Link from "next/link";
import { useMarriageAdvisorsQuery } from "@/hooks/marriage/use-marriage-advisors";
-import Button from "./button";
+import { localizePath } from "@/translations/config";
+import { useI18n } from "@/translations/provider";
import NetworkImage from "./network-image";
const FALLBACK_AVATAR = "/assets/images/Avatar Image.png";
@@ -33,13 +35,14 @@ export function AdvisorActionsCard({
onGetAdvisor,
className,
}: AdvisorActionsCardProps) {
+ const { locale } = useI18n();
const { data, isLoading } = useMarriageAdvisorsQuery();
const advisors = data?.results ?? [];
- // Derive real avatars from API response (pick first 3 with an avatar).
+ // Derive real avatars from API response (pick up to 4 with an avatar).
const realAvatars: AdvisorAvatar[] = advisors
.filter((a) => a.avatar_url)
- .slice(0, 3)
+ .slice(0, 4)
.map((a) => ({
id: a.username,
src: a.avatar_url ?? FALLBACK_AVATAR,
@@ -55,54 +58,63 @@ 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}
)}
-
- {getAdvisorLabel}
-
+ {getAdvisorHref ? (
+
+ {getAdvisorLabel}
+
+ ) : (
+
+ {getAdvisorLabel}
+
+ )}
diff --git a/src/components/Componentes/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx
index 53b72d8..cfcfafb 100644
--- a/src/components/Componentes/information-sheet.tsx
+++ b/src/components/Componentes/information-sheet.tsx
@@ -16,6 +16,12 @@ type InformationSheetPresetIcon =
| "warning"
| "coin"
| "check"
+ | "diamond"
+ | "diamond-color"
+ | "diamond-color.svg"
+ | "diamond-color.png"
+ | "/assets/images/diamond-color.png"
+ | "/assets/images/diamond-color.svg"
| "stash_play-solid.svg"
| "warning.svg"
| "coin.svg"
@@ -42,6 +48,7 @@ export type InformationSheetProps = Omit<
closeOnOutside?: boolean;
onClose?: () => void;
isLoading?: boolean;
+ showCloseButton?: boolean;
};
const DEFAULT_ICON = {
@@ -98,6 +105,42 @@ const ICON_PRESETS: Record<
width: 36,
height: 36,
},
+ diamond: {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "diamond-color": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "diamond-color.svg": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "diamond-color.png": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "/assets/images/diamond-color.png": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "/assets/images/diamond-color.svg": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
};
function resolveIcon(icon: InformationSheetIcon | null | undefined) {
@@ -110,6 +153,16 @@ function resolveIcon(icon: InformationSheetIcon | null | undefined) {
}
if (typeof icon === "string") {
+ // If diamond PNG was requested, automatically upgrade to high-res vector SVG
+ if (icon.includes("diamond-color")) {
+ return {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ };
+ }
+
return (
ICON_PRESETS[icon as InformationSheetPresetIcon] ?? {
src: icon,
@@ -137,6 +190,7 @@ export function InformationSheet({
onClose,
className,
isLoading = false,
+ showCloseButton = true,
...props
}: InformationSheetProps) {
const { locale, dictionary: t } = useI18n();
@@ -241,17 +295,68 @@ export function InformationSheet({
+ {showCloseButton && (
+
+
+
+ )}
+
{isLoading ? (
-
-
+
+ {/* Icon Skeleton */}
+
+
+ {/* Title Skeleton */}
+
+
+ {/* Description Lines Skeleton */}
+
+
+
+
+
+
+ {/* Middle Box Skeleton (e.g. Plan / Info) */}
+
+
+
+
+
+ {/* Subtext Skeleton */}
+
+
+
+
+
+ {/* Action / Swipe Buttons Skeleton */}
+
+
+
+
) : (
<>
diff --git a/src/components/Componentes/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx
index ae4e5d7..3942773 100644
--- a/src/components/Componentes/navigation-button.tsx
+++ b/src/components/Componentes/navigation-button.tsx
@@ -135,7 +135,7 @@ export function NavigationButton({
return hasActiveSubscription ? (
)}
-
+
{option.label.includes(" - ") ? (
(() => {
const parts = option.label.split(" - ");
@@ -593,8 +613,8 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
{option.label}
diff --git a/src/components/Componentes/swipe-button.tsx b/src/components/Componentes/swipe-button.tsx
index 424c0fd..c772932 100644
--- a/src/components/Componentes/swipe-button.tsx
+++ b/src/components/Componentes/swipe-button.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import { useI18n } from "@/translations/provider";
+import { LoadingSkeleton } from "./loading-skeleton";
import { LoadingThreeDot } from "./loading-three-dot";
type SwipeButtonProps = {
@@ -10,6 +11,7 @@ type SwipeButtonProps = {
text: string;
cancelText?: string;
disabled?: boolean;
+ isLoading?: boolean;
theme?: "default" | "green";
};
@@ -19,11 +21,28 @@ export function SwipeButton({
text,
cancelText,
disabled = false,
+ isLoading = false,
theme = "default",
}: SwipeButtonProps) {
const { dictionary: t } = useI18n();
const [clicked, setClicked] = useState(false);
+ if (isLoading) {
+ if (onCancel) {
+ return (
+
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+ }
+
const handleClick = () => {
setClicked(true);
onSuccess();
diff --git a/src/components/Componentes/test-completed-sheet.test.tsx b/src/components/Componentes/test-completed-sheet.test.tsx
new file mode 100644
index 0000000..38d59c2
--- /dev/null
+++ b/src/components/Componentes/test-completed-sheet.test.tsx
@@ -0,0 +1,67 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { TestCompletedSheet } from "./test-completed-sheet";
+
+vi.mock("@/translations/provider", () => ({
+ useI18n: () => ({
+ locale: "fa",
+ dictionary: {},
+ }),
+}));
+
+vi.mock("@/hooks/use-hardware-back-handler", () => ({
+ useHardwareBackHandler: vi.fn(),
+}));
+
+describe("TestCompletedSheet", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders completion message and 'متوجه شدم' button when open", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("heading", { name: "تست گلاسر" })).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد.",
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "متوجه شدم" })).toBeInTheDocument();
+ });
+
+ it("calls onClose when clicking 'متوجه شدم' button", async () => {
+ const handleClose = vi.fn();
+
+ render(
+ ,
+ );
+
+ const gotItBtn = screen.getByRole("button", { name: "متوجه شدم" });
+ fireEvent.click(gotItBtn);
+
+ await new Promise((r) => setTimeout(r, 260));
+ expect(handleClose).toHaveBeenCalled();
+ });
+
+ it("returns null when isOpen is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/components/Componentes/test-completed-sheet.tsx b/src/components/Componentes/test-completed-sheet.tsx
new file mode 100644
index 0000000..8634641
--- /dev/null
+++ b/src/components/Componentes/test-completed-sheet.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useI18n } from "@/translations/provider";
+import InformationSheet from "./information-sheet";
+import SwipeButton from "./swipe-button";
+
+export type TestCompletedSheetProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ title?: string;
+ closeOnOutside?: boolean;
+};
+
+export function TestCompletedSheet({
+ isOpen,
+ onClose,
+ title,
+ closeOnOutside = true,
+}: TestCompletedSheetProps) {
+ const { dictionary: t, locale } = useI18n();
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const isFa = locale === "fa";
+
+ const sheetTitle =
+ title ||
+ (t as Record)["Test Completed"] ||
+ (isFa ? "آزمون تکمیل شده است" : "Test Completed");
+
+ const message = isFa
+ ? "شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد."
+ : (t as Record)["You have already completed this test, and it cannot be retaken."] ||
+ "You have already completed this test, and it cannot be retaken.";
+
+ const buttonLabel = (t as Record)["Got it"] || (isFa ? "متوجه شدم" : "Got it");
+
+ return (
+
+ {message}
+
+ }
+ buttons={({ close }) => (
+
+ )}
+ closeOnOutside={closeOnOutside}
+ onClose={onClose}
+ />
+ );
+}
+
+export default TestCompletedSheet;
diff --git a/src/components/Componentes/test-exit-sheet.test.tsx b/src/components/Componentes/test-exit-sheet.test.tsx
new file mode 100644
index 0000000..bcb1f4d
--- /dev/null
+++ b/src/components/Componentes/test-exit-sheet.test.tsx
@@ -0,0 +1,92 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { TestExitSheet } from "./test-exit-sheet";
+
+vi.mock("@/translations/provider", () => ({
+ useI18n: () => ({
+ locale: "fa",
+ dictionary: {},
+ }),
+}));
+
+vi.mock("@/hooks/use-hardware-back-handler", () => ({
+ useHardwareBackHandler: vi.fn(),
+}));
+
+describe("TestExitSheet", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders warning title and explanation points when open", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("heading", { name: "خروج از آزمون" })).toBeInTheDocument();
+ expect(
+ screen.getByText("در صورت خروج از تست، ادامه فعلی حفظ نمیشود."),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("پاسخهای واردشده ذخیره نخواهند شد."),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("برای انجام دوباره تست باید از ابتدا شروع کنید."),
+ ).toBeInTheDocument();
+ expect(screen.getByText("ادامه آزمون")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "خروج از آزمون" })).toBeInTheDocument();
+ });
+
+ it("calls onClose when clicking continue test (cancel)", async () => {
+ const handleClose = vi.fn();
+ const handleConfirm = vi.fn();
+
+ render(
+ ,
+ );
+
+ const continueBtn = screen.getByText("ادامه آزمون");
+ fireEvent.click(continueBtn);
+
+ await new Promise((r) => setTimeout(r, 260));
+ expect(handleClose).toHaveBeenCalled();
+ });
+
+ it("calls onConfirmExit when confirming exit", () => {
+ const handleClose = vi.fn();
+ const handleConfirm = vi.fn();
+
+ render(
+ ,
+ );
+
+ const exitBtn = screen.getByRole("button", { name: "خروج از آزمون" });
+ fireEvent.click(exitBtn);
+
+ expect(handleConfirm).toHaveBeenCalled();
+ });
+
+ it("returns null when isOpen is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/components/Componentes/test-exit-sheet.tsx b/src/components/Componentes/test-exit-sheet.tsx
new file mode 100644
index 0000000..e408075
--- /dev/null
+++ b/src/components/Componentes/test-exit-sheet.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { useI18n } from "@/translations/provider";
+import InformationSheet from "./information-sheet";
+import SwipeButton from "./swipe-button";
+
+export type TestExitSheetProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirmExit: () => void;
+ closeOnOutside?: boolean;
+};
+
+export function TestExitSheet({
+ isOpen,
+ onClose,
+ onConfirmExit,
+ closeOnOutside = true,
+}: TestExitSheetProps) {
+ const { dictionary: t, locale } = useI18n();
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const isFa = locale === "fa";
+
+ const tr = t as Record;
+ const title = tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test");
+
+ const points = isFa
+ ? [
+ "در صورت خروج از تست، ادامه فعلی حفظ نمیشود.",
+ "پاسخهای واردشده ذخیره نخواهند شد.",
+ "برای انجام دوباره تست باید از ابتدا شروع کنید.",
+ ]
+ : [
+ tr["If you exit the test, your current progress will not be saved."] ||
+ "If you exit the test, your current progress will not be saved.",
+ tr["Your entered answers will not be saved."] ||
+ "Your entered answers will not be saved.",
+ tr["To take the test again, you must start from the beginning."] ||
+ "To take the test again, you must start from the beginning.",
+ ];
+
+ const cancelLabel =
+ tr["Continue Test"] || (isFa ? "ادامه آزمون" : "Continue Test");
+ const exitLabel =
+ tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test");
+
+ return (
+
+ {points.map((pt, idx) => (
+
+ {pt}
+
+ ))}
+
+ }
+ 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/conditional-rules.test.ts b/src/lib/conditional-rules.test.ts
index bf41d9a..b0d48cd 100644
--- a/src/lib/conditional-rules.test.ts
+++ b/src/lib/conditional-rules.test.ts
@@ -200,4 +200,643 @@ describe("Conditional Rules Evaluator", () => {
expect(isQuestionVisible(q, matchingAnswers)).toBe(true);
expect(isQuestionRequired(q, matchingAnswers)).toBe(true);
});
+
+ it("should evaluate representative questions as optional for men and conditional for women based on age", () => {
+ const repNameQuestion: QuestionField = {
+ ...dummyQuestion,
+ id: "contact_residence.representative_s_full_name",
+ title: "Representative's Full Name",
+ required: false,
+ baseRequired: false,
+ requiredWhen: {
+ genders: ["female"],
+ maxAge: 26,
+ },
+ };
+
+ const repPhoneQuestion: QuestionField = {
+ ...dummyQuestion,
+ id: "contact_residence.representative_s_contact_number",
+ title: "Representative's Contact Number",
+ type: "phone",
+ required: false,
+ baseRequired: false,
+ requiredWhen: {
+ genders: ["female"],
+ maxAge: 26,
+ },
+ };
+
+ const repRelQuestion: QuestionField = {
+ ...dummyQuestion,
+ id: "contact_residence.relationship_to_representative",
+ title: "Relationship to Representative",
+ type: "dropdown",
+ required: false,
+ baseRequired: false,
+ requiredWhen: {
+ genders: ["female"],
+ maxAge: 26,
+ },
+ };
+
+ const repQuestions = [repNameQuestion, repPhoneQuestion, repRelQuestion];
+
+ // For Male (regardless of age: 20, 26, 30): ALWAYS OPTIONAL
+ for (const age of [18, 20, 25, 26, 27, 35]) {
+ const maleContext = { gender: "male", age };
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, maleContext)).toBe(false);
+ }
+ }
+
+ // For Female <= 26: REQUIRED
+ for (const age of [18, 20, 25, 26]) {
+ const youngFemaleContext = { gender: "female", age };
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, youngFemaleContext)).toBe(true);
+ }
+ }
+
+ // For Female >= 27: OPTIONAL
+ for (const age of [27, 30, 35]) {
+ const olderFemaleContext = { gender: "female", age };
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, olderFemaleContext)).toBe(false);
+ }
+ }
+
+ // When gender is unknown: OPTIONAL
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, {})).toBe(false);
+ }
+ });
+
+ it("should correctly handle physical health 4-option visibility and medication requirement", () => {
+ // 1. Physical Health Description
+ const physicalHealthDescription: QuestionField = {
+ ...dummyQuestion,
+ id: "appearance_health.physical_health_description",
+ title: "Physical Health Description",
+ required: false,
+ baseRequired: false,
+ visibility: {
+ parent_question_id: "appearance_health.physical_health_status",
+ trigger_option_ids: [
+ "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness",
+ "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation",
+ "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions",
+ ],
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: "appearance_health.physical_health_status",
+ trigger_option_ids: [
+ "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness",
+ "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation",
+ "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ // Option A: No illness -> hidden and not required
+ const optionAAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_currently_have_no_specific_illness_or_physical_limitation",
+ option_id:
+ "appearance_health.physical_health_status.i_currently_have_no_specific_illness_or_physical_limitation",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionAAnswers)).toBe(false);
+ expect(isQuestionRequired(physicalHealthDescription, optionAAnswers)).toBe(false);
+
+ // Options B, C, D -> visible and required
+ const optionBAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_have_a_specific_or_chronic_illness",
+ option_id:
+ "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionBAnswers)).toBe(true);
+ expect(isQuestionRequired(physicalHealthDescription, optionBAnswers)).toBe(true);
+
+ const optionCAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_have_a_disability_or_physical_limitation",
+ option_id:
+ "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionCAnswers)).toBe(true);
+ expect(isQuestionRequired(physicalHealthDescription, optionCAnswers)).toBe(true);
+
+ const optionDAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_have_other_illness_or_physical_conditions",
+ option_id:
+ "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionDAnswers)).toBe(true);
+ expect(isQuestionRequired(physicalHealthDescription, optionDAnswers)).toBe(true);
+
+ // 2. Medication Name and Reason
+ const medicationDescription: QuestionField = {
+ ...dummyQuestion,
+ id: "appearance_health.medication_name_and_reason_for_use",
+ title: "Medication Name and Reason for Use",
+ required: false,
+ baseRequired: false,
+ visibility: {
+ parent_question_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis",
+ trigger_option_ids: [
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes",
+ ],
+ operator: "any_of",
+ conditions: [
+ {
+ parent_question_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues",
+ trigger_option_ids: [
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes",
+ ],
+ operator: "any_of",
+ },
+ ],
+ root_operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis",
+ trigger_option_ids: [
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes",
+ ],
+ operator: "any_of",
+ conditions: [
+ {
+ parent_question_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues",
+ trigger_option_ids: [
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes",
+ ],
+ operator: "any_of",
+ },
+ ],
+ root_operator: "any_of",
+ },
+ };
+
+ // Both No -> hidden
+ const medNoAnswers = {
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": {
+ value: "no",
+ option_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.no",
+ },
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": {
+ value: "no",
+ option_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.no",
+ },
+ };
+ expect(isQuestionVisible(medicationDescription, medNoAnswers)).toBe(false);
+ expect(isQuestionRequired(medicationDescription, medNoAnswers)).toBe(false);
+
+ // Mental health med Yes -> visible and required
+ const medMentalYesAnswers = {
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": {
+ value: "no",
+ option_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.no",
+ },
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": {
+ value: "yes",
+ option_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes",
+ },
+ };
+ expect(isQuestionVisible(medicationDescription, medMentalYesAnswers)).toBe(true);
+ expect(isQuestionRequired(medicationDescription, medMentalYesAnswers)).toBe(true);
+
+ // Ongoing med Yes -> visible and required
+ const medOngoingYesAnswers = {
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": {
+ value: "yes",
+ option_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes",
+ },
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": {
+ value: "no",
+ option_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.no",
+ },
+ };
+ expect(isQuestionVisible(medicationDescription, medOngoingYesAnswers)).toBe(true);
+ expect(isQuestionRequired(medicationDescription, medOngoingYesAnswers)).toBe(true);
+ });
+
+ it("should hide job title, work location, and monthly income for student, unemployed, student & job seeking, and homemaker", () => {
+ const jobTitle: QuestionField = {
+ ...dummyQuestion,
+ id: "education_career.job_title",
+ title: "عنوان شغلی",
+ visibility: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ "education_career.employment_status.retired",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ const workLocation: QuestionField = {
+ ...dummyQuestion,
+ id: "education_career.work_location",
+ title: "محل فعالیت",
+ visibility: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ const monthlyIncome: QuestionField = {
+ ...dummyQuestion,
+ id: "education_career.monthly_income",
+ title: "میزان درآمد ماهانه",
+ required: false,
+ visibility: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ "education_career.employment_status.retired",
+ ],
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ "education_career.employment_status.retired",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ // Hidden cases: دانشجو, دانشجو و جویای کار, جویای کار / بیکار, خانهدار
+ const hiddenStatuses = [
+ "student",
+ "student_and_job_seeking",
+ "job_seeking_unemployed",
+ "homemaker",
+ ];
+
+ for (const status of hiddenStatuses) {
+ const answers = {
+ "education_career.employment_status": {
+ value: status,
+ option_id: `education_career.employment_status.${status}`,
+ },
+ };
+
+ expect(isQuestionVisible(jobTitle, answers)).toBe(false);
+ expect(isQuestionVisible(workLocation, answers)).toBe(false);
+ expect(isQuestionVisible(monthlyIncome, answers)).toBe(false);
+ expect(isQuestionRequired(monthlyIncome, answers)).toBe(false);
+ }
+
+ // Visible case: Full time employed
+ const fullTimeAnswers = {
+ "education_career.employment_status": {
+ value: "full_time_employed",
+ option_id: "education_career.employment_status.full_time_employed",
+ },
+ };
+ expect(isQuestionVisible(jobTitle, fullTimeAnswers)).toBe(true);
+ expect(isQuestionVisible(workLocation, fullTimeAnswers)).toBe(true);
+ expect(isQuestionVisible(monthlyIncome, fullTimeAnswers)).toBe(true);
+ expect(isQuestionRequired(monthlyIncome, fullTimeAnswers)).toBe(true);
+
+ // Visible case: Working student
+ const workingStudentAnswers = {
+ "education_career.employment_status": {
+ value: "working_student",
+ option_id: "education_career.employment_status.working_student",
+ },
+ };
+ expect(isQuestionVisible(jobTitle, workingStudentAnswers)).toBe(true);
+ expect(isQuestionVisible(workLocation, workingStudentAnswers)).toBe(true);
+ expect(isQuestionVisible(monthlyIncome, workingStudentAnswers)).toBe(true);
+ expect(isQuestionRequired(monthlyIncome, workingStudentAnswers)).toBe(true);
+
+ // Retired case: Job title and monthly income visible, work location hidden
+ const retiredAnswers = {
+ "education_career.employment_status": {
+ value: "retired",
+ option_id: "education_career.employment_status.retired",
+ },
+ };
+ expect(isQuestionVisible(jobTitle, retiredAnswers)).toBe(true);
+ expect(isQuestionVisible(workLocation, retiredAnswers)).toBe(false);
+ expect(isQuestionVisible(monthlyIncome, retiredAnswers)).toBe(true);
+ expect(isQuestionRequired(monthlyIncome, retiredAnswers)).toBe(true);
+ });
+
+ it("should show parents marital status only when both parents are alive (Option A) and hide for options B, C, D", () => {
+ const parentsMaritalStatus: QuestionField = {
+ ...dummyQuestion,
+ id: "family_background.parents_marital_status",
+ title: "وضعیت تأهل والدین",
+ required: false,
+ visibility: {
+ parent_question_id: "family_background.parents_survival_status",
+ trigger_option_ids: [
+ "family_background.parents_survival_status.both_parents_are_alive",
+ ],
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: "family_background.parents_survival_status",
+ trigger_option_ids: [
+ "family_background.parents_survival_status.both_parents_are_alive",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ // Option A: Both parents are alive -> Visible & Required
+ const optionAAnswers = {
+ "family_background.parents_survival_status": {
+ value: "both_parents_are_alive",
+ option_id: "family_background.parents_survival_status.both_parents_are_alive",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionAAnswers)).toBe(true);
+ expect(isQuestionRequired(parentsMaritalStatus, optionAAnswers)).toBe(true);
+
+ // Option B: Father has passed away -> Hidden & Not Required
+ const optionBAnswers = {
+ "family_background.parents_survival_status": {
+ value: "father_has_passed_away",
+ option_id: "family_background.parents_survival_status.father_has_passed_away",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionBAnswers)).toBe(false);
+ expect(isQuestionRequired(parentsMaritalStatus, optionBAnswers)).toBe(false);
+
+ // Option C: Mother has passed away -> Hidden & Not Required
+ const optionCAnswers = {
+ "family_background.parents_survival_status": {
+ value: "mother_has_passed_away",
+ option_id: "family_background.parents_survival_status.mother_has_passed_away",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionCAnswers)).toBe(false);
+ expect(isQuestionRequired(parentsMaritalStatus, optionCAnswers)).toBe(false);
+
+ // Option D: Both parents have passed away -> Hidden & Not Required
+ const optionDAnswers = {
+ "family_background.parents_survival_status": {
+ value: "both_parents_have_passed_away",
+ option_id: "family_background.parents_survival_status.both_parents_have_passed_away",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionDAnswers)).toBe(false);
+ expect(isQuestionRequired(parentsMaritalStatus, optionDAnswers)).toBe(false);
+ });
+
+ it("should show family responsibility follow-up questions (live with you, additional details) and make them required only when responsibility options are selected", () => {
+ const parentId = "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member";
+ const triggerOptionIds = [
+ `${parentId}.i_am_responsible_for_caring_for_my_father`,
+ `${parentId}.i_am_responsible_for_caring_for_my_mother`,
+ `${parentId}.i_am_responsible_for_caring_for_both_parents`,
+ `${parentId}.i_am_responsible_for_caring_for_a_sibling_brother_sister`,
+ `${parentId}.i_am_the_legal_guardian_or_supervisor_of_a_family_member`,
+ `${parentId}.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses`,
+ `${parentId}.i_have_other_circumstances_and_will_explain_in_the_description`,
+ `${parentId}.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both`,
+ ];
+
+ const liveWithYou: QuestionField = {
+ ...dummyQuestion,
+ id: "family_background.do_the_supported_individual_s_live_with_you",
+ title: "آیا فرد یا افراد تحت حمایت با شما زندگی میکنند؟",
+ required: false,
+ visibility: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ };
+
+ const additionalDetails: QuestionField = {
+ ...dummyQuestion,
+ id: "family_background.additional_details_about_family_responsibility",
+ title: "توضیحات تکمیلی درباره مسئولیت خانوادگی",
+ required: false,
+ visibility: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ };
+
+ // Case 1: No ongoing responsibility selected -> Hidden & Not Required
+ const noRespAnswers = {
+ [parentId]: {
+ value: ["خیر، مسئولیت مستمری ندارم."],
+ option_id: [`${parentId}.no_i_do_not_have_any_ongoing_responsibility`],
+ },
+ };
+ expect(isQuestionVisible(liveWithYou, noRespAnswers)).toBe(false);
+ expect(isQuestionRequired(liveWithYou, noRespAnswers)).toBe(false);
+ expect(isQuestionVisible(additionalDetails, noRespAnswers)).toBe(false);
+ expect(isQuestionRequired(additionalDetails, noRespAnswers)).toBe(false);
+
+ // Case 2: Responsibility selected (e.g. Caring for father) -> Visible & Required
+ const fatherRespAnswers = {
+ [parentId]: {
+ value: ["مسئولیت مراقبت از پدر را بر عهده دارم."],
+ option_id: [`${parentId}.i_am_responsible_for_caring_for_my_father`],
+ },
+ };
+ expect(isQuestionVisible(liveWithYou, fatherRespAnswers)).toBe(true);
+ expect(isQuestionRequired(liveWithYou, fatherRespAnswers)).toBe(true);
+ expect(isQuestionVisible(additionalDetails, fatherRespAnswers)).toBe(true);
+ expect(isQuestionRequired(additionalDetails, fatherRespAnswers)).toBe(true);
+
+ // Case 3: Multiple responsibilities selected -> Visible & Required
+ const multiRespAnswers = {
+ [parentId]: {
+ value: ["مسئولیت مراقبت از پدر", "حمایت مالی"],
+ option_id: [
+ `${parentId}.i_am_responsible_for_caring_for_my_father`,
+ `${parentId}.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses`,
+ ],
+ },
+ };
+ expect(isQuestionVisible(liveWithYou, multiRespAnswers)).toBe(true);
+ expect(isQuestionRequired(liveWithYou, multiRespAnswers)).toBe(true);
+ expect(isQuestionVisible(additionalDetails, multiRespAnswers)).toBe(true);
+ expect(isQuestionRequired(additionalDetails, multiRespAnswers)).toBe(true);
+ });
+
+ it("should handle Section 6 marital history and children follow-up rules properly", () => {
+ const mStatusId = "marital_history.current_marital_status";
+ const previousTriggerIds = [
+ `${mStatusId}.failed_engagement_annulled_marriage_without_living_together`,
+ `${mStatusId}.failed_marriage_contract_annulled_engagement_without_starting_joint_life`,
+ `${mStatusId}.divorced_after_living_together`,
+ `${mStatusId}.widowed`,
+ ];
+ const divorcedOrAnnulledIds = [
+ `${mStatusId}.failed_engagement_annulled_marriage_without_living_together`,
+ `${mStatusId}.failed_marriage_contract_annulled_engagement_without_starting_joint_life`,
+ `${mStatusId}.divorced_after_living_together`,
+ ];
+
+ const cStatusId = "marital_history.children_and_guardianship_status";
+ const hasChildrenTriggerIds = [
+ `${cStatusId}.have_children_living_with_me`,
+ `${cStatusId}.have_children_not_living_with_me`,
+ ];
+
+ const prevDuration: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.previous_marriage_duration",
+ title: "مدت ازدواج یا عقد قبلی",
+ required: false,
+ visibility: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ };
+
+ const reasonSeparation: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.reason_for_separation",
+ title: "علت جدایی، در صورت وجود",
+ required: false,
+ visibility: { parent_question_id: mStatusId, trigger_option_ids: divorcedOrAnnulledIds, operator: "any_of" },
+ };
+
+ const childrenStatus: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.children_and_guardianship_status",
+ title: "وضعیت فرزند و تکفل",
+ required: false,
+ visibility: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ };
+
+ const numChildren: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.number_of_children",
+ title: "تعداد فرزندان",
+ required: false,
+ visibility: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ };
+
+ const custodyStatus: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.what_is_the_custody_status_of_your_child_ren",
+ title: "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟",
+ required: false,
+ visibility: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ };
+
+ // 1. Single: previous duration, reason for separation, and children status are all HIDDEN
+ const singleAns = {
+ [mStatusId]: {
+ value: "single_never_married",
+ option_id: `${mStatusId}.single_never_married`,
+ },
+ };
+ expect(isQuestionVisible(prevDuration, singleAns)).toBe(false);
+ expect(isQuestionVisible(reasonSeparation, singleAns)).toBe(false);
+ expect(isQuestionVisible(childrenStatus, singleAns)).toBe(false);
+
+ // 2. Divorced: duration, reason, and children status are all VISIBLE
+ const divorcedAns = {
+ [mStatusId]: {
+ value: "divorced_after_living_together",
+ option_id: `${mStatusId}.divorced_after_living_together`,
+ },
+ };
+ expect(isQuestionVisible(prevDuration, divorcedAns)).toBe(true);
+ expect(isQuestionRequired(prevDuration, divorcedAns)).toBe(true);
+ expect(isQuestionVisible(reasonSeparation, divorcedAns)).toBe(true);
+ expect(isQuestionVisible(childrenStatus, divorcedAns)).toBe(true);
+ expect(isQuestionRequired(childrenStatus, divorcedAns)).toBe(true);
+
+ // 3. Widowed: duration & children VISIBLE, reason for separation HIDDEN
+ const widowedAns = {
+ [mStatusId]: {
+ value: "widowed",
+ option_id: `${mStatusId}.widowed`,
+ },
+ };
+ expect(isQuestionVisible(prevDuration, widowedAns)).toBe(true);
+ expect(isQuestionRequired(prevDuration, widowedAns)).toBe(true);
+ expect(isQuestionVisible(reasonSeparation, widowedAns)).toBe(false);
+ expect(isQuestionVisible(childrenStatus, widowedAns)).toBe(true);
+ expect(isQuestionRequired(childrenStatus, widowedAns)).toBe(true);
+
+ // 4. Children follow-ups: when children exist -> VISIBLE & REQUIRED
+ const hasChildAns = {
+ [cStatusId]: {
+ value: "have_children_living_with_me",
+ option_id: `${cStatusId}.have_children_living_with_me`,
+ },
+ };
+ expect(isQuestionVisible(numChildren, hasChildAns)).toBe(true);
+ expect(isQuestionRequired(numChildren, hasChildAns)).toBe(true);
+ expect(isQuestionVisible(custodyStatus, hasChildAns)).toBe(true);
+ expect(isQuestionRequired(custodyStatus, hasChildAns)).toBe(true);
+
+ // 5. No children: child questions HIDDEN & NOT REQUIRED
+ const noChildAns = {
+ [cStatusId]: {
+ value: "no_children",
+ option_id: `${cStatusId}.no_children`,
+ },
+ };
+ expect(isQuestionVisible(numChildren, noChildAns)).toBe(false);
+ expect(isQuestionRequired(numChildren, noChildAns)).toBe(false);
+ expect(isQuestionVisible(custodyStatus, noChildAns)).toBe(false);
+ expect(isQuestionRequired(custodyStatus, noChildAns)).toBe(false);
+ });
});
diff --git a/src/lib/conditional-rules.ts b/src/lib/conditional-rules.ts
index 7aeee6e..b9fd253 100644
--- a/src/lib/conditional-rules.ts
+++ b/src/lib/conditional-rules.ts
@@ -58,6 +58,16 @@ export function canonicalRule(rule: any): CanonicalRule | null {
if (rule.audience && typeof rule.audience === "object") {
result.audience = rule.audience;
+ } else if (
+ rule.genders ||
+ rule.minAge !== undefined ||
+ rule.maxAge !== undefined
+ ) {
+ result.audience = {
+ genders: rule.genders,
+ minAge: rule.minAge,
+ maxAge: rule.maxAge,
+ };
}
if (Array.isArray(rule.conditions)) {
@@ -140,19 +150,24 @@ export function matchesAudience(
}
if (audience.genders && audience.genders.length > 0) {
- if (context?.gender && !audience.genders.includes(context.gender)) {
+ if (
+ !context?.gender ||
+ !audience.genders
+ .map((g) => g.toLowerCase())
+ .includes(context.gender.toLowerCase())
+ ) {
return false;
}
}
- if (audience.minAge !== undefined && context?.age !== undefined && context.age !== null) {
- if (context.age < audience.minAge) {
+ if (audience.minAge !== undefined) {
+ if (context?.age === undefined || context.age === null || context.age < audience.minAge) {
return false;
}
}
- if (audience.maxAge !== undefined && context?.age !== undefined && context.age !== null) {
- if (context.age > audience.maxAge) {
+ if (audience.maxAge !== undefined) {
+ if (context?.age === undefined || context.age === null || context.age > audience.maxAge) {
return false;
}
}
@@ -250,7 +265,9 @@ export function ruleMatches(
: mainMatches && conditionsResult;
}
- return mainMatches;
+ return parentId
+ ? mainMatches
+ : Boolean(!rule.audience || matchesAudience(rule.audience, context));
}
export function isQuestionVisible(
@@ -289,16 +306,16 @@ export function isQuestionRequired(
return false;
}
- if (question.required || question.baseRequired) {
- return true;
- }
-
if (question.requiredWhen) {
- if (question.requiredWhen.genders || question.requiredWhen.minAge || question.requiredWhen.maxAge) {
+ if (
+ question.requiredWhen.genders ||
+ question.requiredWhen.minAge !== undefined ||
+ question.requiredWhen.maxAge !== undefined
+ ) {
return matchesAudience(question.requiredWhen, context);
}
return ruleMatches(question.requiredWhen, answers, context);
}
- return false;
+ return Boolean(question.baseRequired ?? question.required);
}
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/ar.json b/src/translations/locales/ar.json
index a4d1241..614fd9f 100644
--- a/src/translations/locales/ar.json
+++ b/src/translations/locales/ar.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "أدخل اسم الدواء وسبب الاستخدام...",
"وضعیت سلامت جسمانی": "حالة الصحة الجسدية",
"توضیحات وضعیت جسمانی": "وصف الصحة الجسدية",
- "Name": "الاسم"
-}
\ No newline at end of file
+ "Name": "الاسم",
+ "upload_certificates": "تحميل الشهادات والوثائق",
+ "add_another_document": "إضافة وثيقة أخرى",
+ "max_files_reached": "تم تحميل الحد الأقصى (4 ملفات)",
+ "remove_document": "حذف الوثيقة",
+ "upload_failed": "فشل التحميل. يرجى المحاولة مرة أخرى."
+}
diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json
index be8ef8f..53cbbb8 100644
--- a/src/translations/locales/az.json
+++ b/src/translations/locales/az.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Dərmanın adını və istifadə səbəbini daxil edin...",
"وضعیت سلامت جسمانی": "Fiziki Sağlamlıq Vəziyyəti",
"توضیحات وضعیت جسمانی": "Fiziki Sağlamlıq Təsviri",
- "Name": "Ad"
-}
\ No newline at end of file
+ "Name": "Ad",
+ "upload_certificates": "Sənədləri yükləyin",
+ "add_another_document": "Başqa sənəd əlavə edin",
+ "max_files_reached": "Maksimum 4 fayl yükləndi",
+ "remove_document": "Sənədi sil",
+ "upload_failed": "Yükləmə uğursuz oldu. Yenidən cəhd edin."
+}
diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json
index cd3a011..d275f8e 100644
--- a/src/translations/locales/bn.json
+++ b/src/translations/locales/bn.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "ওষুধের নাম এবং কারণ লিখুন...",
"وضعیت سلامت جسمانی": "শারীরিক স্বাস্থ্যের অবস্থা",
"توضیحات وضعیت جسمانی": "শারীরিক স্বাস্থ্যের বিবরণ",
- "Name": "নাম"
-}
\ No newline at end of file
+ "Name": "নাম",
+ "upload_certificates": "নথিপত্র আপলোড করুন",
+ "add_another_document": "অন্য নথি যোগ করুন",
+ "max_files_reached": "সর্বোচ্চ ৪টি ফাইল আপলোড করা হয়েছে",
+ "remove_document": "নথি মুছুন",
+ "upload_failed": "আপলোড ব্যর্থ হয়েছে। আবার চেষ্টা করুন।"
+}
diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json
index 6ca63b0..ac26ef6 100644
--- a/src/translations/locales/da.json
+++ b/src/translations/locales/da.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Indtast medicinnavn og årsag...",
"وضعیت سلامت جسمانی": "Fysisk helbredstilstand",
"توضیحات وضعیت جسمانی": "Beskrivelse af fysisk helbred",
- "Name": "Navn"
-}
\ No newline at end of file
+ "Name": "Navn",
+ "upload_certificates": "Upload certifikater",
+ "add_another_document": "Tilføj et andet dokument",
+ "max_files_reached": "Maksimalt 4 filer uploadet",
+ "remove_document": "Fjern dokument",
+ "upload_failed": "Upload mislykkedes. Prøv igen."
+}
diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json
index 4f19576..b3ab795 100644
--- a/src/translations/locales/de.json
+++ b/src/translations/locales/de.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Geben Sie den Medikamentennamen und den Grund ein...",
"وضعیت سلامت جسمانی": "Körperlicher Gesundheitszustand",
"توضیحات وضعیت جسمانی": "Beschreibung des körperlichen Zustands",
- "Name": "Name"
-}
\ No newline at end of file
+ "Name": "Name",
+ "upload_certificates": "Zertifikate hochladen",
+ "add_another_document": "Ein weiteres Dokument hinzufügen",
+ "max_files_reached": "Maximal 4 Dateien hochgeladen",
+ "remove_document": "Dokument entfernen",
+ "upload_failed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut."
+}
diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json
index a34416a..15abd91 100644
--- a/src/translations/locales/en.json
+++ b/src/translations/locales/en.json
@@ -248,8 +248,8 @@
"General Health:": "General Health:",
"German": "German",
"Germany": "Germany",
- "Get Advisor": "Get Advisor",
- "Get an advisor": "Get an advisor",
+ "Get Advisor": "Talk to Advisor",
+ "Get an advisor": "Need Marriage Guidance?",
"Glasser 5 Needs Test": "Glasser 5 Needs Test",
"Go back": "Go back",
"Good": "Good",
@@ -406,7 +406,7 @@
"Not a good personal fit": "Not a good personal fit",
"Not committed": "Not committed",
"Not important": "Not important",
- "Not sure what to do next? Our psychology section is here to guide you at every step.": "Not sure what to do next? Our psychology section is here to guide you at every step.",
+ "Not sure what to do next? Our psychology section is here to guide you at every step.": "Unsure about your next step? Our expert counselors are here to help you make confident decisions.",
"Nothing is shared without your consent.": "Nothing is shared without your consent.",
"Number of Children": "Number of Children",
"Number of Siblings": "Number of Siblings",
@@ -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",
"View Profile": "View Profile",
"View contact number": "View contact number",
- "View more details": "View more details",
+ "View more details": "View Full Profile",
"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",
@@ -741,6 +741,7 @@
"Your details are only used for the matching process.": "Your details are only used for the matching process.",
"Your information is kept strictly confidential.": "Your information is kept strictly confidential.",
"Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.",
+ "Your request has been sent. Once the gentleman reviews your request, you will be notified.": "Your request has been sent. Once the gentleman reviews your request, you will be notified.",
"Your request has been sent. Once the lady reviews your request, you will be notified.": "Your request has been sent. Once the lady reviews your request, you will be notified.",
"Your request was rejected": "Your request was rejected",
"Your request was rejected by the lady. You will be introduced to other candidates in the future.": "Your request was rejected by the lady. You will be introduced to other candidates in the future.",
@@ -845,5 +846,10 @@
"Medication Name and Reason for Use": "Medication Name and Reason for Use",
"Enter medication name and reason for use...": "Enter medication name and reason for use...",
"Enter a valid phone number with country code.": "Enter a valid phone number with country code.",
- "Name": "Name"
-}
\ No newline at end of file
+ "Name": "Name",
+ "upload_certificates": "Upload certificates",
+ "add_another_document": "Add another document",
+ "max_files_reached": "Maximum of 4 files uploaded",
+ "remove_document": "Remove document",
+ "upload_failed": "Upload failed. Please try again."
+}
diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json
index cdbd522..69c328d 100644
--- a/src/translations/locales/es.json
+++ b/src/translations/locales/es.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Ingrese el nombre del medicamento y el motivo...",
"وضعیت سلامت جسمانی": "Estado de salud física",
"توضیحات وضعیت جسمانی": "Descripción de la salud física",
- "Name": "Nombre"
-}
\ No newline at end of file
+ "Name": "Nombre",
+ "upload_certificates": "Subir certificados",
+ "add_another_document": "Agregar otro documento",
+ "max_files_reached": "Máximo de 4 archivos subidos",
+ "remove_document": "Eliminar documento",
+ "upload_failed": "Error al subir. Por favor, inténtelo de nuevo."
+}
diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json
index bb11a05..4748c18 100644
--- a/src/translations/locales/fa.json
+++ b/src/translations/locales/fa.json
@@ -248,8 +248,8 @@
"General Health:": "سلامت عمومی:",
"German": "آلمانی",
"Germany": "آلمان",
- "Get Advisor": "دریافت مشاور",
- "Get an advisor": "دریافت مشاور",
+ "Get Advisor": "گفتگو با مشاور",
+ "Get an advisor": "نیاز به راهنمایی دارید؟",
"Glasser 5 Needs Test": "تست ۵ نیاز گلاسر",
"Go back": "بازگشت",
"Good": "خوب",
@@ -406,7 +406,7 @@
"Not a good personal fit": "تناسب شخصی کافی نبود",
"Not committed": "مقید نیستم",
"Not important": "این معیار برایم اهمیت زیادی ندارد.",
- "Not sure what to do next? Our psychology section is here to guide you at every step.": "نمیدانید قدم بعدی چیست؟ بخش روانشناسی ما در هر مرحله شما را راهنمایی میکند.",
+ "Not sure what to do next? Our psychology section is here to guide you at every step.": "در تصمیمگیری یا ادامه مسیر مردد هستید؟ مشاوران متخصص ما در تمام مراحل آشنایی همراه شما هستند.",
"Nothing is shared without your consent.": "هیچ چیز بدون رضایت شما به اشتراک گذاشته نمیشود.",
"Number of Children": "تعداد فرزندان",
"Number of Siblings": "تعداد خواهر و برادر",
@@ -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": "در مسیر آشنایی و خواستگاری هستیم و هنوز هیچی نهایی نشده",
@@ -740,7 +740,7 @@
"Your Personality Traits": "ویژگیهای شخصیتی خودتان",
"Your details are only used for the matching process.": "اطلاعات شما فقط برای فرآیند تطبیقدهی استفاده میشود.",
"Your information is kept strictly confidential.": "اطلاعات شما کاملاً محرمانه نگهداری میشود.",
- "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "حفظ حریم خصوصی و امنیت شما بالاترین اولویت ماست. ما متعهد به حفظ امنیت اطلاعات شما و اعطای کنترل کامل به شما در طول فرآیند هستیم.",
+ "Your request has been sent. Once the gentleman reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاعرسانی خواهد شد.",
"Your request has been sent. Once the lady reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط خانم، به شما اطلاعرسانی خواهد شد.",
"Your request was rejected": "درخواست شما رد شد",
"Your request was rejected by the lady. You will be introduced to other candidates in the future.": "درخواست شما توسط خانم رد شد. به شما مورد های دیگه ای در اینده معرفی خواهد شد.",
@@ -856,5 +856,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "نام دارو و دلیل مصرف را وارد نمایید...",
"وضعیت سلامت جسمانی": "وضعیت سلامت جسمانی",
"توضیحات وضعیت جسمانی": "توضیحات وضعیت جسمانی",
- "Name": "نام"
-}
\ No newline at end of file
+ "Name": "نام",
+ "upload_certificates": "بارگذاری مدارک",
+ "add_another_document": "افزودن مدرک جدید",
+ "max_files_reached": "حداکثر ۴ فایل بارگذاری شده است",
+ "remove_document": "حذف مدرک",
+ "upload_failed": "بارگذاری با خطا مواجه شد. لطفاً دوباره تلاش کنید."
+}
diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json
index 282e887..e3f2496 100644
--- a/src/translations/locales/fr.json
+++ b/src/translations/locales/fr.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Entrez le nom du médicament et le motif...",
"وضعیت سلامت جسمانی": "État de santé physique",
"توضیحات وضعیت جسمانی": "Description de la santé physique",
- "Name": "Nom"
-}
\ No newline at end of file
+ "Name": "Nom",
+ "upload_certificates": "Télécharger les certificats",
+ "add_another_document": "Ajouter un autre document",
+ "max_files_reached": "Maximum de 4 fichiers téléchargés",
+ "remove_document": "Supprimer le document",
+ "upload_failed": "Échec du téléchargement. Veuillez réessayer."
+}
diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json
index fe99916..dce5747 100644
--- a/src/translations/locales/gu.json
+++ b/src/translations/locales/gu.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "દવાનું નામ અને કારણ દાખલ કરો...",
"وضعیت سلامت جسمانی": "શારીરિક સ્વાસ્થ્ય સ્થિતિ",
"توضیحات وضعیت جسمانی": "શારીરિક સ્વાસ્થ્ય વર્ણન",
- "Name": "નામ"
-}
\ No newline at end of file
+ "Name": "નામ",
+ "upload_certificates": "પ્રમાણપત્રો અપલોડ કરો",
+ "add_another_document": "બીજો દસ્તાવેજ ઉમેરો",
+ "max_files_reached": "મહત્તમ 4 ફાઇલો અપલોડ કરવામાં આવી છે",
+ "remove_document": "દસ્તાવેજ દૂર કરો",
+ "upload_failed": "અપલોડ નિષ્ફળ ગયું. કૃપા કરીને ફરી પ્રયાસ કરો."
+}
diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json
index 986c768..e7c1c8b 100644
--- a/src/translations/locales/ha.json
+++ b/src/translations/locales/ha.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Shigar da sunan magani da dalilin sha...",
"وضعیت سلامت جسمانی": "Yanayin Lafiyar Jiki",
"توضیحات وضعیت جسمانی": "Bayanin Lafiyar Jiki",
- "Name": "Suna"
-}
\ No newline at end of file
+ "Name": "Suna",
+ "upload_certificates": "Loda takardun shaida",
+ "add_another_document": "Ƙara wata takarda",
+ "max_files_reached": "An loda matsakaicin fayiloli 4",
+ "remove_document": "Cire takarda",
+ "upload_failed": "Loda ya faskara. Da fatan za a sake gwadawa."
+}
diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json
index 0fbd3ef..50b80e2 100644
--- a/src/translations/locales/he.json
+++ b/src/translations/locales/he.json
@@ -355,5 +355,10 @@
"Job Title": "תואר התפקיד",
"Employment Status": "מצב תעסוקתי",
"Your Hobbies and Main Interests": "התחביבים ותחומי העניין העיקריים שלך",
- "View more details": "הצג פרטים נוספים"
-}
\ No newline at end of file
+ "View more details": "הצג פרטים נוספים",
+ "upload_certificates": "העלאת תעודות ומסמכים",
+ "add_another_document": "הוסף מסמך נוסף",
+ "max_files_reached": "הועלו מקסימום 4 קבצים",
+ "remove_document": "הסר מסמך",
+ "upload_failed": "ההעלאה נכשלה. אנא נסה שוב."
+}
diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json
index edfc79b..ca2ea00 100644
--- a/src/translations/locales/hi.json
+++ b/src/translations/locales/hi.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "दवा का नाम और उपयोग का कारण दर्ज करें...",
"وضعیت سلامت جسمانی": "शारीरिक स्वास्थ्य की स्थिति",
"توضیحات وضعیت جسمانی": "शारीरिक स्वास्थ्य का विवरण",
- "Name": "नाम"
-}
\ No newline at end of file
+ "Name": "नाम",
+ "upload_certificates": "प्रमाणपत्र अपलोड करें",
+ "add_another_document": "दूसरा दस्तावेज़ जोड़ें",
+ "max_files_reached": "अधिकतम 4 फ़ाइलें अपलोड की गईं",
+ "remove_document": "दस्तावेज़ हटाएं",
+ "upload_failed": "अपलोड विफल रहा। कृपया पुन: प्रयास करें।"
+}
diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json
index 15bb748..598c76b 100644
--- a/src/translations/locales/id.json
+++ b/src/translations/locales/id.json
@@ -355,5 +355,10 @@
"Job Title": "Jabatan / Pekerjaan",
"Employment Status": "Status Pekerjaan",
"Your Hobbies and Main Interests": "Hobi dan Minat Utama Anda",
- "View more details": "Lihat detail selengkapnya"
-}
\ No newline at end of file
+ "View more details": "Lihat detail selengkapnya",
+ "upload_certificates": "Unggah sertifikat",
+ "add_another_document": "Tambah dokumen lain",
+ "max_files_reached": "Maksimum 4 file diunggah",
+ "remove_document": "Hapus dokumen",
+ "upload_failed": "Pengunggahan gagal. Silakan coba lagi."
+}
diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json
index ff18a96..cdf402a 100644
--- a/src/translations/locales/ks.json
+++ b/src/translations/locales/ks.json
@@ -355,5 +355,10 @@
"Job Title": "کٲم ہُنٛد ناو",
"Employment Status": "مُلازمتٕچ حالت",
"Your Hobbies and Main Interests": "تُہنٛدؠ شۄق تہٕ اَہَم دِلچسپی",
- "View more details": "مزید تفصیل وُچھِو"
-}
\ No newline at end of file
+ "View more details": "مزید تفصیل وُچھِو",
+ "upload_certificates": "دستاویز اپ لوڈ کریو",
+ "add_another_document": "بیٛاکھ دستاویز جمع کریو",
+ "max_files_reached": "زیاد کھوتہ زیاد ۴ فائل اپ لوڈ کرنہ آمژٕ",
+ "remove_document": "دستاویز ہٹاوِیو",
+ "upload_failed": "اپ لوڈ ناکام۔ مہربانی کرتھ دوبارہ کوشش کریو۔"
+}
diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json
index 5ed018f..2530412 100644
--- a/src/translations/locales/pt.json
+++ b/src/translations/locales/pt.json
@@ -355,5 +355,10 @@
"Job Title": "Cargo / Título profissional",
"Employment Status": "Situação profissional",
"Your Hobbies and Main Interests": "Seus hobbies e principais interesses",
- "View more details": "Ver mais detalhes"
-}
\ No newline at end of file
+ "View more details": "Ver mais detalhes",
+ "upload_certificates": "Enviar certificados",
+ "add_another_document": "Adicionar outro documento",
+ "max_files_reached": "Máximo de 4 arquivos enviados",
+ "remove_document": "Remover documento",
+ "upload_failed": "Falha no envio. Por favor, tente novamente."
+}
diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json
index 268671b..5241ead 100644
--- a/src/translations/locales/ru.json
+++ b/src/translations/locales/ru.json
@@ -812,5 +812,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Введите название препарата и причину приема...",
"وضعیت سلامت جسمانی": "Физическое состояние здоровья",
"توضیحات وضعیت جسمانی": "Описание физического состояния",
- "Name": "Имя"
-}
\ No newline at end of file
+ "Name": "Имя",
+ "upload_certificates": "Загрузить сертификаты",
+ "add_another_document": "Добавить еще один документ",
+ "max_files_reached": "Загружено максимум 4 файла",
+ "remove_document": "Удалить документ",
+ "upload_failed": "Ошибка загрузки. Пожалуйста, повторите попытку."
+}
diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json
index 224c42e..5190fa7 100644
--- a/src/translations/locales/sw.json
+++ b/src/translations/locales/sw.json
@@ -355,5 +355,10 @@
"Job Title": "Wadhifa wa Kazi",
"Employment Status": "Hali ya Ajira",
"Your Hobbies and Main Interests": "Mambo unayopenda na Maslahi Kuu",
- "View more details": "Angalia maelezo zaidi"
-}
\ No newline at end of file
+ "View more details": "Angalia maelezo zaidi",
+ "upload_certificates": "Pakia vyeti",
+ "add_another_document": "Ongeza hati nyingine",
+ "max_files_reached": "Upeo wa faili 4 zimepakiwa",
+ "remove_document": "Ondoa hati",
+ "upload_failed": "Upakiaji umeshindwa. Tafadhali jaribu tena."
+}
diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json
index b0dbf5e..2dfac2f 100644
--- a/src/translations/locales/tg.json
+++ b/src/translations/locales/tg.json
@@ -355,5 +355,10 @@
"Job Title": "Унвони вазифа",
"Employment Status": "Вазъи шуғл",
"Your Hobbies and Main Interests": "Машғулиятҳо ва манфиатҳои асосии шумо",
- "View more details": "Дидани тафсилоти бештар"
-}
\ No newline at end of file
+ "View more details": "Дидани тафсилоти бештар",
+ "upload_certificates": "Боргузории ҳуҷҷатҳо",
+ "add_another_document": "Ҳуҷҷати дигар илова кунед",
+ "max_files_reached": "Ҳадди аксар 4 файл боргузорӣ шудааст",
+ "remove_document": "Ҳуҷҷатро нест кунед",
+ "upload_failed": "Боргузорӣ ноком шуд. Лутфан бори дигар кӯшиш кунед."
+}
diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json
index 0f5be69..402c6ca 100644
--- a/src/translations/locales/tr.json
+++ b/src/translations/locales/tr.json
@@ -355,5 +355,10 @@
"Job Title": "Meslek / Unvan",
"Employment Status": "Çalışma Durumu",
"Your Hobbies and Main Interests": "Hobileriniz ve Temel İlgi Alanlarınız",
- "View more details": "Daha fazla ayrıntı gör"
-}
\ No newline at end of file
+ "View more details": "Daha fazla ayrıntı gör",
+ "upload_certificates": "Belgeleri yükle",
+ "add_another_document": "Başka bir belge ekle",
+ "max_files_reached": "Maksimum 4 dosya yüklendi",
+ "remove_document": "Belgeyi kaldır",
+ "upload_failed": "Yükleme başarısız oldu. Lütfen tekrar deneyin."
+}
diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json
index 3ba710b..76f347d 100644
--- a/src/translations/locales/ul.json
+++ b/src/translations/locales/ul.json
@@ -355,5 +355,10 @@
"Job Title": "Job Title",
"Employment Status": "Employment Status",
"Your Hobbies and Main Interests": "Your Hobbies and Main Interests",
- "View more details": "View more details"
-}
\ No newline at end of file
+ "View more details": "View more details",
+ "upload_certificates": "دستاویزات اپ لوڈ کریں",
+ "add_another_document": "مزید دستاویز شامل کریں",
+ "max_files_reached": "زیادہ سے زیادہ 4 فائلیں اپ لوڈ کی گئیں",
+ "remove_document": "دستاویز ہٹائیں",
+ "upload_failed": "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔"
+}
diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json
index 7677d0e..eb53bcc 100644
--- a/src/translations/locales/ur.json
+++ b/src/translations/locales/ur.json
@@ -355,5 +355,10 @@
"Job Title": "عہدہ / ملازمت کا عنوان",
"Employment Status": "ملازمت کی صورتحال",
"Your Hobbies and Main Interests": "آپ کے مشاغل اور اہم دلچسپیاں",
- "View more details": "مزید تفصیلات دیکھیں"
-}
\ No newline at end of file
+ "View more details": "مزید تفصیلات دیکھیں",
+ "upload_certificates": "دستاویزات اپ لوڈ کریں",
+ "add_another_document": "مزید دستاویز شامل کریں",
+ "max_files_reached": "زیادہ سے زیادہ 4 فائلیں اپ لوڈ کی گئیں",
+ "remove_document": "دستاویز ہٹائیں",
+ "upload_failed": "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔"
+}
diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json
index 9c69367..7bd6271 100644
--- a/src/translations/locales/uz.json
+++ b/src/translations/locales/uz.json
@@ -355,5 +355,10 @@
"Job Title": "Kasb / Lavozim",
"Employment Status": "Bandlik holati",
"Your Hobbies and Main Interests": "Qiziqishlaringiz va asosiy mashgʻulotlaringiz",
- "View more details": "Batafsil maʼlumotni koʻrish"
-}
\ No newline at end of file
+ "View more details": "Batafsil maʼlumotni koʻrish",
+ "upload_certificates": "Hujjatlarni yuklash",
+ "add_another_document": "Boshqa hujjat qo'shish",
+ "max_files_reached": "Maksimal 4 ta fayl yuklandi",
+ "remove_document": "Hujjatni o'chirish",
+ "upload_failed": "Yuklab bo'lmadi. Qayta urinib ko'ring."
+}
diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json
index 9cd097a..667bd97 100644
--- a/src/translations/locales/zh.json
+++ b/src/translations/locales/zh.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "输入药物名称和使用原因...",
"وضعیت سلامت جسمانی": "身体健康状况",
"توضیحات وضعیت جسمانی": "身体健康说明",
- "Name": "姓名"
-}
\ No newline at end of file
+ "Name": "姓名",
+ "upload_certificates": "上传证书和文件",
+ "add_another_document": "添加其他文件",
+ "max_files_reached": "最多已上传 4 个文件",
+ "remove_document": "删除文件",
+ "upload_failed": "上传失败,请重试。"
+}
+
{isFemaleProfile ? t[ "Thank you for your feedback. To complete the process, please submit the final outcome of this introduction/contact so that the final status can be determined. If the final status is not yet determined, you can stay in this state until it is finalized." @@ -583,7 +588,7 @@ export default function RequestAcceptedClient() { )}
+
{noContactReportedSuccess ? t[ "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." @@ -602,14 +607,14 @@ export default function RequestAcceptedClient() { {caseStatus === "contacted" || isFemaleContactConfirmed || (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."
diff --git a/src/app/request-sent/request-sent-client.tsx b/src/app/request-sent/request-sent-client.tsx
index 0396236..01fa028 100644
--- a/src/app/request-sent/request-sent-client.tsx
+++ b/src/app/request-sent/request-sent-client.tsx
@@ -67,12 +67,17 @@ export default function RequestSentClient() {
],
getAdvisor: t["Get Advisor"],
};
+ const isFemaleProfile = profile?.gender === "female";
const requestSentCopy = {
title: t["Request Sent"],
- description:
- t[
- "Your request has been sent. Once the lady reviews your request, you will be notified."
- ],
+ description: isFemaleProfile
+ ? t[
+ "Your request has been sent. Once the gentleman reviews your request, you will be notified."
+ ] ||
+ "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاعرسانی خواهد شد."
+ : 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"],
};
@@ -87,33 +92,41 @@ export default function RequestSentClient() {
>
+ {/* Title */}
+
{requestSentCopy.title}
-
+ {/* Description */} +
{requestSentCopy.description}
+ {/* Button */}
+
+
+
{title}
-
+
{description}
-
-
+
+
{isLoading
? /* ── Shimmer circles while API loads ── */
Array.from({ length: 3 }).map((_, i) => (
))
: displayAvatars.map((avatar) => (
))}
{!isLoading && displayExtraCount > 0 && (
-
+
+{displayExtraCount}
)}
-
- {getAdvisorLabel}
-
+ {getAdvisorHref ? (
+
+ {getAdvisorLabel}
+
+ ) : (
+
+ {getAdvisorLabel}
+
+ )}
diff --git a/src/components/Componentes/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx
index 53b72d8..cfcfafb 100644
--- a/src/components/Componentes/information-sheet.tsx
+++ b/src/components/Componentes/information-sheet.tsx
@@ -16,6 +16,12 @@ type InformationSheetPresetIcon =
| "warning"
| "coin"
| "check"
+ | "diamond"
+ | "diamond-color"
+ | "diamond-color.svg"
+ | "diamond-color.png"
+ | "/assets/images/diamond-color.png"
+ | "/assets/images/diamond-color.svg"
| "stash_play-solid.svg"
| "warning.svg"
| "coin.svg"
@@ -42,6 +48,7 @@ export type InformationSheetProps = Omit<
closeOnOutside?: boolean;
onClose?: () => void;
isLoading?: boolean;
+ showCloseButton?: boolean;
};
const DEFAULT_ICON = {
@@ -98,6 +105,42 @@ const ICON_PRESETS: Record<
width: 36,
height: 36,
},
+ diamond: {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "diamond-color": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "diamond-color.svg": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "diamond-color.png": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "/assets/images/diamond-color.png": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
+ "/assets/images/diamond-color.svg": {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ },
};
function resolveIcon(icon: InformationSheetIcon | null | undefined) {
@@ -110,6 +153,16 @@ function resolveIcon(icon: InformationSheetIcon | null | undefined) {
}
if (typeof icon === "string") {
+ // If diamond PNG was requested, automatically upgrade to high-res vector SVG
+ if (icon.includes("diamond-color")) {
+ return {
+ src: "/assets/images/diamond-color.svg",
+ alt: "Subscription",
+ width: 48,
+ height: 48,
+ };
+ }
+
return (
ICON_PRESETS[icon as InformationSheetPresetIcon] ?? {
src: icon,
@@ -137,6 +190,7 @@ export function InformationSheet({
onClose,
className,
isLoading = false,
+ showCloseButton = true,
...props
}: InformationSheetProps) {
const { locale, dictionary: t } = useI18n();
@@ -241,17 +295,68 @@ export function InformationSheet({
+ {showCloseButton && (
+
+
+
+ )}
+
{isLoading ? (
-
-
+
+ {/* Icon Skeleton */}
+
+
+ {/* Title Skeleton */}
+
+
+ {/* Description Lines Skeleton */}
+
+
+
+
+
+
+ {/* Middle Box Skeleton (e.g. Plan / Info) */}
+
+
+
+
+
+ {/* Subtext Skeleton */}
+
+
+
+
+
+ {/* Action / Swipe Buttons Skeleton */}
+
+
+
+
) : (
<>
diff --git a/src/components/Componentes/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx
index ae4e5d7..3942773 100644
--- a/src/components/Componentes/navigation-button.tsx
+++ b/src/components/Componentes/navigation-button.tsx
@@ -135,7 +135,7 @@ export function NavigationButton({
return hasActiveSubscription ? (
)}
-
+
{option.label.includes(" - ") ? (
(() => {
const parts = option.label.split(" - ");
@@ -593,8 +613,8 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) {
{option.label}
diff --git a/src/components/Componentes/swipe-button.tsx b/src/components/Componentes/swipe-button.tsx
index 424c0fd..c772932 100644
--- a/src/components/Componentes/swipe-button.tsx
+++ b/src/components/Componentes/swipe-button.tsx
@@ -2,6 +2,7 @@
import { useState } from "react";
import { useI18n } from "@/translations/provider";
+import { LoadingSkeleton } from "./loading-skeleton";
import { LoadingThreeDot } from "./loading-three-dot";
type SwipeButtonProps = {
@@ -10,6 +11,7 @@ type SwipeButtonProps = {
text: string;
cancelText?: string;
disabled?: boolean;
+ isLoading?: boolean;
theme?: "default" | "green";
};
@@ -19,11 +21,28 @@ export function SwipeButton({
text,
cancelText,
disabled = false,
+ isLoading = false,
theme = "default",
}: SwipeButtonProps) {
const { dictionary: t } = useI18n();
const [clicked, setClicked] = useState(false);
+ if (isLoading) {
+ if (onCancel) {
+ return (
+
+
+
+
+ );
+ }
+ return (
+
+
+
+ );
+ }
+
const handleClick = () => {
setClicked(true);
onSuccess();
diff --git a/src/components/Componentes/test-completed-sheet.test.tsx b/src/components/Componentes/test-completed-sheet.test.tsx
new file mode 100644
index 0000000..38d59c2
--- /dev/null
+++ b/src/components/Componentes/test-completed-sheet.test.tsx
@@ -0,0 +1,67 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { TestCompletedSheet } from "./test-completed-sheet";
+
+vi.mock("@/translations/provider", () => ({
+ useI18n: () => ({
+ locale: "fa",
+ dictionary: {},
+ }),
+}));
+
+vi.mock("@/hooks/use-hardware-back-handler", () => ({
+ useHardwareBackHandler: vi.fn(),
+}));
+
+describe("TestCompletedSheet", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders completion message and 'متوجه شدم' button when open", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("heading", { name: "تست گلاسر" })).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد.",
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "متوجه شدم" })).toBeInTheDocument();
+ });
+
+ it("calls onClose when clicking 'متوجه شدم' button", async () => {
+ const handleClose = vi.fn();
+
+ render(
+ ,
+ );
+
+ const gotItBtn = screen.getByRole("button", { name: "متوجه شدم" });
+ fireEvent.click(gotItBtn);
+
+ await new Promise((r) => setTimeout(r, 260));
+ expect(handleClose).toHaveBeenCalled();
+ });
+
+ it("returns null when isOpen is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/components/Componentes/test-completed-sheet.tsx b/src/components/Componentes/test-completed-sheet.tsx
new file mode 100644
index 0000000..8634641
--- /dev/null
+++ b/src/components/Componentes/test-completed-sheet.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import { useI18n } from "@/translations/provider";
+import InformationSheet from "./information-sheet";
+import SwipeButton from "./swipe-button";
+
+export type TestCompletedSheetProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ title?: string;
+ closeOnOutside?: boolean;
+};
+
+export function TestCompletedSheet({
+ isOpen,
+ onClose,
+ title,
+ closeOnOutside = true,
+}: TestCompletedSheetProps) {
+ const { dictionary: t, locale } = useI18n();
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const isFa = locale === "fa";
+
+ const sheetTitle =
+ title ||
+ (t as Record)["Test Completed"] ||
+ (isFa ? "آزمون تکمیل شده است" : "Test Completed");
+
+ const message = isFa
+ ? "شما این آزمون را قبلاً تکمیل کردهاید و امکان شرکت مجدد در آن وجود ندارد."
+ : (t as Record)["You have already completed this test, and it cannot be retaken."] ||
+ "You have already completed this test, and it cannot be retaken.";
+
+ const buttonLabel = (t as Record)["Got it"] || (isFa ? "متوجه شدم" : "Got it");
+
+ return (
+
+ {message}
+
+ }
+ buttons={({ close }) => (
+
+ )}
+ closeOnOutside={closeOnOutside}
+ onClose={onClose}
+ />
+ );
+}
+
+export default TestCompletedSheet;
diff --git a/src/components/Componentes/test-exit-sheet.test.tsx b/src/components/Componentes/test-exit-sheet.test.tsx
new file mode 100644
index 0000000..bcb1f4d
--- /dev/null
+++ b/src/components/Componentes/test-exit-sheet.test.tsx
@@ -0,0 +1,92 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { TestExitSheet } from "./test-exit-sheet";
+
+vi.mock("@/translations/provider", () => ({
+ useI18n: () => ({
+ locale: "fa",
+ dictionary: {},
+ }),
+}));
+
+vi.mock("@/hooks/use-hardware-back-handler", () => ({
+ useHardwareBackHandler: vi.fn(),
+}));
+
+describe("TestExitSheet", () => {
+ afterEach(() => {
+ cleanup();
+ });
+
+ it("renders warning title and explanation points when open", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByRole("heading", { name: "خروج از آزمون" })).toBeInTheDocument();
+ expect(
+ screen.getByText("در صورت خروج از تست، ادامه فعلی حفظ نمیشود."),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("پاسخهای واردشده ذخیره نخواهند شد."),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("برای انجام دوباره تست باید از ابتدا شروع کنید."),
+ ).toBeInTheDocument();
+ expect(screen.getByText("ادامه آزمون")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "خروج از آزمون" })).toBeInTheDocument();
+ });
+
+ it("calls onClose when clicking continue test (cancel)", async () => {
+ const handleClose = vi.fn();
+ const handleConfirm = vi.fn();
+
+ render(
+ ,
+ );
+
+ const continueBtn = screen.getByText("ادامه آزمون");
+ fireEvent.click(continueBtn);
+
+ await new Promise((r) => setTimeout(r, 260));
+ expect(handleClose).toHaveBeenCalled();
+ });
+
+ it("calls onConfirmExit when confirming exit", () => {
+ const handleClose = vi.fn();
+ const handleConfirm = vi.fn();
+
+ render(
+ ,
+ );
+
+ const exitBtn = screen.getByRole("button", { name: "خروج از آزمون" });
+ fireEvent.click(exitBtn);
+
+ expect(handleConfirm).toHaveBeenCalled();
+ });
+
+ it("returns null when isOpen is false", () => {
+ const { container } = render(
+ ,
+ );
+
+ expect(container.firstChild).toBeNull();
+ });
+});
diff --git a/src/components/Componentes/test-exit-sheet.tsx b/src/components/Componentes/test-exit-sheet.tsx
new file mode 100644
index 0000000..e408075
--- /dev/null
+++ b/src/components/Componentes/test-exit-sheet.tsx
@@ -0,0 +1,78 @@
+"use client";
+
+import { useI18n } from "@/translations/provider";
+import InformationSheet from "./information-sheet";
+import SwipeButton from "./swipe-button";
+
+export type TestExitSheetProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirmExit: () => void;
+ closeOnOutside?: boolean;
+};
+
+export function TestExitSheet({
+ isOpen,
+ onClose,
+ onConfirmExit,
+ closeOnOutside = true,
+}: TestExitSheetProps) {
+ const { dictionary: t, locale } = useI18n();
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const isFa = locale === "fa";
+
+ const tr = t as Record;
+ const title = tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test");
+
+ const points = isFa
+ ? [
+ "در صورت خروج از تست، ادامه فعلی حفظ نمیشود.",
+ "پاسخهای واردشده ذخیره نخواهند شد.",
+ "برای انجام دوباره تست باید از ابتدا شروع کنید.",
+ ]
+ : [
+ tr["If you exit the test, your current progress will not be saved."] ||
+ "If you exit the test, your current progress will not be saved.",
+ tr["Your entered answers will not be saved."] ||
+ "Your entered answers will not be saved.",
+ tr["To take the test again, you must start from the beginning."] ||
+ "To take the test again, you must start from the beginning.",
+ ];
+
+ const cancelLabel =
+ tr["Continue Test"] || (isFa ? "ادامه آزمون" : "Continue Test");
+ const exitLabel =
+ tr["Exit Test"] || (isFa ? "خروج از آزمون" : "Exit Test");
+
+ return (
+
+ {points.map((pt, idx) => (
+
+ {pt}
+
+ ))}
+
+ }
+ 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/conditional-rules.test.ts b/src/lib/conditional-rules.test.ts
index bf41d9a..b0d48cd 100644
--- a/src/lib/conditional-rules.test.ts
+++ b/src/lib/conditional-rules.test.ts
@@ -200,4 +200,643 @@ describe("Conditional Rules Evaluator", () => {
expect(isQuestionVisible(q, matchingAnswers)).toBe(true);
expect(isQuestionRequired(q, matchingAnswers)).toBe(true);
});
+
+ it("should evaluate representative questions as optional for men and conditional for women based on age", () => {
+ const repNameQuestion: QuestionField = {
+ ...dummyQuestion,
+ id: "contact_residence.representative_s_full_name",
+ title: "Representative's Full Name",
+ required: false,
+ baseRequired: false,
+ requiredWhen: {
+ genders: ["female"],
+ maxAge: 26,
+ },
+ };
+
+ const repPhoneQuestion: QuestionField = {
+ ...dummyQuestion,
+ id: "contact_residence.representative_s_contact_number",
+ title: "Representative's Contact Number",
+ type: "phone",
+ required: false,
+ baseRequired: false,
+ requiredWhen: {
+ genders: ["female"],
+ maxAge: 26,
+ },
+ };
+
+ const repRelQuestion: QuestionField = {
+ ...dummyQuestion,
+ id: "contact_residence.relationship_to_representative",
+ title: "Relationship to Representative",
+ type: "dropdown",
+ required: false,
+ baseRequired: false,
+ requiredWhen: {
+ genders: ["female"],
+ maxAge: 26,
+ },
+ };
+
+ const repQuestions = [repNameQuestion, repPhoneQuestion, repRelQuestion];
+
+ // For Male (regardless of age: 20, 26, 30): ALWAYS OPTIONAL
+ for (const age of [18, 20, 25, 26, 27, 35]) {
+ const maleContext = { gender: "male", age };
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, maleContext)).toBe(false);
+ }
+ }
+
+ // For Female <= 26: REQUIRED
+ for (const age of [18, 20, 25, 26]) {
+ const youngFemaleContext = { gender: "female", age };
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, youngFemaleContext)).toBe(true);
+ }
+ }
+
+ // For Female >= 27: OPTIONAL
+ for (const age of [27, 30, 35]) {
+ const olderFemaleContext = { gender: "female", age };
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, olderFemaleContext)).toBe(false);
+ }
+ }
+
+ // When gender is unknown: OPTIONAL
+ for (const question of repQuestions) {
+ expect(isQuestionRequired(question, {}, {})).toBe(false);
+ }
+ });
+
+ it("should correctly handle physical health 4-option visibility and medication requirement", () => {
+ // 1. Physical Health Description
+ const physicalHealthDescription: QuestionField = {
+ ...dummyQuestion,
+ id: "appearance_health.physical_health_description",
+ title: "Physical Health Description",
+ required: false,
+ baseRequired: false,
+ visibility: {
+ parent_question_id: "appearance_health.physical_health_status",
+ trigger_option_ids: [
+ "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness",
+ "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation",
+ "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions",
+ ],
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: "appearance_health.physical_health_status",
+ trigger_option_ids: [
+ "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness",
+ "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation",
+ "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ // Option A: No illness -> hidden and not required
+ const optionAAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_currently_have_no_specific_illness_or_physical_limitation",
+ option_id:
+ "appearance_health.physical_health_status.i_currently_have_no_specific_illness_or_physical_limitation",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionAAnswers)).toBe(false);
+ expect(isQuestionRequired(physicalHealthDescription, optionAAnswers)).toBe(false);
+
+ // Options B, C, D -> visible and required
+ const optionBAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_have_a_specific_or_chronic_illness",
+ option_id:
+ "appearance_health.physical_health_status.i_have_a_specific_or_chronic_illness",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionBAnswers)).toBe(true);
+ expect(isQuestionRequired(physicalHealthDescription, optionBAnswers)).toBe(true);
+
+ const optionCAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_have_a_disability_or_physical_limitation",
+ option_id:
+ "appearance_health.physical_health_status.i_have_a_disability_or_physical_limitation",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionCAnswers)).toBe(true);
+ expect(isQuestionRequired(physicalHealthDescription, optionCAnswers)).toBe(true);
+
+ const optionDAnswers = {
+ "appearance_health.physical_health_status": {
+ value: "i_have_other_illness_or_physical_conditions",
+ option_id:
+ "appearance_health.physical_health_status.i_have_other_illness_or_physical_conditions",
+ },
+ };
+ expect(isQuestionVisible(physicalHealthDescription, optionDAnswers)).toBe(true);
+ expect(isQuestionRequired(physicalHealthDescription, optionDAnswers)).toBe(true);
+
+ // 2. Medication Name and Reason
+ const medicationDescription: QuestionField = {
+ ...dummyQuestion,
+ id: "appearance_health.medication_name_and_reason_for_use",
+ title: "Medication Name and Reason for Use",
+ required: false,
+ baseRequired: false,
+ visibility: {
+ parent_question_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis",
+ trigger_option_ids: [
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes",
+ ],
+ operator: "any_of",
+ conditions: [
+ {
+ parent_question_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues",
+ trigger_option_ids: [
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes",
+ ],
+ operator: "any_of",
+ },
+ ],
+ root_operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis",
+ trigger_option_ids: [
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes",
+ ],
+ operator: "any_of",
+ conditions: [
+ {
+ parent_question_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues",
+ trigger_option_ids: [
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes",
+ ],
+ operator: "any_of",
+ },
+ ],
+ root_operator: "any_of",
+ },
+ };
+
+ // Both No -> hidden
+ const medNoAnswers = {
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": {
+ value: "no",
+ option_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.no",
+ },
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": {
+ value: "no",
+ option_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.no",
+ },
+ };
+ expect(isQuestionVisible(medicationDescription, medNoAnswers)).toBe(false);
+ expect(isQuestionRequired(medicationDescription, medNoAnswers)).toBe(false);
+
+ // Mental health med Yes -> visible and required
+ const medMentalYesAnswers = {
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": {
+ value: "no",
+ option_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.no",
+ },
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": {
+ value: "yes",
+ option_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.yes",
+ },
+ };
+ expect(isQuestionVisible(medicationDescription, medMentalYesAnswers)).toBe(true);
+ expect(isQuestionRequired(medicationDescription, medMentalYesAnswers)).toBe(true);
+
+ // Ongoing med Yes -> visible and required
+ const medOngoingYesAnswers = {
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis": {
+ value: "yes",
+ option_id:
+ "appearance_health.do_you_currently_take_any_medication_on_an_ongoing_basis.yes",
+ },
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues": {
+ value: "no",
+ option_id:
+ "appearance_health.are_you_currently_taking_medication_for_mental_health_related_issues.no",
+ },
+ };
+ expect(isQuestionVisible(medicationDescription, medOngoingYesAnswers)).toBe(true);
+ expect(isQuestionRequired(medicationDescription, medOngoingYesAnswers)).toBe(true);
+ });
+
+ it("should hide job title, work location, and monthly income for student, unemployed, student & job seeking, and homemaker", () => {
+ const jobTitle: QuestionField = {
+ ...dummyQuestion,
+ id: "education_career.job_title",
+ title: "عنوان شغلی",
+ visibility: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ "education_career.employment_status.retired",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ const workLocation: QuestionField = {
+ ...dummyQuestion,
+ id: "education_career.work_location",
+ title: "محل فعالیت",
+ visibility: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ const monthlyIncome: QuestionField = {
+ ...dummyQuestion,
+ id: "education_career.monthly_income",
+ title: "میزان درآمد ماهانه",
+ required: false,
+ visibility: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ "education_career.employment_status.retired",
+ ],
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: "education_career.employment_status",
+ trigger_option_ids: [
+ "education_career.employment_status.full_time_employed",
+ "education_career.employment_status.part_time_employed",
+ "education_career.employment_status.self_employed_freelancer",
+ "education_career.employment_status.entrepreneur_business_owner",
+ "education_career.employment_status.working_student",
+ "education_career.employment_status.retired",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ // Hidden cases: دانشجو, دانشجو و جویای کار, جویای کار / بیکار, خانهدار
+ const hiddenStatuses = [
+ "student",
+ "student_and_job_seeking",
+ "job_seeking_unemployed",
+ "homemaker",
+ ];
+
+ for (const status of hiddenStatuses) {
+ const answers = {
+ "education_career.employment_status": {
+ value: status,
+ option_id: `education_career.employment_status.${status}`,
+ },
+ };
+
+ expect(isQuestionVisible(jobTitle, answers)).toBe(false);
+ expect(isQuestionVisible(workLocation, answers)).toBe(false);
+ expect(isQuestionVisible(monthlyIncome, answers)).toBe(false);
+ expect(isQuestionRequired(monthlyIncome, answers)).toBe(false);
+ }
+
+ // Visible case: Full time employed
+ const fullTimeAnswers = {
+ "education_career.employment_status": {
+ value: "full_time_employed",
+ option_id: "education_career.employment_status.full_time_employed",
+ },
+ };
+ expect(isQuestionVisible(jobTitle, fullTimeAnswers)).toBe(true);
+ expect(isQuestionVisible(workLocation, fullTimeAnswers)).toBe(true);
+ expect(isQuestionVisible(monthlyIncome, fullTimeAnswers)).toBe(true);
+ expect(isQuestionRequired(monthlyIncome, fullTimeAnswers)).toBe(true);
+
+ // Visible case: Working student
+ const workingStudentAnswers = {
+ "education_career.employment_status": {
+ value: "working_student",
+ option_id: "education_career.employment_status.working_student",
+ },
+ };
+ expect(isQuestionVisible(jobTitle, workingStudentAnswers)).toBe(true);
+ expect(isQuestionVisible(workLocation, workingStudentAnswers)).toBe(true);
+ expect(isQuestionVisible(monthlyIncome, workingStudentAnswers)).toBe(true);
+ expect(isQuestionRequired(monthlyIncome, workingStudentAnswers)).toBe(true);
+
+ // Retired case: Job title and monthly income visible, work location hidden
+ const retiredAnswers = {
+ "education_career.employment_status": {
+ value: "retired",
+ option_id: "education_career.employment_status.retired",
+ },
+ };
+ expect(isQuestionVisible(jobTitle, retiredAnswers)).toBe(true);
+ expect(isQuestionVisible(workLocation, retiredAnswers)).toBe(false);
+ expect(isQuestionVisible(monthlyIncome, retiredAnswers)).toBe(true);
+ expect(isQuestionRequired(monthlyIncome, retiredAnswers)).toBe(true);
+ });
+
+ it("should show parents marital status only when both parents are alive (Option A) and hide for options B, C, D", () => {
+ const parentsMaritalStatus: QuestionField = {
+ ...dummyQuestion,
+ id: "family_background.parents_marital_status",
+ title: "وضعیت تأهل والدین",
+ required: false,
+ visibility: {
+ parent_question_id: "family_background.parents_survival_status",
+ trigger_option_ids: [
+ "family_background.parents_survival_status.both_parents_are_alive",
+ ],
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: "family_background.parents_survival_status",
+ trigger_option_ids: [
+ "family_background.parents_survival_status.both_parents_are_alive",
+ ],
+ operator: "any_of",
+ },
+ };
+
+ // Option A: Both parents are alive -> Visible & Required
+ const optionAAnswers = {
+ "family_background.parents_survival_status": {
+ value: "both_parents_are_alive",
+ option_id: "family_background.parents_survival_status.both_parents_are_alive",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionAAnswers)).toBe(true);
+ expect(isQuestionRequired(parentsMaritalStatus, optionAAnswers)).toBe(true);
+
+ // Option B: Father has passed away -> Hidden & Not Required
+ const optionBAnswers = {
+ "family_background.parents_survival_status": {
+ value: "father_has_passed_away",
+ option_id: "family_background.parents_survival_status.father_has_passed_away",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionBAnswers)).toBe(false);
+ expect(isQuestionRequired(parentsMaritalStatus, optionBAnswers)).toBe(false);
+
+ // Option C: Mother has passed away -> Hidden & Not Required
+ const optionCAnswers = {
+ "family_background.parents_survival_status": {
+ value: "mother_has_passed_away",
+ option_id: "family_background.parents_survival_status.mother_has_passed_away",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionCAnswers)).toBe(false);
+ expect(isQuestionRequired(parentsMaritalStatus, optionCAnswers)).toBe(false);
+
+ // Option D: Both parents have passed away -> Hidden & Not Required
+ const optionDAnswers = {
+ "family_background.parents_survival_status": {
+ value: "both_parents_have_passed_away",
+ option_id: "family_background.parents_survival_status.both_parents_have_passed_away",
+ },
+ };
+ expect(isQuestionVisible(parentsMaritalStatus, optionDAnswers)).toBe(false);
+ expect(isQuestionRequired(parentsMaritalStatus, optionDAnswers)).toBe(false);
+ });
+
+ it("should show family responsibility follow-up questions (live with you, additional details) and make them required only when responsibility options are selected", () => {
+ const parentId = "family_background.do_you_currently_have_an_ongoing_financial_caregiving_or_guardianship_responsibility_for_a_family_member";
+ const triggerOptionIds = [
+ `${parentId}.i_am_responsible_for_caring_for_my_father`,
+ `${parentId}.i_am_responsible_for_caring_for_my_mother`,
+ `${parentId}.i_am_responsible_for_caring_for_both_parents`,
+ `${parentId}.i_am_responsible_for_caring_for_a_sibling_brother_sister`,
+ `${parentId}.i_am_the_legal_guardian_or_supervisor_of_a_family_member`,
+ `${parentId}.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses`,
+ `${parentId}.i_have_other_circumstances_and_will_explain_in_the_description`,
+ `${parentId}.i_am_responsible_for_caring_for_my_parent_s_father_mother_or_both`,
+ ];
+
+ const liveWithYou: QuestionField = {
+ ...dummyQuestion,
+ id: "family_background.do_the_supported_individual_s_live_with_you",
+ title: "آیا فرد یا افراد تحت حمایت با شما زندگی میکنند؟",
+ required: false,
+ visibility: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ };
+
+ const additionalDetails: QuestionField = {
+ ...dummyQuestion,
+ id: "family_background.additional_details_about_family_responsibility",
+ title: "توضیحات تکمیلی درباره مسئولیت خانوادگی",
+ required: false,
+ visibility: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ requiredWhen: {
+ parent_question_id: parentId,
+ trigger_option_ids: triggerOptionIds,
+ operator: "any_of",
+ },
+ };
+
+ // Case 1: No ongoing responsibility selected -> Hidden & Not Required
+ const noRespAnswers = {
+ [parentId]: {
+ value: ["خیر، مسئولیت مستمری ندارم."],
+ option_id: [`${parentId}.no_i_do_not_have_any_ongoing_responsibility`],
+ },
+ };
+ expect(isQuestionVisible(liveWithYou, noRespAnswers)).toBe(false);
+ expect(isQuestionRequired(liveWithYou, noRespAnswers)).toBe(false);
+ expect(isQuestionVisible(additionalDetails, noRespAnswers)).toBe(false);
+ expect(isQuestionRequired(additionalDetails, noRespAnswers)).toBe(false);
+
+ // Case 2: Responsibility selected (e.g. Caring for father) -> Visible & Required
+ const fatherRespAnswers = {
+ [parentId]: {
+ value: ["مسئولیت مراقبت از پدر را بر عهده دارم."],
+ option_id: [`${parentId}.i_am_responsible_for_caring_for_my_father`],
+ },
+ };
+ expect(isQuestionVisible(liveWithYou, fatherRespAnswers)).toBe(true);
+ expect(isQuestionRequired(liveWithYou, fatherRespAnswers)).toBe(true);
+ expect(isQuestionVisible(additionalDetails, fatherRespAnswers)).toBe(true);
+ expect(isQuestionRequired(additionalDetails, fatherRespAnswers)).toBe(true);
+
+ // Case 3: Multiple responsibilities selected -> Visible & Required
+ const multiRespAnswers = {
+ [parentId]: {
+ value: ["مسئولیت مراقبت از پدر", "حمایت مالی"],
+ option_id: [
+ `${parentId}.i_am_responsible_for_caring_for_my_father`,
+ `${parentId}.i_regularly_provide_financial_support_for_a_family_member_s_living_expenses`,
+ ],
+ },
+ };
+ expect(isQuestionVisible(liveWithYou, multiRespAnswers)).toBe(true);
+ expect(isQuestionRequired(liveWithYou, multiRespAnswers)).toBe(true);
+ expect(isQuestionVisible(additionalDetails, multiRespAnswers)).toBe(true);
+ expect(isQuestionRequired(additionalDetails, multiRespAnswers)).toBe(true);
+ });
+
+ it("should handle Section 6 marital history and children follow-up rules properly", () => {
+ const mStatusId = "marital_history.current_marital_status";
+ const previousTriggerIds = [
+ `${mStatusId}.failed_engagement_annulled_marriage_without_living_together`,
+ `${mStatusId}.failed_marriage_contract_annulled_engagement_without_starting_joint_life`,
+ `${mStatusId}.divorced_after_living_together`,
+ `${mStatusId}.widowed`,
+ ];
+ const divorcedOrAnnulledIds = [
+ `${mStatusId}.failed_engagement_annulled_marriage_without_living_together`,
+ `${mStatusId}.failed_marriage_contract_annulled_engagement_without_starting_joint_life`,
+ `${mStatusId}.divorced_after_living_together`,
+ ];
+
+ const cStatusId = "marital_history.children_and_guardianship_status";
+ const hasChildrenTriggerIds = [
+ `${cStatusId}.have_children_living_with_me`,
+ `${cStatusId}.have_children_not_living_with_me`,
+ ];
+
+ const prevDuration: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.previous_marriage_duration",
+ title: "مدت ازدواج یا عقد قبلی",
+ required: false,
+ visibility: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ };
+
+ const reasonSeparation: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.reason_for_separation",
+ title: "علت جدایی، در صورت وجود",
+ required: false,
+ visibility: { parent_question_id: mStatusId, trigger_option_ids: divorcedOrAnnulledIds, operator: "any_of" },
+ };
+
+ const childrenStatus: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.children_and_guardianship_status",
+ title: "وضعیت فرزند و تکفل",
+ required: false,
+ visibility: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: mStatusId, trigger_option_ids: previousTriggerIds, operator: "any_of" },
+ };
+
+ const numChildren: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.number_of_children",
+ title: "تعداد فرزندان",
+ required: false,
+ visibility: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ };
+
+ const custodyStatus: QuestionField = {
+ ...dummyQuestion,
+ id: "marital_history.what_is_the_custody_status_of_your_child_ren",
+ title: "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟",
+ required: false,
+ visibility: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ requiredWhen: { parent_question_id: cStatusId, trigger_option_ids: hasChildrenTriggerIds, operator: "any_of" },
+ };
+
+ // 1. Single: previous duration, reason for separation, and children status are all HIDDEN
+ const singleAns = {
+ [mStatusId]: {
+ value: "single_never_married",
+ option_id: `${mStatusId}.single_never_married`,
+ },
+ };
+ expect(isQuestionVisible(prevDuration, singleAns)).toBe(false);
+ expect(isQuestionVisible(reasonSeparation, singleAns)).toBe(false);
+ expect(isQuestionVisible(childrenStatus, singleAns)).toBe(false);
+
+ // 2. Divorced: duration, reason, and children status are all VISIBLE
+ const divorcedAns = {
+ [mStatusId]: {
+ value: "divorced_after_living_together",
+ option_id: `${mStatusId}.divorced_after_living_together`,
+ },
+ };
+ expect(isQuestionVisible(prevDuration, divorcedAns)).toBe(true);
+ expect(isQuestionRequired(prevDuration, divorcedAns)).toBe(true);
+ expect(isQuestionVisible(reasonSeparation, divorcedAns)).toBe(true);
+ expect(isQuestionVisible(childrenStatus, divorcedAns)).toBe(true);
+ expect(isQuestionRequired(childrenStatus, divorcedAns)).toBe(true);
+
+ // 3. Widowed: duration & children VISIBLE, reason for separation HIDDEN
+ const widowedAns = {
+ [mStatusId]: {
+ value: "widowed",
+ option_id: `${mStatusId}.widowed`,
+ },
+ };
+ expect(isQuestionVisible(prevDuration, widowedAns)).toBe(true);
+ expect(isQuestionRequired(prevDuration, widowedAns)).toBe(true);
+ expect(isQuestionVisible(reasonSeparation, widowedAns)).toBe(false);
+ expect(isQuestionVisible(childrenStatus, widowedAns)).toBe(true);
+ expect(isQuestionRequired(childrenStatus, widowedAns)).toBe(true);
+
+ // 4. Children follow-ups: when children exist -> VISIBLE & REQUIRED
+ const hasChildAns = {
+ [cStatusId]: {
+ value: "have_children_living_with_me",
+ option_id: `${cStatusId}.have_children_living_with_me`,
+ },
+ };
+ expect(isQuestionVisible(numChildren, hasChildAns)).toBe(true);
+ expect(isQuestionRequired(numChildren, hasChildAns)).toBe(true);
+ expect(isQuestionVisible(custodyStatus, hasChildAns)).toBe(true);
+ expect(isQuestionRequired(custodyStatus, hasChildAns)).toBe(true);
+
+ // 5. No children: child questions HIDDEN & NOT REQUIRED
+ const noChildAns = {
+ [cStatusId]: {
+ value: "no_children",
+ option_id: `${cStatusId}.no_children`,
+ },
+ };
+ expect(isQuestionVisible(numChildren, noChildAns)).toBe(false);
+ expect(isQuestionRequired(numChildren, noChildAns)).toBe(false);
+ expect(isQuestionVisible(custodyStatus, noChildAns)).toBe(false);
+ expect(isQuestionRequired(custodyStatus, noChildAns)).toBe(false);
+ });
});
diff --git a/src/lib/conditional-rules.ts b/src/lib/conditional-rules.ts
index 7aeee6e..b9fd253 100644
--- a/src/lib/conditional-rules.ts
+++ b/src/lib/conditional-rules.ts
@@ -58,6 +58,16 @@ export function canonicalRule(rule: any): CanonicalRule | null {
if (rule.audience && typeof rule.audience === "object") {
result.audience = rule.audience;
+ } else if (
+ rule.genders ||
+ rule.minAge !== undefined ||
+ rule.maxAge !== undefined
+ ) {
+ result.audience = {
+ genders: rule.genders,
+ minAge: rule.minAge,
+ maxAge: rule.maxAge,
+ };
}
if (Array.isArray(rule.conditions)) {
@@ -140,19 +150,24 @@ export function matchesAudience(
}
if (audience.genders && audience.genders.length > 0) {
- if (context?.gender && !audience.genders.includes(context.gender)) {
+ if (
+ !context?.gender ||
+ !audience.genders
+ .map((g) => g.toLowerCase())
+ .includes(context.gender.toLowerCase())
+ ) {
return false;
}
}
- if (audience.minAge !== undefined && context?.age !== undefined && context.age !== null) {
- if (context.age < audience.minAge) {
+ if (audience.minAge !== undefined) {
+ if (context?.age === undefined || context.age === null || context.age < audience.minAge) {
return false;
}
}
- if (audience.maxAge !== undefined && context?.age !== undefined && context.age !== null) {
- if (context.age > audience.maxAge) {
+ if (audience.maxAge !== undefined) {
+ if (context?.age === undefined || context.age === null || context.age > audience.maxAge) {
return false;
}
}
@@ -250,7 +265,9 @@ export function ruleMatches(
: mainMatches && conditionsResult;
}
- return mainMatches;
+ return parentId
+ ? mainMatches
+ : Boolean(!rule.audience || matchesAudience(rule.audience, context));
}
export function isQuestionVisible(
@@ -289,16 +306,16 @@ export function isQuestionRequired(
return false;
}
- if (question.required || question.baseRequired) {
- return true;
- }
-
if (question.requiredWhen) {
- if (question.requiredWhen.genders || question.requiredWhen.minAge || question.requiredWhen.maxAge) {
+ if (
+ question.requiredWhen.genders ||
+ question.requiredWhen.minAge !== undefined ||
+ question.requiredWhen.maxAge !== undefined
+ ) {
return matchesAudience(question.requiredWhen, context);
}
return ruleMatches(question.requiredWhen, answers, context);
}
- return false;
+ return Boolean(question.baseRequired ?? question.required);
}
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/ar.json b/src/translations/locales/ar.json
index a4d1241..614fd9f 100644
--- a/src/translations/locales/ar.json
+++ b/src/translations/locales/ar.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "أدخل اسم الدواء وسبب الاستخدام...",
"وضعیت سلامت جسمانی": "حالة الصحة الجسدية",
"توضیحات وضعیت جسمانی": "وصف الصحة الجسدية",
- "Name": "الاسم"
-}
\ No newline at end of file
+ "Name": "الاسم",
+ "upload_certificates": "تحميل الشهادات والوثائق",
+ "add_another_document": "إضافة وثيقة أخرى",
+ "max_files_reached": "تم تحميل الحد الأقصى (4 ملفات)",
+ "remove_document": "حذف الوثيقة",
+ "upload_failed": "فشل التحميل. يرجى المحاولة مرة أخرى."
+}
diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json
index be8ef8f..53cbbb8 100644
--- a/src/translations/locales/az.json
+++ b/src/translations/locales/az.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Dərmanın adını və istifadə səbəbini daxil edin...",
"وضعیت سلامت جسمانی": "Fiziki Sağlamlıq Vəziyyəti",
"توضیحات وضعیت جسمانی": "Fiziki Sağlamlıq Təsviri",
- "Name": "Ad"
-}
\ No newline at end of file
+ "Name": "Ad",
+ "upload_certificates": "Sənədləri yükləyin",
+ "add_another_document": "Başqa sənəd əlavə edin",
+ "max_files_reached": "Maksimum 4 fayl yükləndi",
+ "remove_document": "Sənədi sil",
+ "upload_failed": "Yükləmə uğursuz oldu. Yenidən cəhd edin."
+}
diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json
index cd3a011..d275f8e 100644
--- a/src/translations/locales/bn.json
+++ b/src/translations/locales/bn.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "ওষুধের নাম এবং কারণ লিখুন...",
"وضعیت سلامت جسمانی": "শারীরিক স্বাস্থ্যের অবস্থা",
"توضیحات وضعیت جسمانی": "শারীরিক স্বাস্থ্যের বিবরণ",
- "Name": "নাম"
-}
\ No newline at end of file
+ "Name": "নাম",
+ "upload_certificates": "নথিপত্র আপলোড করুন",
+ "add_another_document": "অন্য নথি যোগ করুন",
+ "max_files_reached": "সর্বোচ্চ ৪টি ফাইল আপলোড করা হয়েছে",
+ "remove_document": "নথি মুছুন",
+ "upload_failed": "আপলোড ব্যর্থ হয়েছে। আবার চেষ্টা করুন।"
+}
diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json
index 6ca63b0..ac26ef6 100644
--- a/src/translations/locales/da.json
+++ b/src/translations/locales/da.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Indtast medicinnavn og årsag...",
"وضعیت سلامت جسمانی": "Fysisk helbredstilstand",
"توضیحات وضعیت جسمانی": "Beskrivelse af fysisk helbred",
- "Name": "Navn"
-}
\ No newline at end of file
+ "Name": "Navn",
+ "upload_certificates": "Upload certifikater",
+ "add_another_document": "Tilføj et andet dokument",
+ "max_files_reached": "Maksimalt 4 filer uploadet",
+ "remove_document": "Fjern dokument",
+ "upload_failed": "Upload mislykkedes. Prøv igen."
+}
diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json
index 4f19576..b3ab795 100644
--- a/src/translations/locales/de.json
+++ b/src/translations/locales/de.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Geben Sie den Medikamentennamen und den Grund ein...",
"وضعیت سلامت جسمانی": "Körperlicher Gesundheitszustand",
"توضیحات وضعیت جسمانی": "Beschreibung des körperlichen Zustands",
- "Name": "Name"
-}
\ No newline at end of file
+ "Name": "Name",
+ "upload_certificates": "Zertifikate hochladen",
+ "add_another_document": "Ein weiteres Dokument hinzufügen",
+ "max_files_reached": "Maximal 4 Dateien hochgeladen",
+ "remove_document": "Dokument entfernen",
+ "upload_failed": "Upload fehlgeschlagen. Bitte versuchen Sie es erneut."
+}
diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json
index a34416a..15abd91 100644
--- a/src/translations/locales/en.json
+++ b/src/translations/locales/en.json
@@ -248,8 +248,8 @@
"General Health:": "General Health:",
"German": "German",
"Germany": "Germany",
- "Get Advisor": "Get Advisor",
- "Get an advisor": "Get an advisor",
+ "Get Advisor": "Talk to Advisor",
+ "Get an advisor": "Need Marriage Guidance?",
"Glasser 5 Needs Test": "Glasser 5 Needs Test",
"Go back": "Go back",
"Good": "Good",
@@ -406,7 +406,7 @@
"Not a good personal fit": "Not a good personal fit",
"Not committed": "Not committed",
"Not important": "Not important",
- "Not sure what to do next? Our psychology section is here to guide you at every step.": "Not sure what to do next? Our psychology section is here to guide you at every step.",
+ "Not sure what to do next? Our psychology section is here to guide you at every step.": "Unsure about your next step? Our expert counselors are here to help you make confident decisions.",
"Nothing is shared without your consent.": "Nothing is shared without your consent.",
"Number of Children": "Number of Children",
"Number of Siblings": "Number of Siblings",
@@ -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",
"View Profile": "View Profile",
"View contact number": "View contact number",
- "View more details": "View more details",
+ "View more details": "View Full Profile",
"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",
@@ -741,6 +741,7 @@
"Your details are only used for the matching process.": "Your details are only used for the matching process.",
"Your information is kept strictly confidential.": "Your information is kept strictly confidential.",
"Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.",
+ "Your request has been sent. Once the gentleman reviews your request, you will be notified.": "Your request has been sent. Once the gentleman reviews your request, you will be notified.",
"Your request has been sent. Once the lady reviews your request, you will be notified.": "Your request has been sent. Once the lady reviews your request, you will be notified.",
"Your request was rejected": "Your request was rejected",
"Your request was rejected by the lady. You will be introduced to other candidates in the future.": "Your request was rejected by the lady. You will be introduced to other candidates in the future.",
@@ -845,5 +846,10 @@
"Medication Name and Reason for Use": "Medication Name and Reason for Use",
"Enter medication name and reason for use...": "Enter medication name and reason for use...",
"Enter a valid phone number with country code.": "Enter a valid phone number with country code.",
- "Name": "Name"
-}
\ No newline at end of file
+ "Name": "Name",
+ "upload_certificates": "Upload certificates",
+ "add_another_document": "Add another document",
+ "max_files_reached": "Maximum of 4 files uploaded",
+ "remove_document": "Remove document",
+ "upload_failed": "Upload failed. Please try again."
+}
diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json
index cdbd522..69c328d 100644
--- a/src/translations/locales/es.json
+++ b/src/translations/locales/es.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Ingrese el nombre del medicamento y el motivo...",
"وضعیت سلامت جسمانی": "Estado de salud física",
"توضیحات وضعیت جسمانی": "Descripción de la salud física",
- "Name": "Nombre"
-}
\ No newline at end of file
+ "Name": "Nombre",
+ "upload_certificates": "Subir certificados",
+ "add_another_document": "Agregar otro documento",
+ "max_files_reached": "Máximo de 4 archivos subidos",
+ "remove_document": "Eliminar documento",
+ "upload_failed": "Error al subir. Por favor, inténtelo de nuevo."
+}
diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json
index bb11a05..4748c18 100644
--- a/src/translations/locales/fa.json
+++ b/src/translations/locales/fa.json
@@ -248,8 +248,8 @@
"General Health:": "سلامت عمومی:",
"German": "آلمانی",
"Germany": "آلمان",
- "Get Advisor": "دریافت مشاور",
- "Get an advisor": "دریافت مشاور",
+ "Get Advisor": "گفتگو با مشاور",
+ "Get an advisor": "نیاز به راهنمایی دارید؟",
"Glasser 5 Needs Test": "تست ۵ نیاز گلاسر",
"Go back": "بازگشت",
"Good": "خوب",
@@ -406,7 +406,7 @@
"Not a good personal fit": "تناسب شخصی کافی نبود",
"Not committed": "مقید نیستم",
"Not important": "این معیار برایم اهمیت زیادی ندارد.",
- "Not sure what to do next? Our psychology section is here to guide you at every step.": "نمیدانید قدم بعدی چیست؟ بخش روانشناسی ما در هر مرحله شما را راهنمایی میکند.",
+ "Not sure what to do next? Our psychology section is here to guide you at every step.": "در تصمیمگیری یا ادامه مسیر مردد هستید؟ مشاوران متخصص ما در تمام مراحل آشنایی همراه شما هستند.",
"Nothing is shared without your consent.": "هیچ چیز بدون رضایت شما به اشتراک گذاشته نمیشود.",
"Number of Children": "تعداد فرزندان",
"Number of Siblings": "تعداد خواهر و برادر",
@@ -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": "در مسیر آشنایی و خواستگاری هستیم و هنوز هیچی نهایی نشده",
@@ -740,7 +740,7 @@
"Your Personality Traits": "ویژگیهای شخصیتی خودتان",
"Your details are only used for the matching process.": "اطلاعات شما فقط برای فرآیند تطبیقدهی استفاده میشود.",
"Your information is kept strictly confidential.": "اطلاعات شما کاملاً محرمانه نگهداری میشود.",
- "Your privacy and safety are our top priorities. We are committed to keeping your information secure and giving you full control throughout the process.": "حفظ حریم خصوصی و امنیت شما بالاترین اولویت ماست. ما متعهد به حفظ امنیت اطلاعات شما و اعطای کنترل کامل به شما در طول فرآیند هستیم.",
+ "Your request has been sent. Once the gentleman reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط آقا، به شما اطلاعرسانی خواهد شد.",
"Your request has been sent. Once the lady reviews your request, you will be notified.": "درخواست شما ارسال شد. پس از بررسی درخواست شما توسط خانم، به شما اطلاعرسانی خواهد شد.",
"Your request was rejected": "درخواست شما رد شد",
"Your request was rejected by the lady. You will be introduced to other candidates in the future.": "درخواست شما توسط خانم رد شد. به شما مورد های دیگه ای در اینده معرفی خواهد شد.",
@@ -856,5 +856,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "نام دارو و دلیل مصرف را وارد نمایید...",
"وضعیت سلامت جسمانی": "وضعیت سلامت جسمانی",
"توضیحات وضعیت جسمانی": "توضیحات وضعیت جسمانی",
- "Name": "نام"
-}
\ No newline at end of file
+ "Name": "نام",
+ "upload_certificates": "بارگذاری مدارک",
+ "add_another_document": "افزودن مدرک جدید",
+ "max_files_reached": "حداکثر ۴ فایل بارگذاری شده است",
+ "remove_document": "حذف مدرک",
+ "upload_failed": "بارگذاری با خطا مواجه شد. لطفاً دوباره تلاش کنید."
+}
diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json
index 282e887..e3f2496 100644
--- a/src/translations/locales/fr.json
+++ b/src/translations/locales/fr.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Entrez le nom du médicament et le motif...",
"وضعیت سلامت جسمانی": "État de santé physique",
"توضیحات وضعیت جسمانی": "Description de la santé physique",
- "Name": "Nom"
-}
\ No newline at end of file
+ "Name": "Nom",
+ "upload_certificates": "Télécharger les certificats",
+ "add_another_document": "Ajouter un autre document",
+ "max_files_reached": "Maximum de 4 fichiers téléchargés",
+ "remove_document": "Supprimer le document",
+ "upload_failed": "Échec du téléchargement. Veuillez réessayer."
+}
diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json
index fe99916..dce5747 100644
--- a/src/translations/locales/gu.json
+++ b/src/translations/locales/gu.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "દવાનું નામ અને કારણ દાખલ કરો...",
"وضعیت سلامت جسمانی": "શારીરિક સ્વાસ્થ્ય સ્થિતિ",
"توضیحات وضعیت جسمانی": "શારીરિક સ્વાસ્થ્ય વર્ણન",
- "Name": "નામ"
-}
\ No newline at end of file
+ "Name": "નામ",
+ "upload_certificates": "પ્રમાણપત્રો અપલોડ કરો",
+ "add_another_document": "બીજો દસ્તાવેજ ઉમેરો",
+ "max_files_reached": "મહત્તમ 4 ફાઇલો અપલોડ કરવામાં આવી છે",
+ "remove_document": "દસ્તાવેજ દૂર કરો",
+ "upload_failed": "અપલોડ નિષ્ફળ ગયું. કૃપા કરીને ફરી પ્રયાસ કરો."
+}
diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json
index 986c768..e7c1c8b 100644
--- a/src/translations/locales/ha.json
+++ b/src/translations/locales/ha.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Shigar da sunan magani da dalilin sha...",
"وضعیت سلامت جسمانی": "Yanayin Lafiyar Jiki",
"توضیحات وضعیت جسمانی": "Bayanin Lafiyar Jiki",
- "Name": "Suna"
-}
\ No newline at end of file
+ "Name": "Suna",
+ "upload_certificates": "Loda takardun shaida",
+ "add_another_document": "Ƙara wata takarda",
+ "max_files_reached": "An loda matsakaicin fayiloli 4",
+ "remove_document": "Cire takarda",
+ "upload_failed": "Loda ya faskara. Da fatan za a sake gwadawa."
+}
diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json
index 0fbd3ef..50b80e2 100644
--- a/src/translations/locales/he.json
+++ b/src/translations/locales/he.json
@@ -355,5 +355,10 @@
"Job Title": "תואר התפקיד",
"Employment Status": "מצב תעסוקתי",
"Your Hobbies and Main Interests": "התחביבים ותחומי העניין העיקריים שלך",
- "View more details": "הצג פרטים נוספים"
-}
\ No newline at end of file
+ "View more details": "הצג פרטים נוספים",
+ "upload_certificates": "העלאת תעודות ומסמכים",
+ "add_another_document": "הוסף מסמך נוסף",
+ "max_files_reached": "הועלו מקסימום 4 קבצים",
+ "remove_document": "הסר מסמך",
+ "upload_failed": "ההעלאה נכשלה. אנא נסה שוב."
+}
diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json
index edfc79b..ca2ea00 100644
--- a/src/translations/locales/hi.json
+++ b/src/translations/locales/hi.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "दवा का नाम और उपयोग का कारण दर्ज करें...",
"وضعیت سلامت جسمانی": "शारीरिक स्वास्थ्य की स्थिति",
"توضیحات وضعیت جسمانی": "शारीरिक स्वास्थ्य का विवरण",
- "Name": "नाम"
-}
\ No newline at end of file
+ "Name": "नाम",
+ "upload_certificates": "प्रमाणपत्र अपलोड करें",
+ "add_another_document": "दूसरा दस्तावेज़ जोड़ें",
+ "max_files_reached": "अधिकतम 4 फ़ाइलें अपलोड की गईं",
+ "remove_document": "दस्तावेज़ हटाएं",
+ "upload_failed": "अपलोड विफल रहा। कृपया पुन: प्रयास करें।"
+}
diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json
index 15bb748..598c76b 100644
--- a/src/translations/locales/id.json
+++ b/src/translations/locales/id.json
@@ -355,5 +355,10 @@
"Job Title": "Jabatan / Pekerjaan",
"Employment Status": "Status Pekerjaan",
"Your Hobbies and Main Interests": "Hobi dan Minat Utama Anda",
- "View more details": "Lihat detail selengkapnya"
-}
\ No newline at end of file
+ "View more details": "Lihat detail selengkapnya",
+ "upload_certificates": "Unggah sertifikat",
+ "add_another_document": "Tambah dokumen lain",
+ "max_files_reached": "Maksimum 4 file diunggah",
+ "remove_document": "Hapus dokumen",
+ "upload_failed": "Pengunggahan gagal. Silakan coba lagi."
+}
diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json
index ff18a96..cdf402a 100644
--- a/src/translations/locales/ks.json
+++ b/src/translations/locales/ks.json
@@ -355,5 +355,10 @@
"Job Title": "کٲم ہُنٛد ناو",
"Employment Status": "مُلازمتٕچ حالت",
"Your Hobbies and Main Interests": "تُہنٛدؠ شۄق تہٕ اَہَم دِلچسپی",
- "View more details": "مزید تفصیل وُچھِو"
-}
\ No newline at end of file
+ "View more details": "مزید تفصیل وُچھِو",
+ "upload_certificates": "دستاویز اپ لوڈ کریو",
+ "add_another_document": "بیٛاکھ دستاویز جمع کریو",
+ "max_files_reached": "زیاد کھوتہ زیاد ۴ فائل اپ لوڈ کرنہ آمژٕ",
+ "remove_document": "دستاویز ہٹاوِیو",
+ "upload_failed": "اپ لوڈ ناکام۔ مہربانی کرتھ دوبارہ کوشش کریو۔"
+}
diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json
index 5ed018f..2530412 100644
--- a/src/translations/locales/pt.json
+++ b/src/translations/locales/pt.json
@@ -355,5 +355,10 @@
"Job Title": "Cargo / Título profissional",
"Employment Status": "Situação profissional",
"Your Hobbies and Main Interests": "Seus hobbies e principais interesses",
- "View more details": "Ver mais detalhes"
-}
\ No newline at end of file
+ "View more details": "Ver mais detalhes",
+ "upload_certificates": "Enviar certificados",
+ "add_another_document": "Adicionar outro documento",
+ "max_files_reached": "Máximo de 4 arquivos enviados",
+ "remove_document": "Remover documento",
+ "upload_failed": "Falha no envio. Por favor, tente novamente."
+}
diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json
index 268671b..5241ead 100644
--- a/src/translations/locales/ru.json
+++ b/src/translations/locales/ru.json
@@ -812,5 +812,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "Введите название препарата и причину приема...",
"وضعیت سلامت جسمانی": "Физическое состояние здоровья",
"توضیحات وضعیت جسمانی": "Описание физического состояния",
- "Name": "Имя"
-}
\ No newline at end of file
+ "Name": "Имя",
+ "upload_certificates": "Загрузить сертификаты",
+ "add_another_document": "Добавить еще один документ",
+ "max_files_reached": "Загружено максимум 4 файла",
+ "remove_document": "Удалить документ",
+ "upload_failed": "Ошибка загрузки. Пожалуйста, повторите попытку."
+}
diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json
index 224c42e..5190fa7 100644
--- a/src/translations/locales/sw.json
+++ b/src/translations/locales/sw.json
@@ -355,5 +355,10 @@
"Job Title": "Wadhifa wa Kazi",
"Employment Status": "Hali ya Ajira",
"Your Hobbies and Main Interests": "Mambo unayopenda na Maslahi Kuu",
- "View more details": "Angalia maelezo zaidi"
-}
\ No newline at end of file
+ "View more details": "Angalia maelezo zaidi",
+ "upload_certificates": "Pakia vyeti",
+ "add_another_document": "Ongeza hati nyingine",
+ "max_files_reached": "Upeo wa faili 4 zimepakiwa",
+ "remove_document": "Ondoa hati",
+ "upload_failed": "Upakiaji umeshindwa. Tafadhali jaribu tena."
+}
diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json
index b0dbf5e..2dfac2f 100644
--- a/src/translations/locales/tg.json
+++ b/src/translations/locales/tg.json
@@ -355,5 +355,10 @@
"Job Title": "Унвони вазифа",
"Employment Status": "Вазъи шуғл",
"Your Hobbies and Main Interests": "Машғулиятҳо ва манфиатҳои асосии шумо",
- "View more details": "Дидани тафсилоти бештар"
-}
\ No newline at end of file
+ "View more details": "Дидани тафсилоти бештар",
+ "upload_certificates": "Боргузории ҳуҷҷатҳо",
+ "add_another_document": "Ҳуҷҷати дигар илова кунед",
+ "max_files_reached": "Ҳадди аксар 4 файл боргузорӣ шудааст",
+ "remove_document": "Ҳуҷҷатро нест кунед",
+ "upload_failed": "Боргузорӣ ноком шуд. Лутфан бори дигар кӯшиш кунед."
+}
diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json
index 0f5be69..402c6ca 100644
--- a/src/translations/locales/tr.json
+++ b/src/translations/locales/tr.json
@@ -355,5 +355,10 @@
"Job Title": "Meslek / Unvan",
"Employment Status": "Çalışma Durumu",
"Your Hobbies and Main Interests": "Hobileriniz ve Temel İlgi Alanlarınız",
- "View more details": "Daha fazla ayrıntı gör"
-}
\ No newline at end of file
+ "View more details": "Daha fazla ayrıntı gör",
+ "upload_certificates": "Belgeleri yükle",
+ "add_another_document": "Başka bir belge ekle",
+ "max_files_reached": "Maksimum 4 dosya yüklendi",
+ "remove_document": "Belgeyi kaldır",
+ "upload_failed": "Yükleme başarısız oldu. Lütfen tekrar deneyin."
+}
diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json
index 3ba710b..76f347d 100644
--- a/src/translations/locales/ul.json
+++ b/src/translations/locales/ul.json
@@ -355,5 +355,10 @@
"Job Title": "Job Title",
"Employment Status": "Employment Status",
"Your Hobbies and Main Interests": "Your Hobbies and Main Interests",
- "View more details": "View more details"
-}
\ No newline at end of file
+ "View more details": "View more details",
+ "upload_certificates": "دستاویزات اپ لوڈ کریں",
+ "add_another_document": "مزید دستاویز شامل کریں",
+ "max_files_reached": "زیادہ سے زیادہ 4 فائلیں اپ لوڈ کی گئیں",
+ "remove_document": "دستاویز ہٹائیں",
+ "upload_failed": "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔"
+}
diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json
index 7677d0e..eb53bcc 100644
--- a/src/translations/locales/ur.json
+++ b/src/translations/locales/ur.json
@@ -355,5 +355,10 @@
"Job Title": "عہدہ / ملازمت کا عنوان",
"Employment Status": "ملازمت کی صورتحال",
"Your Hobbies and Main Interests": "آپ کے مشاغل اور اہم دلچسپیاں",
- "View more details": "مزید تفصیلات دیکھیں"
-}
\ No newline at end of file
+ "View more details": "مزید تفصیلات دیکھیں",
+ "upload_certificates": "دستاویزات اپ لوڈ کریں",
+ "add_another_document": "مزید دستاویز شامل کریں",
+ "max_files_reached": "زیادہ سے زیادہ 4 فائلیں اپ لوڈ کی گئیں",
+ "remove_document": "دستاویز ہٹائیں",
+ "upload_failed": "اپ لوڈ ناکام ہو گیا۔ براہ کرم دوبارہ کوشش کریں۔"
+}
diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json
index 9c69367..7bd6271 100644
--- a/src/translations/locales/uz.json
+++ b/src/translations/locales/uz.json
@@ -355,5 +355,10 @@
"Job Title": "Kasb / Lavozim",
"Employment Status": "Bandlik holati",
"Your Hobbies and Main Interests": "Qiziqishlaringiz va asosiy mashgʻulotlaringiz",
- "View more details": "Batafsil maʼlumotni koʻrish"
-}
\ No newline at end of file
+ "View more details": "Batafsil maʼlumotni koʻrish",
+ "upload_certificates": "Hujjatlarni yuklash",
+ "add_another_document": "Boshqa hujjat qo'shish",
+ "max_files_reached": "Maksimal 4 ta fayl yuklandi",
+ "remove_document": "Hujjatni o'chirish",
+ "upload_failed": "Yuklab bo'lmadi. Qayta urinib ko'ring."
+}
diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json
index 9cd097a..667bd97 100644
--- a/src/translations/locales/zh.json
+++ b/src/translations/locales/zh.json
@@ -808,5 +808,10 @@
"نام دارو و دلیل مصرف را وارد نمایید...": "输入药物名称和使用原因...",
"وضعیت سلامت جسمانی": "身体健康状况",
"توضیحات وضعیت جسمانی": "身体健康说明",
- "Name": "姓名"
-}
\ No newline at end of file
+ "Name": "姓名",
+ "upload_certificates": "上传证书和文件",
+ "add_another_document": "添加其他文件",
+ "max_files_reached": "最多已上传 4 个文件",
+ "remove_document": "删除文件",
+ "upload_failed": "上传失败,请重试。"
+}
{title}
-+
{description}
-+ {pt} +
+ ))} +