Browse Source

feat: implement core matchmaking application structure and multi-language support

front-test-2
ghorbani 3 weeks ago
parent
commit
ab8b055d8a
  1. 2
      AUTO_CLICK_SETUP_GUIDE.md
  2. 2
      src/app/[lang]/layout.tsx
  3. 2
      src/app/[lang]/test-webview/page.tsx
  4. 8
      src/app/candidate-contact/page.tsx
  5. 6
      src/app/finding-match/page.tsx
  6. 6
      src/app/intro/page.tsx
  7. 4
      src/app/layout.tsx
  8. 8
      src/app/new-match/page.tsx
  9. 14
      src/app/new-match/profile/page.tsx
  10. 2
      src/app/providers.tsx
  11. 4
      src/app/questions-list/[slug]/answer-pace-sheet.tsx
  12. 20
      src/app/questions-list/[slug]/question-detail-client.tsx
  13. 8
      src/app/questions-list/[slug]/test-intro-client.tsx
  14. 14
      src/app/questions-list/page.tsx
  15. 4
      src/app/questions-list/sections-request.tsx
  16. 12
      src/app/request-accepted/page.tsx
  17. 6
      src/app/request-sent/page.tsx
  18. 2
      src/app/terms/page.tsx
  19. 2
      src/components/Componentes/advisor-actions-card.tsx
  20. 0
      src/components/Componentes/booking-terms-card.tsx
  21. 0
      src/components/Componentes/button.tsx
  22. 0
      src/components/Componentes/call-result-sheet.tsx
  23. 0
      src/components/Componentes/conditional-questions.tsx
  24. 0
      src/components/Componentes/dev-click-to-component.tsx
  25. 2
      src/components/Componentes/dismiss-reason-sheet.tsx
  26. 29
      src/components/Componentes/explanation-ui-font.tsx
  27. 0
      src/components/Componentes/female-consent-sheet.tsx
  28. 0
      src/components/Componentes/fixed-bottom.tsx
  29. 0
      src/components/Componentes/habib_marriage_form.md
  30. 2
      src/components/Componentes/help-modal.tsx
  31. 0
      src/components/Componentes/info-progress-card.tsx
  32. 2
      src/components/Componentes/information-sheet.tsx
  33. 0
      src/components/Componentes/language-switcher.tsx
  34. 0
      src/components/Componentes/locator-paths.ts
  35. 2
      src/components/Componentes/navigation-button.tsx
  36. 0
      src/components/Componentes/notice-box.tsx
  37. 0
      src/components/Componentes/page-background.tsx
  38. 454
      src/components/Componentes/progress-helper.ts
  39. 0
      src/components/Componentes/question-answer-storage.tsx
  40. 0
      src/components/Componentes/question-birthplace.tsx
  41. 0
      src/components/Componentes/question-button.tsx
  42. 0
      src/components/Componentes/question-card.tsx
  43. 32
      src/components/Componentes/question-checkbox.tsx
  44. 0
      src/components/Componentes/question-date.tsx
  45. 162
      src/components/Componentes/question-dropdown.tsx
  46. 2
      src/components/Componentes/question-exit-navigation-button.tsx
  47. 0
      src/components/Componentes/question-file.tsx
  48. 0
      src/components/Componentes/question-number.tsx
  49. 0
      src/components/Componentes/question-phone.tsx
  50. 2
      src/components/Componentes/question-photo.tsx
  51. 0
      src/components/Componentes/question-progress-tracker.tsx
  52. 39
      src/components/Componentes/question-radio.tsx
  53. 33
      src/components/Componentes/question-renderer.tsx
  54. 4
      src/components/Componentes/question-section-flow.tsx
  55. 287
      src/components/Componentes/question-slider.tsx
  56. 0
      src/components/Componentes/question-snap-list.tsx
  57. 49
      src/components/Componentes/question-text.tsx
  58. 58
      src/components/Componentes/question-textarea.tsx
  59. 96
      src/components/Componentes/question-title.tsx
  60. 2
      src/components/Componentes/report-actions-sheet.tsx
  61. 2
      src/components/Componentes/required-steps-card.tsx
  62. 16
      src/components/Componentes/slider-page.tsx
  63. 2
      src/components/Componentes/slider-slide-five.tsx
  64. 4
      src/components/Componentes/slider-slide-four.tsx
  65. 4
      src/components/Componentes/slider-slide-one.tsx
  66. 2
      src/components/Componentes/slider-slide-three.tsx
  67. 2
      src/components/Componentes/slider-slide-two.tsx
  68. 0
      src/components/Componentes/slider-slide.tsx
  69. 0
      src/components/Componentes/sticky-header.tsx
  70. 2
      src/components/Componentes/subscription-required-sheet.tsx
  71. 2
      src/components/Componentes/test-intro-page.tsx
  72. 4
      src/components/Componentes/test-loading-screen.tsx
  73. 8
      src/components/Componentes/test-questions-flow.tsx
  74. 0
      src/components/Componentes/token-switcher.tsx
  75. 0
      src/components/Componentes/view-paddings-provider.tsx
  76. 228
      src/components/questions/progress-helper.ts
  77. 138
      src/components/questions/question-slider.tsx
  78. 86
      src/components/questions/question-title.tsx
  79. 4
      src/components/utils/dev-click-to-component.tsx
  80. 60
      src/data/question-data.ts
  81. 77
      src/data/questions/en.json
  82. 77
      src/data/questions/fa.json
  83. 12
      src/translations/locales/ar.json
  84. 12
      src/translations/locales/az.json
  85. 12
      src/translations/locales/bn.json
  86. 12
      src/translations/locales/da.json
  87. 12
      src/translations/locales/de.json
  88. 12
      src/translations/locales/en.json
  89. 12
      src/translations/locales/es.json
  90. 12
      src/translations/locales/fa.json
  91. 12
      src/translations/locales/fr.json
  92. 12
      src/translations/locales/gu.json
  93. 12
      src/translations/locales/ha.json
  94. 12
      src/translations/locales/he.json
  95. 12
      src/translations/locales/hi.json
  96. 12
      src/translations/locales/id.json
  97. 12
      src/translations/locales/ks.json
  98. 12
      src/translations/locales/pt.json
  99. 12
      src/translations/locales/ru.json
  100. 12
      src/translations/locales/sw.json

2
AUTO_CLICK_SETUP_GUIDE.md

@ -69,7 +69,7 @@ Next.js باید بتواند پلاگین ما را بخواند. یک فایل
در نهایت، فایل اصلی پروژه خود (مثلاً `src/pages/_app.tsx` یا `src/app/layout.tsx`) را باز کنید و کامپوننت را در محیط توسعه (Development) فراخوانی کنید: در نهایت، فایل اصلی پروژه خود (مثلاً `src/pages/_app.tsx` یا `src/app/layout.tsx`) را باز کنید و کامپوننت را در محیط توسعه (Development) فراخوانی کنید:
```tsx ```tsx
import { DevClickToComponent } from '@/components/utils/dev-click-to-component';
import { DevClickToComponent } from '@/components/Componentes/dev-click-to-component';
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
return ( return (

2
src/app/[lang]/layout.tsx

@ -1,5 +1,5 @@
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import LanguageSwitcher from "@/components/ui/language-switcher";
import LanguageSwitcher from "@/components/Componentes/language-switcher";
import { isLocale, localeDirections } from "@/translations/config"; import { isLocale, localeDirections } from "@/translations/config";
import { I18nProvider } from "@/translations/provider"; import { I18nProvider } from "@/translations/provider";

2
src/app/[lang]/test-webview/page.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Button from "@/components/ui/button";
import Button from "@/components/Componentes/button";
import { useFlutterBridge } from "@/hooks/useFlutterBridge"; import { useFlutterBridge } from "@/hooks/useFlutterBridge";
export default function TestWebViewPage() { export default function TestWebViewPage() {

8
src/app/candidate-contact/page.tsx

@ -2,10 +2,10 @@
import Image from "next/image"; import Image from "next/image";
import { useState } from "react"; import { useState } from "react";
import Button from "@/components/ui/button";
import CallResultSheet from "@/components/ui/call-result-sheet";
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import Button from "@/components/Componentes/button";
import CallResultSheet from "@/components/Componentes/call-result-sheet";
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
export default function CandidateContactPage() { export default function CandidateContactPage() {

6
src/app/finding-match/page.tsx

@ -5,9 +5,9 @@ import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { FaPen } from "react-icons/fa6"; import { FaPen } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { getSubmitPath } from "@/lib/get-submit-path"; import { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";

6
src/app/intro/page.tsx

@ -3,9 +3,9 @@
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import Button from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
import ReportActionsSheet from "@/components/ui/report-actions-sheet";
import Button from "@/components/Componentes/button";
import NavigationButton from "@/components/Componentes/navigation-button";
import ReportActionsSheet from "@/components/Componentes/report-actions-sheet";
import type { MarriageProfileResponse } from "@/hooks/marriage/types"; import type { MarriageProfileResponse } from "@/hooks/marriage/types";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { authBridge } from "@/lib/auth-bridge"; import { authBridge } from "@/lib/auth-bridge";

4
src/app/layout.tsx

@ -4,8 +4,8 @@ import localFont from "next/font/local";
import Script from "next/script"; import Script from "next/script";
import Providers from "./providers"; import Providers from "./providers";
import "./globals.css"; import "./globals.css";
import DevClickToComponent from "@/components/dev/dev-click-to-component";
import TokenSwitcher from "@/components/ui/token-switcher";
import DevClickToComponent from "@/components/Componentes/dev-click-to-component";
import TokenSwitcher from "@/components/Componentes/token-switcher";
const faminela = localFont({ const faminela = localFont({
src: "../../public/fonts/Faminela/Faminela.otf", src: "../../public/fonts/Faminela/Faminela.otf",

8
src/app/new-match/page.tsx

@ -5,10 +5,10 @@ import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { FaLock } from "react-icons/fa6"; import { FaLock } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import { DotsLoader } from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import { DotsLoader } from "@/components/Componentes/button";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import type { import type {
MarriageField, MarriageField,
MarriageFieldValue, MarriageFieldValue,

14
src/app/new-match/profile/page.tsx

@ -3,13 +3,13 @@
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import Button, { DotsLoader } from "@/components/ui/button";
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import InformationSheet from "@/components/ui/information-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
import Button, { DotsLoader } from "@/components/Componentes/button";
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet";
import InformationSheet from "@/components/Componentes/information-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
import StickyHeader from "@/components/Componentes/sticky-header";
import { PageBackground } from "@/components/Componentes/page-background";
import type { import type {
MarriageCaseStatus, MarriageCaseStatus,
MarriageField, MarriageField,

2
src/app/providers.tsx

@ -6,7 +6,7 @@ import {
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { type ReactNode, useEffect, useState } from "react"; import { type ReactNode, useEffect, useState } from "react";
import { ViewPaddingsProvider } from "@/components/utils/view-paddings-provider";
import { ViewPaddingsProvider } from "@/components/Componentes/view-paddings-provider";
function AppFocusReloader({ children }: { children: ReactNode }) { function AppFocusReloader({ children }: { children: ReactNode }) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();

4
src/app/questions-list/[slug]/answer-pace-sheet.tsx

@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from "react";
import { import {
getQuestionAnswersStorageKey, getQuestionAnswersStorageKey,
hasQuestionAnswerValue, hasQuestionAnswerValue,
} from "@/components/questions/question-answer-storage";
import InformationSheet from "@/components/ui/information-sheet";
} from "@/components/Componentes/question-answer-storage";
import InformationSheet from "@/components/Componentes/information-sheet";
import type { import type {
MarriageField, MarriageField,
MarriagePhoneFieldValue, MarriagePhoneFieldValue,

20
src/app/questions-list/[slug]/question-detail-client.tsx

@ -6,18 +6,18 @@ import {
hasQuestionAnswerValue, hasQuestionAnswerValue,
QuestionAnswersProvider, QuestionAnswersProvider,
useQuestionAnswers, useQuestionAnswers,
} from "@/components/questions/question-answer-storage";
import QuestionExitNavigationButton from "@/components/questions/question-exit-navigation-button";
import QuestionRenderer from "@/components/questions/question-renderer";
import QuestionSectionFlow from "@/components/questions/question-section-flow";
import TestIntroPage from "@/components/questions/test-intro-page";
} from "@/components/Componentes/question-answer-storage";
import QuestionExitNavigationButton from "@/components/Componentes/question-exit-navigation-button";
import QuestionRenderer from "@/components/Componentes/question-renderer";
import QuestionSectionFlow from "@/components/Componentes/question-section-flow";
import TestIntroPage from "@/components/Componentes/test-intro-page";
import TestQuestionsFlow, { import TestQuestionsFlow, {
type TestQuestion, type TestQuestion,
} from "@/components/questions/test-questions-flow";
import { DotsLoader } from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
} from "@/components/Componentes/test-questions-flow";
import { DotsLoader } from "@/components/Componentes/button";
import NavigationButton from "@/components/Componentes/navigation-button";
import StickyHeader from "@/components/Componentes/sticky-header";
import { PageBackground } from "@/components/Componentes/page-background";
import { cattellFallbackQuestions } from "@/data/cattell-fallback"; import { cattellFallbackQuestions } from "@/data/cattell-fallback";
import { glasserFallbackQuestions } from "@/data/glasser-fallback"; import { glasserFallbackQuestions } from "@/data/glasser-fallback";
import { import {

8
src/app/questions-list/[slug]/test-intro-client.tsx

@ -1,10 +1,10 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import TestIntroPage from "@/components/questions/test-intro-page";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
import TestIntroPage from "@/components/Componentes/test-intro-page";
import NavigationButton from "@/components/Componentes/navigation-button";
import StickyHeader from "@/components/Componentes/sticky-header";
import { PageBackground } from "@/components/Componentes/page-background";
type TestIntroClientProps = { type TestIntroClientProps = {
title: string; title: string;

14
src/app/questions-list/page.tsx

@ -7,13 +7,13 @@ import { IoClose } from "react-icons/io5";
import { import {
getLocalSectionProgress, getLocalSectionProgress,
getStoredAge, getStoredAge,
} from "@/components/questions/progress-helper";
import QuestionCard from "@/components/questions/question-card";
import RequiredStepsCard from "@/components/questions/required-steps-card";
import Button from "@/components/ui/button";
import InformationSheet from "@/components/ui/information-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
} from "@/components/Componentes/progress-helper";
import QuestionCard from "@/components/Componentes/question-card";
import RequiredStepsCard from "@/components/Componentes/required-steps-card";
import Button from "@/components/Componentes/button";
import InformationSheet from "@/components/Componentes/information-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import { import {
getQuestionListItems, getQuestionListItems,
isQuestionListItemVisibleForProfile, isQuestionListItemVisibleForProfile,

4
src/app/questions-list/sections-request.tsx

@ -2,8 +2,8 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { IoClose } from "react-icons/io5"; import { IoClose } from "react-icons/io5";
import Button from "@/components/ui/button";
import InformationSheet from "@/components/ui/information-sheet";
import Button from "@/components/Componentes/button";
import InformationSheet from "@/components/Componentes/information-sheet";
import { bookingTerms } from "@/data/question-data"; import { bookingTerms } from "@/data/question-data";
import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections";

12
src/app/request-accepted/page.tsx

@ -5,12 +5,12 @@ import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FiCopy, FiPhone } from "react-icons/fi"; import { FiCopy, FiPhone } from "react-icons/fi";
import { DotsLoader } from "@/components/ui/button";
import CallResultSheet from "@/components/ui/call-result-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
import NavigationButton from "@/components/ui/navigation-button";
import SubscriptionRequiredSheet from "@/components/ui/subscription-required-sheet";
import { PageBackground } from "@/components/utils/page-background";
import { DotsLoader } from "@/components/Componentes/button";
import CallResultSheet from "@/components/Componentes/call-result-sheet";
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet";
import NavigationButton from "@/components/Componentes/navigation-button";
import SubscriptionRequiredSheet from "@/components/Componentes/subscription-required-sheet";
import { PageBackground } from "@/components/Componentes/page-background";
import type { import type {
MarriageField, MarriageField,
MarriagePhoneFieldValue, MarriagePhoneFieldValue,

6
src/app/request-sent/page.tsx

@ -4,9 +4,9 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button";
import { PageBackground } from "@/components/utils/page-background";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
import NavigationButton from "@/components/Componentes/navigation-button";
import { PageBackground } from "@/components/Componentes/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { getSubmitPath } from "@/lib/get-submit-path"; import { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";

2
src/app/terms/page.tsx

@ -1,4 +1,4 @@
import SliderPage from "@/components/sliders/slider-page";
import SliderPage from "@/components/Componentes/slider-page";
export default function TermsRoute() { export default function TermsRoute() {
return <SliderPage />; return <SliderPage />;

2
src/components/ui/advisor-actions-card.tsx → src/components/Componentes/advisor-actions-card.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import Button from "@/components/ui/button";
import Button from "./button";
export type AdvisorAvatar = { export type AdvisorAvatar = {
id: string; id: string;

0
src/components/questions/booking-terms-card.tsx → src/components/Componentes/booking-terms-card.tsx

0
src/components/ui/button.tsx → src/components/Componentes/button.tsx

0
src/components/ui/call-result-sheet.tsx → src/components/Componentes/call-result-sheet.tsx

0
src/components/questions/conditional-questions.tsx → src/components/Componentes/conditional-questions.tsx

0
src/components/dev/dev-click-to-component.tsx → src/components/Componentes/dev-click-to-component.tsx

2
src/components/ui/dismiss-reason-sheet.tsx → src/components/Componentes/dismiss-reason-sheet.tsx

@ -2,7 +2,7 @@
import type { HTMLAttributes } from "react"; import type { HTMLAttributes } from "react";
import { useEffect, useId, useState } from "react"; import { useEffect, useId, useState } from "react";
import Button from "@/components/ui/button";
import Button from "./button";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220; const EXIT_ANIMATION_MS = 220;

29
src/components/Componentes/explanation-ui-font.tsx

@ -0,0 +1,29 @@
import type { ReactNode } from "react";
type ExplanationUiFontProps = {
children: ReactNode;
className?: string;
textColor?: string;
};
export function ExplanationUiFont({
children,
className,
textColor = "text-[#667085]",
}: ExplanationUiFontProps) {
return (
<span
className={[
"text-[12.5px] font-normal leading-normal",
textColor,
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</span>
);
}
export default ExplanationUiFont;

0
src/components/ui/female-consent-sheet.tsx → src/components/Componentes/female-consent-sheet.tsx

0
src/components/utils/fixed-bottom.tsx → src/components/Componentes/fixed-bottom.tsx

0
src/components/questions/habib_marriage_form.md → src/components/Componentes/habib_marriage_form.md

2
src/components/ui/help-modal.tsx → src/components/Componentes/help-modal.tsx

@ -3,7 +3,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import Button from "@/components/ui/button";
import Button from "./button";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220; const EXIT_ANIMATION_MS = 220;

0
src/components/ui/info-progress-card.tsx → src/components/Componentes/info-progress-card.tsx

2
src/components/ui/information-sheet.tsx → src/components/Componentes/information-sheet.tsx

@ -3,7 +3,7 @@
import Image, { type StaticImageData } from "next/image"; import Image, { type StaticImageData } from "next/image";
import type { HTMLAttributes, ReactNode } from "react"; import type { HTMLAttributes, ReactNode } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Button from "@/components/ui/button";
import Button from "./button";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
const EXIT_ANIMATION_MS = 220; const EXIT_ANIMATION_MS = 220;

0
src/components/ui/language-switcher.tsx → src/components/Componentes/language-switcher.tsx

0
src/components/dev/locator-paths.ts → src/components/Componentes/locator-paths.ts

2
src/components/ui/navigation-button.tsx → src/components/Componentes/navigation-button.tsx

@ -5,7 +5,7 @@ import { useRouter } from "next/navigation";
import type { ButtonHTMLAttributes, ReactNode } from "react"; import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useState } from "react"; import { useState } from "react";
import { GoArrowLeft } from "react-icons/go"; import { GoArrowLeft } from "react-icons/go";
import HelpModal from "@/components/ui/help-modal";
import HelpModal from "./help-modal";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
type NavigationButtonIcon = type NavigationButtonIcon =

0
src/components/ui/notice-box.tsx → src/components/Componentes/notice-box.tsx

0
src/components/utils/page-background.tsx → src/components/Componentes/page-background.tsx

454
src/components/Componentes/progress-helper.ts

@ -0,0 +1,454 @@
import {
isQuestionVisibleForProfile,
isQuestionRequiredForProfile,
type QuestionListItem,
} from "@/data/question-data";
import type {
MarriageFieldValue,
MarriageGender,
} from "@/hooks/marriage/types";
import { hasQuestionAnswerValue } from "./question-answer-storage";
export function getStoredAge(): number | null {
try {
if (typeof window === "undefined") return null;
const rawValue = window.localStorage.getItem(
"marriage:sections:personal_info:answers",
);
if (!rawValue) return null;
const storedAnswers = JSON.parse(rawValue);
const ageField = storedAnswers.fields?.find(
(f: Record<string, unknown>) =>
f.type === "number" ||
f.label === "Age" ||
f.label === "سن" ||
(typeof f.key === "string" &&
(f.key.endsWith("_age") || f.key.endsWith("_sn"))),
);
if (ageField && ageField.value !== undefined && ageField.value !== null) {
const num = Number(ageField.value);
if (Number.isFinite(num)) return num;
}
const dobField = storedAnswers.fields?.find(
(f: Record<string, unknown>) =>
f.type === "date" ||
f.label === "Date of Birth" ||
f.label === "تاریخ تولد" ||
(typeof f.key === "string" &&
(f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))),
);
if (dobField?.value) {
const dob = new Date(String(dobField.value));
if (!Number.isNaN(dob.getTime())) {
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) {
age--;
}
return age >= 0 ? age : null;
}
}
} catch (_e) {}
return null;
}
function isFieldAnswered(
q: Record<string, unknown>,
field: Record<string, unknown> | undefined,
): boolean {
if (!field || !hasQuestionAnswerValue(field.value as MarriageFieldValue)) {
return false;
}
if (q.type === "birthplace") {
const strVal = String(field.value);
const parts = strVal.split(",").map((p) => p.trim());
return parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0;
}
return true;
}
function hashString(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash.toString(36);
}
function slugifyTitle(title: string) {
const slug = title
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return slug || `field_${hashString(title)}`;
}
export function getLocalSectionProgress(
item: QuestionListItem,
profile: { gender?: MarriageGender | null } | null | undefined,
age: number | null,
): number | null {
try {
if (typeof window === "undefined") return null;
const fields: Record<string, unknown>[] = [];
let hasFoundStorage = false;
const mainKey = `marriage:sections:${item.slug}:answers`;
const mainRaw = window.localStorage.getItem(mainKey);
if (mainRaw) {
try {
const parsed = JSON.parse(mainRaw);
if (
parsed &&
Array.isArray(parsed.fields) &&
parsed.fields.length > 0
) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
if (item.slug === "family_marital_history") {
const fbRaw = window.localStorage.getItem(
"marriage:sections:family_background:answers",
);
if (fbRaw) {
try {
const parsed = JSON.parse(fbRaw);
if (parsed && Array.isArray(parsed.fields)) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
const mhRaw = window.localStorage.getItem(
"marriage:sections:marital_history_children:answers",
);
if (mhRaw) {
try {
const parsed = JSON.parse(mhRaw);
if (parsed && Array.isArray(parsed.fields)) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
}
if (!hasFoundStorage || fields.length === 0) {
return null;
}
const profileContext = {
age,
gender: profile?.gender,
};
const hasDobQuestion = item.questions.some(
(q) => q.title === "Date of Birth" || q.title === "تاریخ تولد",
);
const profileVisible = item.questions
.filter((question) => {
if (
hasDobQuestion &&
(question.title === "Age" || question.title === "سن")
) {
return false;
}
return isQuestionVisibleForProfile(question, profileContext);
})
.map((question) => ({
...question,
required: isQuestionRequiredForProfile(question, profileContext),
}));
const findAnswer = (
questionTitle: string,
questionIndex: number,
originalIndex?: number,
) => {
const indexToUse =
originalIndex !== undefined ? originalIndex : questionIndex;
const key = `q${indexToUse + 1}_${slugifyTitle(questionTitle)}`;
const field = fields.find(
(f) => f && (f.key === key || f.label === questionTitle),
);
return field?.value;
};
// Find employment status question by title or by choices length (10)
const employmentQuestionIndex = profileVisible.findIndex((q) => {
return (
q.title === "Employment Status" ||
q.title === "وضعیت اشتغال" ||
(q.type === "dropdown" && q.extras?.options?.length === 10)
);
});
let employmentSelectedOptionIndex = -1;
if (employmentQuestionIndex !== -1) {
const employmentQuestion = profileVisible[employmentQuestionIndex];
const ans = findAnswer(
employmentQuestion.title,
employmentQuestionIndex,
employmentQuestion.originalIndex,
);
if (ans) {
employmentSelectedOptionIndex =
employmentQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
}
}
// Find Parents' Marital Status question index and check selected index
const maritalStatusQuestionIndex = profileVisible.findIndex((q) => {
return (
q.title === "Parents' Marital Status" || q.title === "وضعیت تأهل والدین"
);
});
let isCircumstancesSelected = false;
if (maritalStatusQuestionIndex !== -1) {
const maritalQuestion = profileVisible[maritalStatusQuestionIndex];
const ans = findAnswer(
maritalQuestion.title,
maritalStatusQuestionIndex,
maritalQuestion.originalIndex,
);
if (ans) {
const selectedIdx =
maritalQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
isCircumstancesSelected = selectedIdx === 2;
}
}
// Find Current Marital Status question index and check selected index
const currentMaritalQuestionIndex = profileVisible.findIndex((q) => {
return (
q.title === "Current Marital Status" || q.title === "وضعیت تأهل فعلی"
);
});
let maritalSelectedIndex = -1;
if (currentMaritalQuestionIndex !== -1) {
const maritalQuestion = profileVisible[currentMaritalQuestionIndex];
const ans = findAnswer(
maritalQuestion.title,
currentMaritalQuestionIndex,
maritalQuestion.originalIndex,
);
if (ans) {
maritalSelectedIndex =
maritalQuestion.extras?.options?.indexOf(String(ans)) ?? -1;
}
}
// Find Children and Guardianship Status question index
const custodyQuestionIndex = profileVisible.findIndex((q) => {
return (
q.title === "Children and Guardianship Status" ||
q.title === "وضعیت فرزند و تکفل"
);
});
let hasChildrenSelected = false;
let hasAnyGuardianshipSelected = false;
if (custodyQuestionIndex !== -1) {
const custodyQuestion = profileVisible[custodyQuestionIndex];
const ans = findAnswer(
custodyQuestion.title,
custodyQuestionIndex,
custodyQuestion.originalIndex,
);
if (ans) {
const ansList = Array.isArray(ans) ? ans.map(String) : [String(ans)];
hasChildrenSelected = ansList.some(
(val) => val.includes("Have children") || val.includes("فرزند دارم"),
);
hasAnyGuardianshipSelected = ansList.some(
(val) =>
val.includes("Have children") ||
val.includes("فرزند دارم") ||
val.includes("under my guardianship") ||
val.includes("تحت تکفل"),
);
}
}
const filtered = profileVisible.filter((question, index) => {
// Check if this is one of the marital status/children questions
if (currentMaritalQuestionIndex !== -1) {
const isDuration =
index === currentMaritalQuestionIndex + 1 ||
question.title === "Previous Marriage Duration" ||
question.title === "مدت ازدواج یا عقد قبلی";
const isSeparation =
index === currentMaritalQuestionIndex + 2 ||
question.title === "Reason for Separation" ||
question.title === "علت جدایی، در صورت وجود";
const isCustody =
index === currentMaritalQuestionIndex + 3 ||
question.title === "Children and Guardianship Status" ||
question.title === "وضعیت فرزند و تکفل";
const isChildrenCount =
index === currentMaritalQuestionIndex + 4 ||
question.title === "Number of Children" ||
question.title === "تعداد فرزندان";
const isChildrenExplanation =
index === currentMaritalQuestionIndex + 5 ||
question.title === "Short Children/Guardianship Explanation" ||
question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل";
if (isDuration) {
return [1, 2, 3].includes(maritalSelectedIndex);
}
if (isSeparation) {
return [1, 2].includes(maritalSelectedIndex);
}
if (isCustody) {
return [2, 3].includes(maritalSelectedIndex);
}
if (isChildrenCount) {
return [2, 3].includes(maritalSelectedIndex) && hasChildrenSelected;
}
if (isChildrenExplanation) {
return (
[2, 3].includes(maritalSelectedIndex) && hasAnyGuardianshipSelected
);
}
}
// Check if this is one of the three job-related questions
if (employmentQuestionIndex !== -1) {
const isJobTitle =
index === employmentQuestionIndex + 1 ||
question.title === "Job Title" ||
question.title === "عنوان شغلی";
const isWorkLocation =
index === employmentQuestionIndex + 2 ||
question.title === "Work Location" ||
question.title === "محل فعالیت";
const isMonthlyIncome =
index === employmentQuestionIndex + 3 ||
question.title === "Monthly Income" ||
question.title === "میزان درآمد ماهانه";
if (isJobTitle || isWorkLocation || isMonthlyIncome) {
// If no employment status is selected yet, hide them by default
if (employmentSelectedOptionIndex === -1) {
return false;
}
// Options logic:
// Show all 3 for: index 0 (Full-time), 1 (Part-time), 2 (Self-employed), 3 (Entrepreneur), 5 (Working Student)
const showAll = [0, 1, 2, 3, 5].includes(
employmentSelectedOptionIndex,
);
// Hide all 3 for: index 4 (Student), 6 (Student & Job Seeking), 7 (Job Seeking / Unemployed), 8 (Homemaker)
const hideAll = [4, 6, 7, 8].includes(employmentSelectedOptionIndex);
// Special Retired logic: index 9 (Retired)
const isRetired = employmentSelectedOptionIndex === 9;
if (showAll) {
return true;
}
if (hideAll) {
return false;
}
if (isRetired) {
if (isJobTitle || isMonthlyIncome) {
return true;
}
if (isWorkLocation) {
return false;
}
}
return false;
}
}
// Default dependsOn logic
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const dependentQuestionIndex = profileVisible.findIndex(
(q) => q.title === title,
);
if (dependentQuestionIndex !== -1) {
const dependentQuestion = profileVisible[dependentQuestionIndex];
const answer = findAnswer(
dependentQuestion.title,
dependentQuestionIndex,
dependentQuestion.originalIndex,
);
return values.includes(String(answer));
}
return false;
}
return true;
});
const activeQuestions = filtered.map((question) => {
if (
question.title === "Short Family Description" ||
question.title === "توضیح کوتاه درباره خانواده"
) {
return {
...question,
required: isCircumstancesSelected,
};
}
if (
question.title === "Previous Marriage Duration" ||
question.title === "مدت ازدواج یا عقد قبلی" ||
question.title === "Number of Children" ||
question.title === "تعداد فرزندان" ||
question.title === "Short Children/Guardianship Explanation" ||
question.title === "توضیح کوتاه درباره شرایط فرزند یا تکفل"
) {
return {
...question,
required: true,
};
}
return question;
});
const requiredQuestions = activeQuestions.filter((q) => q.required);
if (requiredQuestions.length === 0) {
return 100;
}
const answeredCount = requiredQuestions.filter((q) => {
const idx = profileVisible.indexOf(q);
const indexToUse = q.originalIndex !== undefined ? q.originalIndex : idx;
const key = `q${indexToUse + 1}_${slugifyTitle(q.title)}`;
const field = fields.find(
(f) => f && (f.key === key || f.label === q.title),
);
return isFieldAnswered(q, field);
}).length;
return Math.max(
0,
Math.min(100, Math.round((answeredCount / requiredQuestions.length) * 100)),
);
} catch (_e) {
return null;
}
}

0
src/components/questions/question-answer-storage.tsx → src/components/Componentes/question-answer-storage.tsx

0
src/components/questions/question-birthplace.tsx → src/components/Componentes/question-birthplace.tsx

0
src/components/questions/question-button.tsx → src/components/Componentes/question-button.tsx

0
src/components/questions/question-card.tsx → src/components/Componentes/question-card.tsx

32
src/components/questions/question-checkbox.tsx → src/components/Componentes/question-checkbox.tsx

@ -87,9 +87,9 @@ export function QuestionCheckbox({
key={option} key={option}
htmlFor={optionId} htmlFor={optionId}
className={[ className={[
"cursor-pointer rounded-[16px] font-semibold text-[15px] transition-all duration-200 active:scale-[0.99]",
"cursor-pointer rounded-[16px] font-semibold text-[15px] transition-all duration-200 active:scale-[0.99] flex items-center gap-3",
isShortOptions isShortOptions
? "flex-1 min-w-[90px] px-5 py-3.5 text-center"
? "flex-1 min-w-[90px] px-5 py-3.5 text-center justify-center"
: "w-full px-5 py-4 text-start leading-snug", : "w-full px-5 py-4 text-start leading-snug",
isSelected isSelected
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" ? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]"
@ -104,7 +104,33 @@ export function QuestionCheckbox({
onChange={() => toggleOption(option)} onChange={() => toggleOption(option)}
className="sr-only" className="sr-only"
/> />
{option}
{!isShortOptions && (
<div
className={[
"size-[18px] shrink-0 rounded-[5px] border-[2px] transition-all flex items-center justify-center",
isSelected
? "border-white bg-white text-[#F0445B]"
: "border-[#D0D5DD] bg-white text-transparent",
].join(" ")}
>
{isSelected && (
<svg
width="10"
height="8"
viewBox="0 0 10 8"
fill="none"
className="stroke-current stroke-[2]"
>
<path
d="M1 4L3.5 6.5L9 1"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
)}
<span className="flex-1">{option}</span>
</label> </label>
); );
})} })}

0
src/components/questions/question-date.tsx → src/components/Componentes/question-date.tsx

162
src/components/questions/question-dropdown.tsx → src/components/Componentes/question-dropdown.tsx

@ -1,6 +1,7 @@
"use client"; "use client";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { ExplanationUiFont } from "./explanation-ui-font";
import { getCountryList } from "@/data/countries"; import { getCountryList } from "@/data/countries";
import { getLanguageList } from "@/data/languages"; import { getLanguageList } from "@/data/languages";
import { getNationalityList } from "@/data/nationalities"; import { getNationalityList } from "@/data/nationalities";
@ -20,11 +21,14 @@ export function QuestionDropdown({
questionIndex, questionIndex,
disabled, disabled,
}: QuestionDropdownProps) { }: QuestionDropdownProps) {
const { locale } = useI18n();
const { locale, dictionary: t } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const rawValue = getAnswerValue(question, questionIndex); const rawValue = getAnswerValue(question, questionIndex);
const isMulti = Array.isArray(rawValue) || question.type === "checkbox";
const isMulti =
Array.isArray(rawValue) ||
question.type === "checkbox" ||
(question.extras?.range && question.extras.range[1] > 1);
const selectedList = Array.isArray(rawValue) const selectedList = Array.isArray(rawValue)
? rawValue ? rawValue
: typeof rawValue === "string" && rawValue : typeof rawValue === "string" && rawValue
@ -96,6 +100,23 @@ export function QuestionDropdown({
} }
}, [isOpen]); }, [isOpen]);
const isPersonalityTraits =
question.title === "Your Personality Traits" ||
question.title === "ویژگی‌های شخصیتی خودتان";
const isHobbies =
question.title === "Your Hobbies and Main Interests" ||
question.title === "سرگرمی‌ها و علایق اصلی";
const isSmokingSubstances =
question.title === "Red Lines for Smoking, Alcohol, and Substances" ||
question.title === "خط قرمزهای مربوط به دخانیات، الکل و مواد در همسر آینده";
const isOtherQuestion =
isPersonalityTraits || isHobbies || isSmokingSubstances;
const otherOptionName = t.common.other || "Other";
let options = question.extras.options || []; let options = question.extras.options || [];
const isNationalityDropdown = const isNationalityDropdown =
question.title === "Current Nationality / Citizenship" || question.title === "Current Nationality / Citizenship" ||
@ -124,27 +145,91 @@ export function QuestionDropdown({
options = getLanguageList(locale); options = getLanguageList(locale);
} }
if (isOtherQuestion) {
options = [...options, otherOptionName];
}
const showSearch = (() => {
if (question.extras.noSearch) return false;
const titleLower = question.title.toLowerCase();
return (
isCountryDropdown ||
isNationalityDropdown ||
isLanguageDropdown ||
titleLower.includes("country") ||
titleLower.includes("کشور") ||
titleLower.includes("city") ||
titleLower.includes("شهر") ||
titleLower.includes("currency") ||
titleLower.includes("ارز") ||
titleLower.includes("nationality") ||
titleLower.includes("ملیت") ||
titleLower.includes("language") ||
titleLower.includes("زبان") ||
options.length > 10
);
})();
const filteredOptions = options.filter((option) => const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(searchQuery.toLowerCase()), option.toLowerCase().includes(searchQuery.toLowerCase()),
); );
const getCleanLabel = (val: string) => {
const base = val.split(" - ")[0];
if (isOtherQuestion) {
if (base === otherOptionName || base.startsWith(`${otherOptionName}: `)) {
return otherOptionName;
}
}
return base;
};
const displayLabel = isMulti const displayLabel = isMulti
? selectedList.length > 0 ? selectedList.length > 0
? selectedList.join(", ")
? selectedList.map(getCleanLabel).join(", ")
: question.extras.placeHolder || "Select" : question.extras.placeHolder || "Select"
: singleValue || question.extras.placeHolder || "Select";
: singleValue
? getCleanLabel(singleValue)
: question.extras.placeHolder || "Select";
const hasSelectedValue = isMulti const hasSelectedValue = isMulti
? selectedList.length > 0 ? selectedList.length > 0
: Boolean(singleValue); : Boolean(singleValue);
const otherValueIndex = selectedList.findIndex(
(v) => v === otherOptionName || v.startsWith(`${otherOptionName}: `),
);
const isOtherSelected = otherValueIndex !== -1;
const currentOtherText = isOtherSelected
? selectedList[otherValueIndex].startsWith(`${otherOptionName}: `)
? selectedList[otherValueIndex].slice(otherOptionName.length + 2)
: ""
: "";
const toggleMultiOption = (option: string) => { const toggleMultiOption = (option: string) => {
const isOtherOpt = isOtherQuestion && option === otherOptionName;
let nextValue: string[]; let nextValue: string[];
if (isOtherQuestion && isOtherOpt) {
const hasOther = selectedList.some(
(v) => v === otherOptionName || v.startsWith(`${otherOptionName}: `),
);
if (hasOther) {
nextValue = selectedList.filter(
(v) =>
v !== otherOptionName && !v.startsWith(`${otherOptionName}: `),
);
} else {
nextValue = [...selectedList, otherOptionName];
}
} else {
if (selectedList.includes(option)) { if (selectedList.includes(option)) {
nextValue = selectedList.filter((v) => v !== option); nextValue = selectedList.filter((v) => v !== option);
} else { } else {
nextValue = [...selectedList, option]; nextValue = [...selectedList, option];
} }
}
setAnswerValue( setAnswerValue(
question, question,
questionIndex, questionIndex,
@ -152,6 +237,18 @@ export function QuestionDropdown({
); );
}; };
const handleOtherTextChange = (text: string) => {
if (otherValueIndex !== -1) {
const nextValue = [...selectedList];
if (text.trim() === "") {
nextValue[otherValueIndex] = otherOptionName;
} else {
nextValue[otherValueIndex] = `${otherOptionName}: ${text}`;
}
setAnswerValue(question, questionIndex, nextValue);
}
};
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@ -183,6 +280,7 @@ export function QuestionDropdown({
{displayLabel} {displayLabel}
</span> </span>
<svg <svg
aria-hidden="true"
width="16" width="16"
height="10" height="10"
viewBox="0 0 16 10" viewBox="0 0 16 10"
@ -202,13 +300,33 @@ export function QuestionDropdown({
</svg> </svg>
</button> </button>
{/* RENDER TEXT FIELD IF OTHER IS SELECTED */}
{isOtherQuestion && isOtherSelected ? (
<div className="mt-1">
<input
type="text"
value={currentOtherText}
onChange={(e) => handleOtherTextChange(e.target.value)}
placeholder={
isPersonalityTraits
? (t.questions.writeOtherTraits ?? "Write other options...")
: isSmokingSubstances
? (locale === "fa" ? "خط قرمزهای دیگر را بنویسید..." : "Write other red lines...")
: (locale === "fa" ? "سرگرمی‌ها یا علایق دیگر را بنویسید..." : "Write other hobbies or interests...")
}
className="h-[48px] w-full rounded-[14px] border border-[#D0D5DD] bg-white px-4 text-[14px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F]"
/>
</div>
) : null}
{/* Dropdown Options Panel */} {/* Dropdown Options Panel */}
{isOpen && ( {isOpen && (
<div className="absolute top-[calc(100%+8px)] left-0 z-50 flex w-full flex-col gap-3.5 rounded-[20px] bg-white p-4.5 shadow-[0_12px_36px_rgba(0,0,0,0.09)] border border-[#EAECF0] animate-in fade-in zoom-in-95 duration-150"> <div className="absolute top-[calc(100%+8px)] left-0 z-50 flex w-full flex-col gap-3.5 rounded-[20px] bg-white p-4.5 shadow-[0_12px_36px_rgba(0,0,0,0.09)] border border-[#EAECF0] animate-in fade-in zoom-in-95 duration-150">
{/* Search Input Bar (shown if options > 3 or search is active) */}
{options.length > 3 || searchQuery ? (
{/* Search Input Bar */}
{showSearch ? (
<div className="flex h-[46px] w-full items-center gap-2.5 rounded-[12px] bg-[#EFEFEF] px-3.5 transition-colors focus-within:bg-[#E8E8E8]"> <div className="flex h-[46px] w-full items-center gap-2.5 rounded-[12px] bg-[#EFEFEF] px-3.5 transition-colors focus-within:bg-[#E8E8E8]">
<svg <svg
aria-hidden="true"
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
@ -262,7 +380,9 @@ export function QuestionDropdown({
{filteredOptions.length > 0 ? ( {filteredOptions.length > 0 ? (
filteredOptions.map((option) => { filteredOptions.map((option) => {
const isSelected = isMulti const isSelected = isMulti
? selectedList.includes(option)
? isOtherQuestion && option === otherOptionName
? isOtherSelected
: selectedList.includes(option)
: singleValue === option; : singleValue === option;
return ( return (
@ -277,14 +397,14 @@ export function QuestionDropdown({
setIsOpen(false); setIsOpen(false);
} }
}} }}
className="flex w-full items-center gap-3 text-start cursor-pointer group/opt py-0.5"
className="flex w-full items-start gap-3 text-start cursor-pointer group/opt py-0.5"
> >
{/* Option Indicator Icon */} {/* Option Indicator Icon */}
{isMulti ? ( {isMulti ? (
/* Multi-select / Checkbox style */ /* Multi-select / Checkbox style */
<div <div
className={[ className={[
"size-[20px] shrink-0 rounded-[6px] transition-all duration-150",
"size-[20px] shrink-0 rounded-[6px] transition-all duration-150 mt-0.5",
isSelected isSelected
? "bg-[#F2465F] shadow-xs" ? "bg-[#F2465F] shadow-xs"
: "border-[2px] border-[#344054] bg-transparent group-hover/opt:border-[#181818]", : "border-[2px] border-[#344054] bg-transparent group-hover/opt:border-[#181818]",
@ -294,7 +414,7 @@ export function QuestionDropdown({
/* Single-select / Radio style */ /* Single-select / Radio style */
<div <div
className={[ className={[
"size-[20px] shrink-0 rounded-full transition-all duration-150",
"size-[20px] shrink-0 rounded-full transition-all duration-150 mt-0.5",
isSelected isSelected
? "bg-[#F2465F] shadow-xs" ? "bg-[#F2465F] shadow-xs"
: "border-[2px] border-[#344054] bg-transparent group-hover/opt:border-[#181818]", : "border-[2px] border-[#344054] bg-transparent group-hover/opt:border-[#181818]",
@ -302,8 +422,26 @@ export function QuestionDropdown({
/> />
)} )}
<span className="text-[15px] font-bold text-[#181818]">
{option}
<span className="text-[15px] text-[#181818] leading-tight flex-1">
{option.includes(" - ") ? (
(() => {
const parts = option.split(" - ");
const title = parts[0];
const description = parts.slice(1).join(" - ");
return (
<span className="flex flex-col gap-1 text-start">
<span className="font-bold text-[#181818]">
{title}
</span>
<ExplanationUiFont>
{description}
</ExplanationUiFont>
</span>
);
})()
) : (
<span className="font-bold">{option}</span>
)}
</span> </span>
</button> </button>
); );

2
src/components/questions/question-exit-navigation-button.tsx → src/components/Componentes/question-exit-navigation-button.tsx

@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import NavigationButton, { import NavigationButton, {
type NavigationButtonProps, type NavigationButtonProps,
} from "@/components/ui/navigation-button";
} from "./navigation-button";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage"; import { useQuestionAnswers } from "./question-answer-storage";

0
src/components/questions/question-file.tsx → src/components/Componentes/question-file.tsx

0
src/components/questions/question-number.tsx → src/components/Componentes/question-number.tsx

0
src/components/questions/question-phone.tsx → src/components/Componentes/question-phone.tsx

2
src/components/questions/question-photo.tsx → src/components/Componentes/question-photo.tsx

@ -170,7 +170,7 @@ export function QuestionPhoto({
].join(" ")} ].join(" ")}
> >
<QuestionTitle <QuestionTitle
question={question}
question={{ ...question, description: "" }}
className="justify-center text-center" className="justify-center text-center"
/> />

0
src/components/questions/question-progress-tracker.tsx → src/components/Componentes/question-progress-tracker.tsx

39
src/components/questions/question-radio.tsx → src/components/Componentes/question-radio.tsx

@ -1,5 +1,6 @@
"use client"; "use client";
import { ExplanationUiFont } from "./explanation-ui-font";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import { useQuestionAnswers } from "./question-answer-storage"; import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title"; import QuestionTitle from "./question-title";
@ -52,9 +53,9 @@ export function QuestionRadio({
key={option} key={option}
htmlFor={optionId} htmlFor={optionId}
className={[ className={[
"cursor-pointer rounded-[16px] font-semibold text-[15px] transition-all duration-200 active:scale-[0.99]",
"cursor-pointer rounded-[16px] font-semibold text-[15px] transition-all duration-200 active:scale-[0.99] flex items-center gap-3",
isShortOptions isShortOptions
? "flex-1 min-w-[90px] px-5 py-3.5 text-center"
? "flex-1 min-w-[90px] px-5 py-3.5 text-center justify-center"
: "w-full px-5 py-4 text-start leading-snug", : "w-full px-5 py-4 text-start leading-snug",
isSelected isSelected
? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" ? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]"
@ -71,7 +72,39 @@ export function QuestionRadio({
onChange={() => setAnswerValue(question, questionIndex, option)} onChange={() => setAnswerValue(question, questionIndex, option)}
className="sr-only" className="sr-only"
/> />
{option}
{!isShortOptions && (
<div
className={[
"size-[18px] shrink-0 rounded-full border-[2px] transition-all flex items-center justify-center",
isSelected
? "border-white bg-white text-[#F0445B]"
: "border-[#D0D5DD] bg-white text-transparent",
].join(" ")}
>
{isSelected && (
<div className="size-[8px] rounded-full bg-current" />
)}
</div>
)}
{option.includes(" - ")
? (() => {
const parts = option.split(" - ");
const title = parts[0];
const description = parts.slice(1).join(" - ");
return (
<span className="flex flex-col gap-1 text-start flex-1">
<span className="font-bold">{title}</span>
<ExplanationUiFont
textColor={
isSelected ? "text-white/85" : "text-[#667085]"
}
>
{description}
</ExplanationUiFont>
</span>
);
})()
: <span className="flex-1">{option}</span>}
</label> </label>
); );
})} })}

33
src/components/questions/question-renderer.tsx → src/components/Componentes/question-renderer.tsx

@ -1,18 +1,19 @@
"use client"; "use client";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import QuestionBirthplace from "@/components/questions/question-birthplace";
import QuestionButton from "@/components/questions/question-button";
import QuestionCheckbox from "@/components/questions/question-checkbox";
import QuestionDate from "@/components/questions/question-date";
import QuestionDropdown from "@/components/questions/question-dropdown";
import QuestionFile from "@/components/questions/question-file";
import QuestionNumber from "@/components/questions/question-number";
import QuestionPhone from "@/components/questions/question-phone";
import QuestionPhoto from "@/components/questions/question-photo";
import QuestionRadio from "@/components/questions/question-radio";
import QuestionSlider from "@/components/questions/question-slider";
import QuestionText from "@/components/questions/question-text";
import QuestionBirthplace from "./question-birthplace";
import QuestionButton from "./question-button";
import QuestionCheckbox from "./question-checkbox";
import QuestionDate from "./question-date";
import QuestionDropdown from "./question-dropdown";
import QuestionFile from "./question-file";
import QuestionNumber from "./question-number";
import QuestionPhone from "./question-phone";
import QuestionPhoto from "./question-photo";
import QuestionRadio from "./question-radio";
import QuestionSlider from "./question-slider";
import QuestionText from "./question-text";
import QuestionTextarea from "./question-textarea";
type QuestionRendererProps = { type QuestionRendererProps = {
question: QuestionField; question: QuestionField;
@ -152,6 +153,14 @@ export function QuestionRenderer({
disabled={disabled} disabled={disabled}
/> />
); );
case "textarea":
return (
<QuestionTextarea
question={question}
questionIndex={questionIndex}
disabled={disabled}
/>
);
default: default:
return null; return null;
} }

4
src/components/questions/question-section-flow.tsx → src/components/Componentes/question-section-flow.tsx

@ -4,7 +4,7 @@ import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import Button from "@/components/ui/button";
import Button from "./button";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage"; import { useQuestionAnswers } from "./question-answer-storage";
@ -13,7 +13,7 @@ import QuestionProgressTracker, {
} from "./question-progress-tracker"; } from "./question-progress-tracker";
import QuestionSnapList from "./question-snap-list"; import QuestionSnapList from "./question-snap-list";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import NoticeBox from "@/components/ui/notice-box";
import NoticeBox from "./notice-box";
import { getStoredAge } from "./progress-helper"; import { getStoredAge } from "./progress-helper";
type QuestionSectionFlowProps = { type QuestionSectionFlowProps = {

287
src/components/Componentes/question-slider.tsx

@ -0,0 +1,287 @@
"use client";
import { useLayoutEffect, useRef, useState } from "react";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
type QuestionSliderProps = {
question: QuestionField;
questionIndex: number;
disabled?: boolean;
};
export function QuestionSlider({
question,
questionIndex,
disabled,
}: QuestionSliderProps) {
const { dictionary: t } = useI18n();
const [min, max] = question.extras.range;
const initialValue = Math.round((min + max) / 2);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question, questionIndex);
const isDesiredAgeRange =
question.title === "Desired Age Range of Future Spouse" ||
question.title === "بازه سنی مطلوب همسر آینده";
const thumbWidth = 18;
const bubbleHalfWidth = 18;
const steps = (() => {
const range = max - min;
const interval = range <= 15 ? 1 : range <= 50 ? 5 : range <= 150 ? 10 : 20;
const arr: number[] = [];
for (let val = min; val <= max; val += interval) {
arr.push(val);
}
if (arr[arr.length - 1] !== max) {
const lastVal = arr[arr.length - 1];
if (max - lastVal < interval * 0.5) {
arr.pop();
}
arr.push(max);
}
return arr;
})();
// Multi-state logic for Age Range Slider
let fromVal = 25;
let toVal = 30;
if (isDesiredAgeRange) {
if (typeof storedValue === "string" && storedValue.includes("-")) {
const parts = storedValue.split("-").map(Number);
if (
parts.length === 2 &&
!Number.isNaN(parts[0]) &&
!Number.isNaN(parts[1])
) {
fromVal = parts[0];
toVal = parts[1];
}
} else if (typeof storedValue === "number") {
fromVal = storedValue;
toVal = Math.max(storedValue, max);
}
fromVal = Math.max(min, Math.min(max, fromVal));
toVal = Math.max(min, Math.min(max, toVal));
if (toVal < fromVal) {
toVal = fromVal;
}
}
// Single-slider states (only used if isDesiredAgeRange is false)
const value = typeof storedValue === "number" ? storedValue : initialValue;
const progress = max === min ? 0 : ((value - min) / (max - min)) * 100;
const sliderWrapperRef = useRef<HTMLDivElement>(null);
const sliderRef = useRef<HTMLInputElement>(null);
const [bubblePosition, setBubblePosition] = useState(bubbleHalfWidth);
useLayoutEffect(() => {
if (isDesiredAgeRange) return;
const wrapper = sliderWrapperRef.current;
const slider = sliderRef.current;
if (!wrapper || !slider) {
return;
}
const updateBubblePosition = () => {
const wrapperRect = wrapper.getBoundingClientRect();
const sliderRect = slider.getBoundingClientRect();
const sliderLeft = sliderRect.left - wrapperRect.left;
const thumbCenter =
sliderLeft +
thumbWidth / 2 +
(progress / 100) * (sliderRect.width - thumbWidth);
setBubblePosition(
Math.min(
Math.max(thumbCenter, bubbleHalfWidth),
wrapperRect.width - bubbleHalfWidth,
),
);
};
updateBubblePosition();
const resizeObserver = new ResizeObserver(updateBubblePosition);
resizeObserver.observe(wrapper);
resizeObserver.observe(slider);
return () => resizeObserver.disconnect();
}, [progress, isDesiredAgeRange]);
if (isDesiredAgeRange) {
const handleFromChange = (newFrom: number) => {
const val = Math.min(newFrom, toVal);
setAnswerValue(question, questionIndex, `${val}-${toVal}`);
};
const handleToChange = (newTo: number) => {
const val = Math.max(newTo, fromVal);
setAnswerValue(question, questionIndex, `${fromVal}-${val}`);
};
const progressFrom =
max === min ? 0 : ((fromVal - min) / (max - min)) * 100;
const progressTo = max === min ? 0 : ((toVal - min) / (max - min)) * 100;
return (
<div
className={[
"flex w-full flex-col gap-6 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
{/* First Slider (From Age) */}
<div className="flex flex-col gap-2 pt-2">
<div className="flex justify-between items-center text-[14px] font-bold text-[#181818]">
<span>{t.questions.fromAge ?? "From"}</span>
<span className="rounded-[8px] bg-[#FCE7EA] text-[#F2465F] px-2.5 py-1 text-[13px] font-bold">
{fromVal}
</span>
</div>
<div className="relative">
<input
type="range"
min={min}
max={max}
step={1}
value={fromVal}
onChange={(e) => handleFromChange(Number(e.target.value))}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progressFrom}%, #FCE7EA ${progressFrom}%, #FCE7EA 100%)`,
}}
/>
</div>
<div className="relative h-4 text-[#B7B7B7] text-[10px] font-medium w-full">
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={`from-${step}`}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
{/* Second Slider (To Age) */}
<div className="flex flex-col gap-2 pt-2">
<div className="flex justify-between items-center text-[14px] font-bold text-[#181818]">
<span>{t.questions.toAge ?? "To"}</span>
<span className="rounded-[8px] bg-[#FCE7EA] text-[#F2465F] px-2.5 py-1 text-[13px] font-bold">
{toVal}
</span>
</div>
<div className="relative">
<input
type="range"
min={min}
max={max}
step={1}
value={toVal}
onChange={(e) => handleToChange(Number(e.target.value))}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progressTo}%, #FCE7EA ${progressTo}%, #FCE7EA 100%)`,
}}
/>
</div>
<div className="relative h-4 text-[#B7B7B7] text-[10px] font-medium w-full">
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={`to-${step}`}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
</div>
);
}
return (
<div
className={[
"flex w-full flex-col gap-3.5 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<div className="pt-7">
<div ref={sliderWrapperRef} className="relative">
<div
className="-translate-x-1/2 absolute -top-7.5 z-10 flex items-center justify-center rounded-[8px] bg-[#F2465F] px-3 py-1 text-[14px] font-bold text-white shadow-md after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F2465F] after:content-['']"
style={{ left: `${bubblePosition}px` }}
>
{value}
</div>
<input
ref={sliderRef}
type="range"
min={min}
max={max}
step={1}
value={value}
onChange={(event) =>
setAnswerValue(
question,
questionIndex,
Number(event.target.value),
)
}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progress}%, #FCE7EA ${progress}%, #FCE7EA 100%)`,
}}
/>
</div>
<div
className="relative mt-2 h-5 text-[#B7B7B7] text-[10px] font-medium"
style={{ width: "100%" }}
>
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={step}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
</div>
);
}
export default QuestionSlider;

0
src/components/questions/question-snap-list.tsx → src/components/Componentes/question-snap-list.tsx

49
src/components/questions/question-text.tsx → src/components/Componentes/question-text.tsx

@ -1,6 +1,7 @@
"use client"; "use client";
import type { QuestionField } from "@/data/question-data"; import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage"; import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title"; import QuestionTitle from "./question-title";
@ -17,11 +18,13 @@ export default function QuestionText({
questionIndex, questionIndex,
description, description,
disabled, disabled,
heightClassName,
heightClassName: _heightClassName,
}: QuestionTextProps) { }: QuestionTextProps) {
const { dictionary: t } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex); const value = getAnswerValue(question, questionIndex);
const isMuted = value === "-";
const stringValue = String(value ?? "").trim(); const stringValue = String(value ?? "").trim();
const isEmailQuestion = const isEmailQuestion =
question.title.toLowerCase().includes("email") || question.title.toLowerCase().includes("email") ||
@ -36,6 +39,10 @@ export default function QuestionText({
? isValidEmail || stringValue.length === 0 ? isValidEmail || stringValue.length === 0
: isValidEmail && stringValue.length > 0; : isValidEmail && stringValue.length > 0;
const isMarjaQuestion =
question.title === "Marja' al-Taqlid (Religious Authority)" ||
question.title === "مرجع تقلید";
if (isEmailQuestion) { if (isEmailQuestion) {
return ( return (
<div <div
@ -58,13 +65,13 @@ export default function QuestionText({
> >
<input <input
type="text" type="text"
value={String(value ?? "")}
value={isMuted ? "" : String(value ?? "")}
onChange={(e) => onChange={(e) =>
setAnswerValue(question, questionIndex, e.target.value) setAnswerValue(question, questionIndex, e.target.value)
} }
placeholder={question.extras.placeHolder} placeholder={question.extras.placeHolder}
disabled={disabled}
className="h-full w-full border-0 bg-transparent px-4.5 text-[15px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
disabled={disabled || isMuted}
className="h-full w-full border-0 bg-transparent px-4.5 text-[15px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3] disabled:opacity-50 disabled:cursor-not-allowed"
/> />
</div> </div>
{showInvalidState ? ( {showInvalidState ? (
@ -95,14 +102,42 @@ export default function QuestionText({
<QuestionTitle question={question} /> <QuestionTitle question={question} />
<input <input
type="text" type="text"
value={String(value ?? "")}
value={isMuted ? "" : String(value ?? "")}
onChange={(e) => onChange={(e) =>
setAnswerValue(question, questionIndex, e.target.value) setAnswerValue(question, questionIndex, e.target.value)
} }
placeholder={question.extras.placeHolder} placeholder={question.extras.placeHolder}
disabled={disabled}
className="h-[54px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]"
disabled={disabled || isMuted}
className={[
"h-[54px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472]",
isMuted
? "opacity-50 cursor-not-allowed bg-[#F5F5F5] border-[#E2E8F0]"
: "",
]
.filter(Boolean)
.join(" ")}
/>
{isMarjaQuestion ? (
<div className="mt-2.5 flex items-center">
<label className="flex cursor-pointer items-center gap-2 select-none active:scale-[0.99] transition-all">
<input
type="checkbox"
checked={isMuted}
onChange={(e) => {
if (e.target.checked) {
setAnswerValue(question, questionIndex, "-");
} else {
setAnswerValue(question, questionIndex, "");
}
}}
className="h-[18px] w-[18px] shrink-0 rounded border-[#D0D5DD] text-[#F2465F] focus:ring-[#F2465F] accent-[#F2465F] cursor-pointer"
/> />
<span className="text-[13.5px] font-semibold text-[#4F4F4F]">
{t.questions.notAPriority ?? "This is not a priority for me"}
</span>
</label>
</div>
) : null}
{description ? ( {description ? (
<span className="block group-10 font-semibold text-[#747474]"> <span className="block group-10 font-semibold text-[#747474]">
{description} {description}

58
src/components/Componentes/question-textarea.tsx

@ -0,0 +1,58 @@
"use client";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
type QuestionTextareaProps = {
question: QuestionField;
questionIndex: number;
description?: string;
disabled?: boolean;
};
export function QuestionTextarea({
question,
questionIndex,
description,
disabled,
}: QuestionTextareaProps) {
const { dictionary: t } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question, questionIndex);
const stringValue = String(value ?? "").trim();
const isAnswered =
question.required === false ? true : stringValue.length > 0;
return (
<div
data-question-answered={isAnswered ? "true" : "false"}
data-question-type="textarea"
className={[
"flex w-full flex-col gap-2 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<textarea
value={String(value ?? "")}
onChange={(e) =>
setAnswerValue(question, questionIndex, e.target.value)
}
placeholder={question.extras.placeHolder}
disabled={disabled}
rows={5}
className="h-[270px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 py-4 text-[15px] font-medium text-[#181818] outline-none transition-all placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] disabled:bg-[#F5F2F1] disabled:text-[#7C7472] resize-none"
/>
{description ? (
<span className="block group-10 font-semibold text-[#747474]">
{description}
</span>
) : null}
</div>
);
}
export default QuestionTextarea;

96
src/components/Componentes/question-title.tsx

@ -0,0 +1,96 @@
"use client";
import { useState } from "react";
import { IoEyeOff } from "react-icons/io5";
import HelpModal from "./help-modal";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
type QuestionTitleProps = {
question: QuestionField;
className?: string;
};
export function QuestionTitle({ question, className }: QuestionTitleProps) {
const { dictionary: t, locale } = useI18n();
const [isHelpOpen, setIsHelpOpen] = useState(false);
const isMonthlyIncome =
question.title === "Monthly Income" ||
question.title === "میزان درآمد ماهانه";
const titleText =
question.title +
(isMonthlyIncome
? locale === "fa"
? " (تقریبی)"
: " (approximately)"
: "");
const words = titleText.split(" ");
const lastWord = words[words.length - 1];
const remainingTitle = words.slice(0, -1).join(" ");
return (
<span className="block w-full">
{question.private ? (
<span className="mb-4 mx-auto flex w-fit items-center gap-1.5 rounded-full bg-[#F8D7DA] px-3 py-1 text-center text-[10px] font-bold text-[#D9383A]">
<IoEyeOff
aria-hidden="true"
className="shrink-0 text-[15px] text-[#D9383A]"
/>
<span>
{t.questions.privateFieldNotice ?? "Private (Advisors Only)"}
</span>
</span>
) : null}
<span
className={[
"block text-[15px] font-bold text-[#181818] leading-snug",
className,
]
.filter(Boolean)
.join(" ")}
>
<span>
{remainingTitle}
{remainingTitle ? " " : ""}
<span className="inline-flex items-center gap-1.5 whitespace-nowrap align-baseline">
<span>{lastWord}</span>
{question.required ? (
<span className="text-[#F2465F] font-bold">*</span>
) : null}
{question.tooltip ? (
<>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsHelpOpen(true);
}}
className="inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[#F2465F] text-[12px] font-serif italic text-white shadow-xs cursor-pointer hover:scale-110 active:scale-95 transition-transform"
aria-label="Help"
>
i
</button>
<HelpModal
isOpen={isHelpOpen}
onClose={() => setIsHelpOpen(false)}
description={question.tooltip}
/>
</>
) : null}
</span>
</span>
{question.description ? (
<span className="text-[12.5px] font-normal text-[#667085] mt-1.5 block leading-normal">
{question.description}
</span>
) : null}
</span>
</span>
);
}
export default QuestionTitle;

2
src/components/ui/report-actions-sheet.tsx → src/components/Componentes/report-actions-sheet.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Button from "@/components/ui/button";
import Button from "./button";
import { useFlutterBridge } from "@/hooks/useFlutterBridge"; import { useFlutterBridge } from "@/hooks/useFlutterBridge";
import { import {
copyToClipboard, copyToClipboard,

2
src/components/questions/required-steps-card.tsx → src/components/Componentes/required-steps-card.tsx

@ -5,7 +5,7 @@ import { IoAlert, IoCheckmark } from "react-icons/io5";
import { import {
getLocalSectionProgress, getLocalSectionProgress,
getStoredAge, getStoredAge,
} from "@/components/questions/progress-helper";
} from "./progress-helper";
import { import {
getQuestionListItems, getQuestionListItems,
isQuestionListItemVisibleForProfile, isQuestionListItemVisibleForProfile,

16
src/components/sliders/slider-page.tsx → src/components/Componentes/slider-page.tsx

@ -6,17 +6,17 @@ import { useEffect, useState } from "react";
import type { import type {
GenderAnswer, GenderAnswer,
RegistrationAnswer, RegistrationAnswer,
} from "@/components/sliders/slider-slide";
import { SliderSlideFive } from "@/components/sliders/slider-slide-five";
import { SliderSlideFour } from "@/components/sliders/slider-slide-four";
} from "./slider-slide";
import { SliderSlideFive } from "./slider-slide-five";
import { SliderSlideFour } from "./slider-slide-four";
import { import {
SliderSlideOne, SliderSlideOne,
SliderSlideOneActions, SliderSlideOneActions,
} from "@/components/sliders/slider-slide-one";
import { SliderSlideThree } from "@/components/sliders/slider-slide-three";
import { SliderSlideTwo } from "@/components/sliders/slider-slide-two";
import Button from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
} from "./slider-slide-one";
import { SliderSlideThree } from "./slider-slide-three";
import { SliderSlideTwo } from "./slider-slide-two";
import Button from "./button";
import NavigationButton from "./navigation-button";
import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic"; import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";

2
src/components/sliders/slider-slide-five.tsx → src/components/Componentes/slider-slide-five.tsx

@ -1,4 +1,4 @@
import type { SliderSlideProps } from "@/components/sliders/slider-slide";
import type { SliderSlideProps } from "./slider-slide";
const NOTICE_ITEMS = [ const NOTICE_ITEMS = [
"Your information is kept strictly confidential.", "Your information is kept strictly confidential.",

4
src/components/sliders/slider-slide-four.tsx → src/components/Componentes/slider-slide-four.tsx

@ -3,8 +3,8 @@
import type { import type {
RegistrationAnswer, RegistrationAnswer,
SliderSlideProps, SliderSlideProps,
} from "@/components/sliders/slider-slide";
import NoticeBox from "@/components/ui/notice-box";
} from "./slider-slide";
import NoticeBox from "./notice-box";
type SliderSlideFourProps = SliderSlideProps & { type SliderSlideFourProps = SliderSlideProps & {
selectedRegistration: RegistrationAnswer; selectedRegistration: RegistrationAnswer;

4
src/components/sliders/slider-slide-one.tsx → src/components/Componentes/slider-slide-one.tsx

@ -1,5 +1,5 @@
import type { SliderSlideProps } from "@/components/sliders/slider-slide";
import Button from "../ui/button";
import type { SliderSlideProps } from "./slider-slide";
import Button from "./button";
const ACTION_AREA_HEIGHT = 80; const ACTION_AREA_HEIGHT = 80;
const CONTENT_MASK = `linear-gradient(to bottom, black 0, black calc(100% - ${ACTION_AREA_HEIGHT}px), transparent calc(100% - ${ACTION_AREA_HEIGHT}px), transparent 100%)`; const CONTENT_MASK = `linear-gradient(to bottom, black 0, black calc(100% - ${ACTION_AREA_HEIGHT}px), transparent calc(100% - ${ACTION_AREA_HEIGHT}px), transparent 100%)`;

2
src/components/sliders/slider-slide-three.tsx → src/components/Componentes/slider-slide-three.tsx

@ -4,7 +4,7 @@ import Image from "next/image";
import type { import type {
GenderAnswer, GenderAnswer,
SliderSlideProps, SliderSlideProps,
} from "@/components/sliders/slider-slide";
} from "./slider-slide";
type GenderOption = { type GenderOption = {
id: GenderAnswer; id: GenderAnswer;

2
src/components/sliders/slider-slide-two.tsx → src/components/Componentes/slider-slide-two.tsx

@ -1,5 +1,5 @@
import Image from "next/image"; import Image from "next/image";
import type { SliderSlideProps } from "@/components/sliders/slider-slide";
import type { SliderSlideProps } from "./slider-slide";
export function SliderSlideTwo({ index }: SliderSlideProps) { export function SliderSlideTwo({ index }: SliderSlideProps) {
return ( return (

0
src/components/sliders/slider-slide.tsx → src/components/Componentes/slider-slide.tsx

0
src/components/ui/sticky-header.tsx → src/components/Componentes/sticky-header.tsx

2
src/components/ui/subscription-required-sheet.tsx → src/components/Componentes/subscription-required-sheet.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import InformationSheet from "@/components/ui/information-sheet";
import InformationSheet from "./information-sheet";
type SubscriptionRequiredSheetProps = { type SubscriptionRequiredSheetProps = {
onClose: () => void; onClose: () => void;

2
src/components/questions/test-intro-page.tsx → src/components/Componentes/test-intro-page.tsx

@ -2,7 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import { IoTimeOutline } from "react-icons/io5"; import { IoTimeOutline } from "react-icons/io5";
import Button from "@/components/ui/button";
import Button from "./button";
type TestIntroPageProps = { type TestIntroPageProps = {
title: string; title: string;

4
src/components/questions/test-loading-screen.tsx → src/components/Componentes/test-loading-screen.tsx

@ -1,7 +1,7 @@
"use client"; "use client";
import { DotsLoader } from "@/components/ui/button";
import { PageBackground } from "@/components/utils/page-background";
import { DotsLoader } from "./button";
import { PageBackground } from "./page-background";
export function AnalyzingIllustration({ export function AnalyzingIllustration({
className = "w-44 h-44", className = "w-44 h-44",

8
src/components/questions/test-questions-flow.tsx → src/components/Componentes/test-questions-flow.tsx

@ -4,10 +4,10 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { GoArrowLeft, GoCheck } from "react-icons/go"; import { GoArrowLeft, GoCheck } from "react-icons/go";
import { IoInformation } from "react-icons/io5"; import { IoInformation } from "react-icons/io5";
import Button from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button";
import StickyHeader from "@/components/ui/sticky-header";
import { PageBackground } from "@/components/utils/page-background";
import Button from "./button";
import NavigationButton from "./navigation-button";
import StickyHeader from "./sticky-header";
import { PageBackground } from "./page-background";
import TestLoadingScreen from "./test-loading-screen"; import TestLoadingScreen from "./test-loading-screen";
export type QuestionOption = { export type QuestionOption = {

0
src/components/ui/token-switcher.tsx → src/components/Componentes/token-switcher.tsx

0
src/components/utils/view-paddings-provider.tsx → src/components/Componentes/view-paddings-provider.tsx

228
src/components/questions/progress-helper.ts

@ -1,228 +0,0 @@
import {
isQuestionVisibleForProfile,
type QuestionListItem,
} from "@/data/question-data";
import type {
MarriageFieldValue,
MarriageGender,
} from "@/hooks/marriage/types";
import { hasQuestionAnswerValue } from "./question-answer-storage";
export function getStoredAge(): number | null {
try {
if (typeof window === "undefined") return null;
const rawValue = window.localStorage.getItem(
"marriage:sections:personal_info:answers",
);
if (!rawValue) return null;
const storedAnswers = JSON.parse(rawValue);
const ageField = storedAnswers.fields?.find(
(f: Record<string, unknown>) =>
f.type === "number" ||
f.label === "Age" ||
f.label === "سن" ||
(typeof f.key === "string" &&
(f.key.endsWith("_age") || f.key.endsWith("_sn"))),
);
if (ageField && ageField.value !== undefined && ageField.value !== null) {
const num = Number(ageField.value);
if (Number.isFinite(num)) return num;
}
const dobField = storedAnswers.fields?.find(
(f: Record<string, unknown>) =>
f.type === "date" ||
f.label === "Date of Birth" ||
f.label === "تاریخ تولد" ||
(typeof f.key === "string" &&
(f.key.endsWith("_date_of_birth") || f.key.endsWith("_tarykh_twld"))),
);
if (dobField?.value) {
const dob = new Date(String(dobField.value));
if (!Number.isNaN(dob.getTime())) {
const today = new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) {
age--;
}
return age >= 0 ? age : null;
}
}
} catch (_e) {}
return null;
}
function isFieldAnswered(
q: Record<string, unknown>,
field: Record<string, unknown> | undefined,
): boolean {
if (!field || !hasQuestionAnswerValue(field.value as MarriageFieldValue)) {
return false;
}
if (q.type === "birthplace") {
const strVal = String(field.value);
const parts = strVal.split(",").map((p) => p.trim());
return parts.length >= 2 && parts[0].length > 0 && parts[1].length > 0;
}
return true;
}
function hashString(value: string) {
let hash = 0;
for (let index = 0; index < value.length; index += 1) {
hash = (hash * 31 + value.charCodeAt(index)) >>> 0;
}
return hash.toString(36);
}
function slugifyTitle(title: string) {
const slug = title
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
return slug || `field_${hashString(title)}`;
}
export function getLocalSectionProgress(
item: QuestionListItem,
profile: { gender?: MarriageGender | null } | null | undefined,
age: number | null,
): number | null {
try {
if (typeof window === "undefined") return null;
const fields: Record<string, unknown>[] = [];
let hasFoundStorage = false;
const mainKey = `marriage:sections:${item.slug}:answers`;
const mainRaw = window.localStorage.getItem(mainKey);
if (mainRaw) {
try {
const parsed = JSON.parse(mainRaw);
if (
parsed &&
Array.isArray(parsed.fields) &&
parsed.fields.length > 0
) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
if (item.slug === "family_marital_history") {
const fbRaw = window.localStorage.getItem(
"marriage:sections:family_background:answers",
);
if (fbRaw) {
try {
const parsed = JSON.parse(fbRaw);
if (parsed && Array.isArray(parsed.fields)) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
const mhRaw = window.localStorage.getItem(
"marriage:sections:marital_history_children:answers",
);
if (mhRaw) {
try {
const parsed = JSON.parse(mhRaw);
if (parsed && Array.isArray(parsed.fields)) {
fields.push(...parsed.fields);
hasFoundStorage = true;
}
} catch {}
}
}
if (!hasFoundStorage || fields.length === 0) {
return null;
}
const profileContext = {
age,
gender: profile?.gender,
};
const hasDobQuestion = item.questions.some(
(q) => q.title === "Date of Birth" || q.title === "تاریخ تولد",
);
const profileVisible = item.questions.filter((question) => {
if (
hasDobQuestion &&
(question.title === "Age" || question.title === "سن")
) {
return false;
}
return isQuestionVisibleForProfile(question, profileContext);
});
const findAnswer = (
questionTitle: string,
questionIndex: number,
originalIndex?: number,
) => {
const indexToUse =
originalIndex !== undefined ? originalIndex : questionIndex;
const key = `q${indexToUse + 1}_${slugifyTitle(questionTitle)}`;
const field = fields.find(
(f) => f && (f.key === key || f.label === questionTitle),
);
return field?.value;
};
const activeQuestions = profileVisible.filter((question, _idx) => {
if (question.logic?.dependsOn) {
const { title, values } = question.logic.dependsOn;
const depIndex = profileVisible.findIndex((q) => q.title === title);
const depQuestion = depIndex !== -1 ? profileVisible[depIndex] : null;
const depAnswer = depQuestion
? findAnswer(depQuestion.title, depIndex, depQuestion.originalIndex)
: undefined;
if (depAnswer === undefined || depAnswer === null) {
return false;
}
const ansList = Array.isArray(depAnswer)
? depAnswer.map(String)
: [String(depAnswer)];
const isMatch = values.some((val) =>
ansList.some((a) => a === val || a.includes(val) || val.includes(a)),
);
if (!isMatch) {
return false;
}
}
return true;
});
if (activeQuestions.length === 0) {
return 100;
}
const answeredCount = activeQuestions.filter((q, idx) => {
const indexToUse = q.originalIndex !== undefined ? q.originalIndex : idx;
const key = `q${indexToUse + 1}_${slugifyTitle(q.title)}`;
const field = fields.find(
(f) => f && (f.key === key || f.label === q.title),
);
return isFieldAnswered(q, field);
}).length;
return Math.max(
0,
Math.min(100, Math.round((answeredCount / activeQuestions.length) * 100)),
);
} catch (_e) {
return null;
}
}

138
src/components/questions/question-slider.tsx

@ -1,138 +0,0 @@
"use client";
import { useLayoutEffect, useRef, useState } from "react";
import type { QuestionField } from "@/data/question-data";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
type QuestionSliderProps = {
question: QuestionField;
questionIndex: number;
disabled?: boolean;
};
export function QuestionSlider({
question,
questionIndex,
disabled,
}: QuestionSliderProps) {
const [min, max] = question.extras.range;
const initialValue = Math.round((min + max) / 2);
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const storedValue = getAnswerValue(question, questionIndex);
const value = typeof storedValue === "number" ? storedValue : initialValue;
const progress = max === min ? 0 : ((value - min) / (max - min)) * 100;
const sliderWrapperRef = useRef<HTMLDivElement>(null);
const sliderRef = useRef<HTMLInputElement>(null);
const thumbWidth = 18;
const bubbleHalfWidth = 18;
const [bubblePosition, setBubblePosition] = useState(bubbleHalfWidth);
const steps = (() => {
const range = max - min;
const interval = range <= 15 ? 1 : range <= 50 ? 5 : range <= 150 ? 10 : 20;
const arr: number[] = [];
for (let val = min; val <= max; val += interval) {
arr.push(val);
}
if (arr[arr.length - 1] !== max) {
arr.push(max);
}
return arr;
})();
useLayoutEffect(() => {
const wrapper = sliderWrapperRef.current;
const slider = sliderRef.current;
if (!wrapper || !slider) {
return;
}
const updateBubblePosition = () => {
const wrapperRect = wrapper.getBoundingClientRect();
const sliderRect = slider.getBoundingClientRect();
const sliderLeft = sliderRect.left - wrapperRect.left;
const thumbCenter =
sliderLeft +
thumbWidth / 2 +
(progress / 100) * (sliderRect.width - thumbWidth);
setBubblePosition(
Math.min(
Math.max(thumbCenter, bubbleHalfWidth),
wrapperRect.width - bubbleHalfWidth,
),
);
};
updateBubblePosition();
const resizeObserver = new ResizeObserver(updateBubblePosition);
resizeObserver.observe(wrapper);
resizeObserver.observe(slider);
return () => resizeObserver.disconnect();
}, [progress]);
return (
<div
className={[
"flex w-full flex-col gap-3.5 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<div className="pt-7">
<div ref={sliderWrapperRef} className="relative">
<div
className="-translate-x-1/2 absolute -top-7.5 z-10 flex items-center justify-center rounded-[8px] bg-[#F2465F] px-3 py-1 text-[14px] font-bold text-white shadow-md after:absolute after:-bottom-[4px] after:left-1/2 after:h-[9px] after:w-2.5 after:-translate-x-1/2 after:rotate-45 after:rounded-[2px] after:bg-[#F2465F] after:content-['']"
style={{ left: `${bubblePosition}px` }}
>
{value}
</div>
<input
ref={sliderRef}
type="range"
min={min}
max={max}
step={1}
value={value}
onChange={(event) =>
setAnswerValue(
question,
questionIndex,
Number(event.target.value),
)
}
disabled={disabled}
className="question-slider-range w-full"
style={{
background: `linear-gradient(to right, #F2465F 0%, #F2465F ${progress}%, #FCE7EA ${progress}%, #FCE7EA 100%)`,
}}
/>
</div>
<div
className="relative mt-2 h-5 text-[#B7B7B7] text-[10px] font-medium"
style={{ width: "100%" }}
>
{steps.map((step) => {
const leftPct = ((step - min) / (max - min)) * 100;
return (
<span
key={step}
className="absolute -translate-x-1/2"
style={{
left: `calc(${leftPct}% - (${leftPct / 100} * ${thumbWidth}px) + ${thumbWidth / 2}px)`,
}}
>
{step}
</span>
);
})}
</div>
</div>
</div>
);
}
export default QuestionSlider;

86
src/components/questions/question-title.tsx

@ -1,86 +0,0 @@
"use client";
import { useState } from "react";
import { IoEyeOff } from "react-icons/io5";
import HelpModal from "@/components/ui/help-modal";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
type QuestionTitleProps = {
question: QuestionField;
className?: string;
};
export function QuestionTitle({ question, className }: QuestionTitleProps) {
const { dictionary: t, locale } = useI18n();
const [isHelpOpen, setIsHelpOpen] = useState(false);
const isMonthlyIncome =
question.title === "Monthly Income" ||
question.title === "میزان درآمد ماهانه";
return (
<span className="block w-full">
{question.private ? (
<span className="mb-4 mx-auto flex w-fit items-center gap-1.5 rounded-full bg-[#F8D7DA] px-3 py-1 text-center text-[10px] font-bold text-[#D9383A]">
<IoEyeOff
aria-hidden="true"
className="shrink-0 text-[15px] text-[#D9383A]"
/>
<span>
{t.questions.privateFieldNotice ?? "Private (Advisors Only)"}
</span>
</span>
) : null}
<span
className={[
"flex flex-wrap items-center gap-1.5 text-[15px] font-bold text-[#181818]",
className,
]
.filter(Boolean)
.join(" ")}
>
<span className="inline">
{question.title}
{isMonthlyIncome
? locale === "fa"
? " (تقریبی)"
: " (approximately)"
: ""}
{question.required ? (
<span className="text-[#F2465F] font-bold ml-1 inline">*</span>
) : null}
{question.tooltip ? (
<>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsHelpOpen(true);
}}
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-[#F2465F] text-[10px] font-serif italic text-white shadow-xs cursor-pointer hover:scale-110 active:scale-95 transition-transform ml-1.5 align-middle"
aria-label="Help"
>
i
</button>
<HelpModal
isOpen={isHelpOpen}
onClose={() => setIsHelpOpen(false)}
description={question.tooltip}
/>
</>
) : null}
</span>
{question.description ? (
<span className="text-[13.5px] font-normal text-[#667085]">
({question.description})
</span>
) : null}
</span>
</span>
);
}
export default QuestionTitle;

4
src/components/utils/dev-click-to-component.tsx

@ -1,4 +0,0 @@
export {
DevClickToComponent,
default,
} from "@/components/dev/dev-click-to-component";

60
src/data/question-data.ts

@ -164,9 +164,69 @@ export function getQuestionListItems(locale: Locale = defaultLocale) {
const newItems = [...items]; const newItems = [...items];
newItems[fbIndex] = mergedItem; newItems[fbIndex] = mergedItem;
newItems.splice(mhIndex, 1); newItems.splice(mhIndex, 1);
// Reorder items: bring future_spouse_criteria and identity_verification above the tests
const personalityIndex = newItems.findIndex(
(item) => item.slug === "personality_test",
);
const spouseItem = newItems.find(
(item) => item.slug === "future_spouse_criteria",
);
const identityItem = newItems.find(
(item) => item.slug === "identity_verification",
);
if (personalityIndex !== -1 && (spouseItem || identityItem)) {
const filtered = newItems.filter(
(item) =>
item.slug !== "future_spouse_criteria" &&
item.slug !== "identity_verification",
);
const insertAt = filtered.findIndex(
(item) => item.slug === "personality_test",
);
if (insertAt !== -1) {
const result = [...filtered];
const itemsToInsert = [];
if (spouseItem) itemsToInsert.push(spouseItem);
if (identityItem) itemsToInsert.push(identityItem);
result.splice(insertAt, 0, ...itemsToInsert);
return result;
}
}
return newItems; return newItems;
} }
// Also handle reordering if fbIndex/mhIndex are not found (fallback)
const personalityIndex = items.findIndex(
(item) => item.slug === "personality_test",
);
const spouseItem = items.find(
(item) => item.slug === "future_spouse_criteria",
);
const identityItem = items.find(
(item) => item.slug === "identity_verification",
);
if (personalityIndex !== -1 && (spouseItem || identityItem)) {
const filtered = items.filter(
(item) =>
item.slug !== "future_spouse_criteria" &&
item.slug !== "identity_verification",
);
const insertAt = filtered.findIndex(
(item) => item.slug === "personality_test",
);
if (insertAt !== -1) {
const result = [...filtered];
const itemsToInsert = [];
if (spouseItem) itemsToInsert.push(spouseItem);
if (identityItem) itemsToInsert.push(identityItem);
result.splice(insertAt, 0, ...itemsToInsert);
return result;
}
}
return items; return items;
} }

77
src/data/questions/en.json

@ -555,6 +555,9 @@
"title": "Ability to Support Marriage Expenses", "title": "Ability to Support Marriage Expenses",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": {
"genders": ["male"]
},
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
@ -573,6 +576,9 @@
"title": "Current Housing Status", "title": "Current Housing Status",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": {
"genders": ["male"]
},
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
@ -592,6 +598,9 @@
"title": "Post-Marriage Housing Plan", "title": "Post-Marriage Housing Plan",
"type": "radio", "type": "radio",
"required": true, "required": true,
"audience": {
"genders": ["male"]
},
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
@ -606,7 +615,7 @@
}, },
{ {
"title": "Additional Comments on Economic and Housing Status", "title": "Additional Comments on Economic and Housing Status",
"type": "text",
"type": "textarea",
"required": false, "required": false,
"extras": { "extras": {
"placeHolder": "Enter your explanation here...", "placeHolder": "Enter your explanation here...",
@ -669,13 +678,14 @@
}, },
{ {
"title": "Short Family Description", "title": "Short Family Description",
"type": "text",
"type": "textarea",
"required": false, "required": false,
"extras": { "extras": {
"placeHolder": "Enter details here...", "placeHolder": "Enter details here...",
"range": [0, 0], "range": [0, 0],
"options": [] "options": []
}
},
"tooltip": "Write any important point that was not covered in the options above here."
}, },
{ {
"title": "Family's Religious and Ideological Atmosphere", "title": "Family's Religious and Ideological Atmosphere",
@ -886,7 +896,8 @@
], ],
"noSearch": true "noSearch": true
}, },
"private": true
"private": true,
"tooltip": "Please select the option that most closely matches your daily attire in public.\n\n* **For Female Users:** This question asks you to specify your own **current status**, personal traits, and individual lifestyle preferences.\n* **For Male Users:** This question asks you to specify your **expectations**, desired criteria, and preferences regarding your future spouse."
}, },
{ {
"title": "Makeup in Public", "title": "Makeup in Public",
@ -904,7 +915,8 @@
"Full makeup" "Full makeup"
] ]
}, },
"private": true
"private": true,
"tooltip": "Please select the option that most closely matches your daily use of makeup in public.\n\nThis question asks you to specify your own **current status**, personal traits, and individual lifestyle preferences."
}, },
{ {
"title": "Attitude towards Religion and Politics", "title": "Attitude towards Religion and Politics",
@ -922,7 +934,8 @@
], ],
"noSearch": true "noSearch": true
}, },
"private": true
"private": true,
"tooltip": "Please select the option that best describes your view on religion and your expectations of your future spouse."
}, },
{ {
"title": "Stance on Current Government/State", "title": "Stance on Current Government/State",
@ -940,7 +953,8 @@
], ],
"noSearch": true "noSearch": true
}, },
"private": true
"private": true,
"tooltip": "This section is designed to prevent serious ideological conflicts in married life."
}, },
{ {
"title": "Boundaries with the Opposite Sex", "title": "Boundaries with the Opposite Sex",
@ -956,7 +970,8 @@
"No specific boundaries - Fully comfortable with modern social interactions." "No specific boundaries - Fully comfortable with modern social interactions."
] ]
}, },
"private": true
"private": true,
"tooltip": "Please select the option that best describes your daily behavior when interacting with members of the opposite sex."
}, },
{ {
"title": "Smoking", "title": "Smoking",
@ -1133,14 +1148,15 @@
}, },
{ {
"title": "Short explanation about your lifestyle", "title": "Short explanation about your lifestyle",
"type": "text",
"type": "textarea",
"required": false, "required": false,
"extras": { "extras": {
"placeHolder": "Enter details here...", "placeHolder": "Enter details here...",
"range": [0, 0], "range": [0, 0],
"options": [] "options": []
}, },
"private": true
"private": true,
"tooltip": "Write any important point that was not covered in the options above here."
} }
] ]
}, },
@ -1175,12 +1191,12 @@
"questions": [ "questions": [
{ {
"title": "Desired Age Range of Future Spouse", "title": "Desired Age Range of Future Spouse",
"type": "text",
"type": "scale",
"required": true, "required": true,
"description": "from [number] to [number] years",
"description": "",
"extras": { "extras": {
"placeHolder": "25-30", "placeHolder": "25-30",
"range": [0, 0],
"range": [18, 80],
"options": [] "options": []
}, },
"private": true "private": true
@ -1245,7 +1261,7 @@
"title": "Desired Ethnicity, Language, or Nationality of Future Spouse", "title": "Desired Ethnicity, Language, or Nationality of Future Spouse",
"type": "text", "type": "text",
"required": false, "required": false,
"description": "Optional text field",
"description": "",
"extras": { "extras": {
"placeHolder": "English, French, etc.", "placeHolder": "English, French, etc.",
"range": [0, 0], "range": [0, 0],
@ -1323,10 +1339,19 @@
"Only if children don't live with them" "Only if children don't live with them"
] ]
}, },
"private": true
"private": true,
"logic": {
"dependsOn": {
"title": "Acceptance of Future Spouse's Marriage History",
"values": [
"No difference",
"Depends on reason, duration, and conditions"
]
}
}
}, },
{ {
"title": "Desired Clothing and Hijab Criteria",
"title": "Desired Clothing style for Future Spouse",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": { "audience": {
@ -1347,7 +1372,7 @@
"private": true "private": true
}, },
{ {
"title": "Desired Clothing and Grooming for Future Spouse",
"title": "Desired Clothing style for Future Spouse",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": { "audience": {
@ -1389,6 +1414,11 @@
"title": "Desired Spouse's Tendency for Employment", "title": "Desired Spouse's Tendency for Employment",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": {
"genders": [
"female"
]
},
"extras": { "extras": {
"placeHolder": "Select one option", "placeHolder": "Select one option",
"range": [0, 0], "range": [0, 0],
@ -1492,7 +1522,7 @@
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "Select options", "placeHolder": "Select options",
"range": [0, 0],
"range": [1, 10],
"options": [ "options": [
"Smoking is a red line", "Smoking is a red line",
"Hookah is a red line", "Hookah is a red line",
@ -1609,15 +1639,16 @@
}, },
{ {
"title": "Additional Comments and Red Lines", "title": "Additional Comments and Red Lines",
"type": "text",
"type": "textarea",
"required": true, "required": true,
"description": "Large text field",
"description": "",
"extras": { "extras": {
"placeHolder": "Enter details here...", "placeHolder": "Enter details here...",
"range": [0, 0], "range": [0, 0],
"options": [] "options": []
}, },
"private": true
"private": true,
"tooltip": "Write any important point that was not covered in the options above here."
} }
] ]
}, },
@ -1635,7 +1666,7 @@
"type": "photo", "type": "photo",
"required": true, "required": true,
"private": true, "private": true,
"description": "Recent and clear face photo",
"description": "This field requires the user to upload a recent, clear facial photograph that will remain private and accessible exclusively to advisors.",
"extras": { "extras": {
"placeHolder": "Upload photo", "placeHolder": "Upload photo",
"range": [0, 0], "range": [0, 0],
@ -1656,7 +1687,7 @@
}, },
{ {
"title": "Confirmation of Document and Information Accuracy", "title": "Confirmation of Document and Information Accuracy",
"type": "radio",
"type": "checkbox",
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "I confirm", "placeHolder": "I confirm",

77
src/data/questions/fa.json

@ -555,6 +555,9 @@
"title": "توانایی تأمین هزینه‌های زندگی مشترک", "title": "توانایی تأمین هزینه‌های زندگی مشترک",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": {
"genders": ["male"]
},
"extras": { "extras": {
"placeHolder": "یک گزینه را انتخاب کنید", "placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0], "range": [0, 0],
@ -573,6 +576,9 @@
"title": "وضعیت مسکن فعلی", "title": "وضعیت مسکن فعلی",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": {
"genders": ["male"]
},
"extras": { "extras": {
"placeHolder": "یک گزینه را انتخاب کنید", "placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0], "range": [0, 0],
@ -592,6 +598,9 @@
"title": "برنامه یا توانایی تأمین مسکن بعد از ازدواج", "title": "برنامه یا توانایی تأمین مسکن بعد از ازدواج",
"type": "radio", "type": "radio",
"required": true, "required": true,
"audience": {
"genders": ["male"]
},
"extras": { "extras": {
"placeHolder": "یک گزینه را انتخاب کنید", "placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0], "range": [0, 0],
@ -606,7 +615,7 @@
}, },
{ {
"title": "توضیح تکمیلی درباره وضعیت اقتصادی و مسکن", "title": "توضیح تکمیلی درباره وضعیت اقتصادی و مسکن",
"type": "text",
"type": "textarea",
"required": false, "required": false,
"extras": { "extras": {
"placeHolder": "توضیحات خود را اینجا وارد کنید...", "placeHolder": "توضیحات خود را اینجا وارد کنید...",
@ -669,13 +678,14 @@
}, },
{ {
"title": "توضیح کوتاه درباره خانواده", "title": "توضیح کوتاه درباره خانواده",
"type": "text",
"type": "textarea",
"required": false, "required": false,
"extras": { "extras": {
"placeHolder": "توضیحات خود را اینجا وارد کنید...", "placeHolder": "توضیحات خود را اینجا وارد کنید...",
"range": [0, 0], "range": [0, 0],
"options": [] "options": []
}
},
"tooltip": "هر نکته مهمی که در گزینه‌های بالا نبود، اینجا بنویسید."
}, },
{ {
"title": "فضای مذهبی و اعتقادی خانواده", "title": "فضای مذهبی و اعتقادی خانواده",
@ -886,7 +896,8 @@
], ],
"noSearch": true "noSearch": true
}, },
"private": true
"private": true,
"tooltip": "لطفاً گزینه‌ای را انتخاب کنید که بیشترین تطابق را با پوشش روزمره شما در اجتماع دارد.\n\n### توضیحات تکمیلی سوال\n\n* **برای کاربران خانم:** این سوال از شما می‌خواهد که **وضعیت فعلی**، ویژگی‌های شخصی و ترجیحات سبک زندگی فردی خود را مشخص کنید.\n* **برای کاربران آقا:** این سوال از شما می‌خواهد که **انتظارات**، معیارهای مورد نظر و ترجیحات خود را در رابطه با همسر آینده‌تان مشخص کنید."
}, },
{ {
"title": "استفاده از آرایش در اجتماع", "title": "استفاده از آرایش در اجتماع",
@ -904,7 +915,8 @@
"آرایش کامل" "آرایش کامل"
] ]
}, },
"private": true
"private": true,
"tooltip": "لطفاً گزینه‌ای را انتخاب کنید که بیشترین تطابق را با استفاده روزمره شما از آرایش در اجتماع دارد.\n\nاین سوال از شما می‌خواهد که **وضعیت فعلی**، ویژگی‌های شخصی و ترجیحات سبک زندگی فردی خود را مشخص کنید."
}, },
{ {
"title": "نگرش به رابطه دین و سیاست", "title": "نگرش به رابطه دین و سیاست",
@ -922,7 +934,8 @@
], ],
"noSearch": true "noSearch": true
}, },
"private": true
"private": true,
"tooltip": "لطفاً گزینه‌ای را انتخاب کنید که نگاه شما به مذهب و انتظار شما از همسر آینده‌تان را بهتر توصیف می‌کند."
}, },
{ {
"title": "موضع نسبت به نظام و حاکمیت فعلی", "title": "موضع نسبت به نظام و حاکمیت فعلی",
@ -940,7 +953,8 @@
], ],
"noSearch": true "noSearch": true
}, },
"private": true
"private": true,
"tooltip": "این بخش برای جلوگیری از تنش‌های جدیِ اعتقادی در زندگی مشترک طراحی شده است."
}, },
{ {
"title": "حدود روابط با جنس مخالف", "title": "حدود روابط با جنس مخالف",
@ -956,7 +970,8 @@
"مرز خاصی ندارم - با تعاملات اجتماعی مدرن کاملاً راحت هستم." "مرز خاصی ندارم - با تعاملات اجتماعی مدرن کاملاً راحت هستم."
] ]
}, },
"private": true
"private": true,
"tooltip": "لطفاً گزینه‌ای را انتخاب کنید که رفتار روزمره شما را در مواجهه با نامحرم بهتر توصیف می‌کند."
}, },
{ {
"title": "سیگار", "title": "سیگار",
@ -1133,14 +1148,15 @@
}, },
{ {
"title": "توضیح کوتاه درباره سبک زندگی", "title": "توضیح کوتاه درباره سبک زندگی",
"type": "text",
"type": "textarea",
"required": false, "required": false,
"extras": { "extras": {
"placeHolder": "توضیحات را اینجا وارد کنید...", "placeHolder": "توضیحات را اینجا وارد کنید...",
"range": [0, 0], "range": [0, 0],
"options": [] "options": []
}, },
"private": true
"private": true,
"tooltip": "هر نکته مهمی که در گزینه‌های بالا نبود، اینجا بنویسید."
} }
] ]
}, },
@ -1175,12 +1191,12 @@
"questions": [ "questions": [
{ {
"title": "بازه سنی مطلوب همسر آینده", "title": "بازه سنی مطلوب همسر آینده",
"type": "text",
"type": "scale",
"required": true, "required": true,
"description": "از [عدد] تا [عدد] سال",
"description": "",
"extras": { "extras": {
"placeHolder": "۲۵-۳۰", "placeHolder": "۲۵-۳۰",
"range": [0, 0],
"range": [18, 80],
"options": [] "options": []
}, },
"private": true "private": true
@ -1245,7 +1261,7 @@
"title": "قومیت، زبان یا ملیت مطلوب همسر آینده", "title": "قومیت، زبان یا ملیت مطلوب همسر آینده",
"type": "text", "type": "text",
"required": false, "required": false,
"description": "فیلد متنی اختیاری",
"description": "",
"extras": { "extras": {
"placeHolder": "انگلیسی، فرانسوی و...", "placeHolder": "انگلیسی، فرانسوی و...",
"range": [0, 0], "range": [0, 0],
@ -1323,10 +1339,19 @@
"فقط اگر فرزند با او زندگی نکند، بررسی می‌کنم." "فقط اگر فرزند با او زندگی نکند، بررسی می‌کنم."
] ]
}, },
"private": true
"private": true,
"logic": {
"dependsOn": {
"title": "پذیرش سابقه عقد یا ازدواج همسر آینده",
"values": [
"تفاوتی ندارد.",
"بستگی به علت جدایی، مدت ازدواج قبلی و شرایط خانوادگی دارد."
]
}
}
}, },
{ {
"title": "معیارهای پوشش و حجاب همسر آینده (برای آقایان)",
"title": "پوشش و استایل مطلوب همسر آینده",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": { "audience": {
@ -1347,7 +1372,7 @@
"private": true "private": true
}, },
{ {
"title": "معیارهای پوشش و حجاب همسر آینده (برای خانم‌ها)",
"title": "پوشش و استایل مطلوب همسر آینده",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": { "audience": {
@ -1389,6 +1414,11 @@
"title": "تمایل به اشتغال همسر آینده", "title": "تمایل به اشتغال همسر آینده",
"type": "dropdown", "type": "dropdown",
"required": true, "required": true,
"audience": {
"genders": [
"female"
]
},
"extras": { "extras": {
"placeHolder": "یک گزینه را انتخاب کنید", "placeHolder": "یک گزینه را انتخاب کنید",
"range": [0, 0], "range": [0, 0],
@ -1492,7 +1522,7 @@
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "انتخاب کنید", "placeHolder": "انتخاب کنید",
"range": [0, 0],
"range": [1, 10],
"options": [ "options": [
"سیگار برایم خط قرمز است.", "سیگار برایم خط قرمز است.",
"قلیان برایم خط قرمز است.", "قلیان برایم خط قرمز است.",
@ -1609,15 +1639,16 @@
}, },
{ {
"title": "توضیحات تکمیلی و سایر خطوط قرمز", "title": "توضیحات تکمیلی و سایر خطوط قرمز",
"type": "text",
"type": "textarea",
"required": true, "required": true,
"description": "فیلد متنی بزرگ",
"description": "",
"extras": { "extras": {
"placeHolder": "توضیحات تکمیلی را اینجا وارد کنید...", "placeHolder": "توضیحات تکمیلی را اینجا وارد کنید...",
"range": [0, 0], "range": [0, 0],
"options": [] "options": []
}, },
"private": true
"private": true,
"tooltip": "هر نکته مهمی که در گزینه‌های بالا نبود، اینجا بنویسید."
} }
] ]
}, },
@ -1635,7 +1666,7 @@
"type": "photo", "type": "photo",
"required": true, "required": true,
"private": true, "private": true,
"description": "عکس جدید و واضح",
"description": "این بخش از کاربر می‌خواهد یک عکس جدید و واضح از چهره خود بارگذاری کند که کاملاً خصوصی باقی مانده و منحصراً برای معرف‌ها قابل دسترسی خواهد بود.",
"extras": { "extras": {
"placeHolder": "بارگذاری تصویر", "placeHolder": "بارگذاری تصویر",
"range": [0, 0], "range": [0, 0],
@ -1656,7 +1687,7 @@
}, },
{ {
"title": "تأیید تطابق مدارک و اطلاعات", "title": "تأیید تطابق مدارک و اطلاعات",
"type": "radio",
"type": "checkbox",
"required": true, "required": true,
"extras": { "extras": {
"placeHolder": "تأیید می‌کنم", "placeHolder": "تأیید می‌کنم",

12
src/translations/locales/ar.json

@ -21,7 +21,8 @@
"itemsPerPage": "العناصر في الصفحة", "itemsPerPage": "العناصر في الصفحة",
"help": "Help", "help": "Help",
"gotIt": "Got it", "gotIt": "Got it",
"helpDescription": "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance."
"helpDescription": "Psychologically, this practice fosters a sense of empathy and contentment, which can reduce financial stress. Socially, lending strengthens neighborhood bonds and creates support networks that can lead to economic opportunities. This hadith encourages believers to lend dough, bread, and fire to increase their sustenance.",
"other": "أخرى"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "الخلفية العائلية، الحالة الاجتماعية والأطفال", "familyMaritalTitle": "الخلفية العائلية، الحالة الاجتماعية والأطفال",
"familyMaritalEstimate": "20 دقيقة"
"familyMaritalEstimate": "20 دقيقة",
"notAPriority": "هذا الموضوع ليس أولوية بالنسبة لي.",
"writeOtherTraits": "Write other options...",
"fromAge": "من",
"toAge": "إلى"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/az.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Digər"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Ailə Keçmişi, Ailə Vəziyyəti və Uşaqlar", "familyMaritalTitle": "Ailə Keçmişi, Ailə Vəziyyəti və Uşaqlar",
"familyMaritalEstimate": "20 dəqiqə"
"familyMaritalEstimate": "20 dəqiqə",
"notAPriority": "Bu mövzu mənim üçün prioritet deyil.",
"writeOtherTraits": "Write other options...",
"fromAge": "Aşağı",
"toAge": "Yuxarı"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/bn.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "অন্যান্য"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "পারিবারিক পটভূমি, বৈবাহিক অবস্থা এবং সন্তানাদি", "familyMaritalTitle": "পারিবারিক পটভূমি, বৈবাহিক অবস্থা এবং সন্তানাদি",
"familyMaritalEstimate": "20 মিনিট"
"familyMaritalEstimate": "20 মিনিট",
"notAPriority": "এই বিষয়টি আমার জন্য অগ্রাধিকার নয়।",
"writeOtherTraits": "Write other options...",
"fromAge": "থেকে",
"toAge": "পর্যন্ত"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/da.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Andet"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Familiebaggrund, civilstand og børn", "familyMaritalTitle": "Familiebaggrund, civilstand og børn",
"familyMaritalEstimate": "20 minutter"
"familyMaritalEstimate": "20 minutter",
"notAPriority": "Dette emne er ikke en prioritet for mig.",
"writeOtherTraits": "Write other options...",
"fromAge": "Fra",
"toAge": "Til"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/de.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Andere"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Familiärer Hintergrund, Familienstand und Kinder", "familyMaritalTitle": "Familiärer Hintergrund, Familienstand und Kinder",
"familyMaritalEstimate": "20 Minuten"
"familyMaritalEstimate": "20 Minuten",
"notAPriority": "Dieses Thema hat für mich keine Priorität.",
"writeOtherTraits": "Write other options...",
"fromAge": "Von",
"toAge": "Bis"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/en.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Other"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Family Background, Marital Status, and Children", "familyMaritalTitle": "Family Background, Marital Status, and Children",
"familyMaritalEstimate": "20 minutes"
"familyMaritalEstimate": "20 minutes",
"notAPriority": "This is not a priority for me",
"writeOtherTraits": "Write other options...",
"fromAge": "From",
"toAge": "To"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/es.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Otro"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Antecedentes familiares, estado civil e hijos", "familyMaritalTitle": "Antecedentes familiares, estado civil e hijos",
"familyMaritalEstimate": "20 minutos"
"familyMaritalEstimate": "20 minutos",
"notAPriority": "Este tema no es una prioridad para mí.",
"writeOtherTraits": "Write other options...",
"fromAge": "Desde",
"toAge": "Hasta"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/fa.json

@ -21,7 +21,8 @@
"totalPages": "مجموع صفحات", "totalPages": "مجموع صفحات",
"nextPage": "صفحه بعدی", "nextPage": "صفحه بعدی",
"previousPage": "صفحه قبلی", "previousPage": "صفحه قبلی",
"itemsPerPage": "تعداد موارد در هر صفحه"
"itemsPerPage": "تعداد موارد در هر صفحه",
"other": "سایر"
}, },
"intro": { "intro": {
"imageAlt": "ازدواج آسمانی", "imageAlt": "ازدواج آسمانی",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "برای حفظ آرامش، امنیت و شأن شما، روند آشنایی در پلتفرم ما با الگوگیری از رسوم اصیل و محترمانه خانوادگی پیش می‌رود. حضور یک فرد معتمد (ترجیحاً پدر یا مادر) به عنوان رابط، علاوه بر اینکه نشان‌دهنده اصالت شماست، باعث می‌شود طرف مقابل نیز با جدیت، احترام و اطمینان کامل قدم پیش بگذارد.", "guardianNoticeUnder27": "برای حفظ آرامش، امنیت و شأن شما، روند آشنایی در پلتفرم ما با الگوگیری از رسوم اصیل و محترمانه خانوادگی پیش می‌رود. حضور یک فرد معتمد (ترجیحاً پدر یا مادر) به عنوان رابط، علاوه بر اینکه نشان‌دهنده اصالت شماست، باعث می‌شود طرف مقابل نیز با جدیت، احترام و اطمینان کامل قدم پیش بگذارد.",
"guardianNoticeOver27": "هدف ما شکل‌گیری پیوندهای پایدار بر بستر اعتماد متقابل است. با اینکه ثبت اطلاعات رابط برای شما الزامی نیست، اما معرفی یک فرد معتمد (مانند پدر، مادر یا بزرگتر خانواده) نشان‌دهنده شفافیت و نیت جدی شما برای ازدواج است. پروفایل‌هایی که دارای رابط معتمد هستند، اعتبار بسیار بالاتری دارند و باعث ایجاد اطمینان خاطر بیشتری در خانواده طرف مقابل می‌شوند.", "guardianNoticeOver27": "هدف ما شکل‌گیری پیوندهای پایدار بر بستر اعتماد متقابل است. با اینکه ثبت اطلاعات رابط برای شما الزامی نیست، اما معرفی یک فرد معتمد (مانند پدر، مادر یا بزرگتر خانواده) نشان‌دهنده شفافیت و نیت جدی شما برای ازدواج است. پروفایل‌هایی که دارای رابط معتمد هستند، اعتبار بسیار بالاتری دارند و باعث ایجاد اطمینان خاطر بیشتری در خانواده طرف مقابل می‌شوند.",
"familyMaritalTitle": "پیشینه خانوادگی، وضعیت تأهل و فرزندان", "familyMaritalTitle": "پیشینه خانوادگی، وضعیت تأهل و فرزندان",
"familyMaritalEstimate": "20 دقیقه"
"familyMaritalEstimate": "20 دقیقه",
"notAPriority": "این موضوع برایم اولویت ندارد.",
"writeOtherTraits": "سایر موارد را بنویسید...",
"fromAge": "از",
"toAge": "تا"
}, },
"match": { "match": {
"title": "گزینه جدید", "title": "گزینه جدید",
@ -154,5 +159,6 @@
"description": "درخواست معرفی شما از طرف ما به خانواده طرف مقابل ارسال شد. در صورت تایید ایشان، اطلاعات تماس به اطلاع شما خواهد رسید.", "description": "درخواست معرفی شما از طرف ما به خانواده طرف مقابل ارسال شد. در صورت تایید ایشان، اطلاعات تماس به اطلاع شما خواهد رسید.",
"matchProfile": "مشاهده پروفایل گزینه", "matchProfile": "مشاهده پروفایل گزینه",
"profileLocked": "پروفایل قفل است" "profileLocked": "پروفایل قفل است"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ این بخش کاملاً محرمانه است و فقط برای مچینگ و بررسی کارشناسان استفاده میشود."
} }

12
src/translations/locales/fr.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Autre"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Antécédents familiaux, état civil et enfants", "familyMaritalTitle": "Antécédents familiaux, état civil et enfants",
"familyMaritalEstimate": "20 minutes"
"familyMaritalEstimate": "20 minutes",
"notAPriority": "Ce sujet n'est pas une priorité pour moi.",
"writeOtherTraits": "Write other options...",
"fromAge": "De",
"toAge": "À"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/gu.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "બીજું"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "પારિવારિક પૃષ્ઠભૂમિ, વૈવાહિક સ્થિતિ અને બાળકો", "familyMaritalTitle": "પારિવારિક પૃષ્ઠભૂમિ, વૈવાહિક સ્થિતિ અને બાળકો",
"familyMaritalEstimate": "20 મિનિટ"
"familyMaritalEstimate": "20 મિનિટ",
"notAPriority": "આ વિષય મારા માટે પ્રાથમિકતા નથી.",
"writeOtherTraits": "Write other options...",
"fromAge": "થી",
"toAge": "સુધી"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/ha.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Sauran"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Tarihin Iyali, Yanayin Aure da Yara", "familyMaritalTitle": "Tarihin Iyali, Yanayin Aure da Yara",
"familyMaritalEstimate": "Minti 20"
"familyMaritalEstimate": "Minti 20",
"notAPriority": "Wannan batu ba fifiko ba ne a gare ni.",
"writeOtherTraits": "Write other options...",
"fromAge": "Daga",
"toAge": "Zuwa"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/he.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "אחר"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "רקע משפחתי, מצב משפחתי וילדים", "familyMaritalTitle": "רקע משפחתי, מצב משפחתי וילדים",
"familyMaritalEstimate": "20 דקות"
"familyMaritalEstimate": "20 דקות",
"notAPriority": "נושא זה אינו בראש סדר העדיפויות שלי.",
"writeOtherTraits": "Write other options...",
"fromAge": "מ-",
"toAge": "עד"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/hi.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "अन्य"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "पारिवारिक पृष्ठभूमि, वैवाहिक स्थिति और बच्चे", "familyMaritalTitle": "पारिवारिक पृष्ठभूमि, वैवाहिक स्थिति और बच्चे",
"familyMaritalEstimate": "20 मिनट"
"familyMaritalEstimate": "20 मिनट",
"notAPriority": "यह विषय मेरे लिए प्राथमिकता नहीं है।",
"writeOtherTraits": "Write other options...",
"fromAge": "से",
"toAge": "तक"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/id.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Lainnya"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Latar Belakang Keluarga, Status Pernikahan, dan Anak-anak", "familyMaritalTitle": "Latar Belakang Keluarga, Status Pernikahan, dan Anak-anak",
"familyMaritalEstimate": "20 menit"
"familyMaritalEstimate": "20 menit",
"notAPriority": "Topik ini bukan prioritas bagi saya.",
"writeOtherTraits": "Write other options...",
"fromAge": "Dari",
"toAge": "Hingga"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/ks.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "أخرى"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "خاندانی پس منظر، ازدواجی حیثیت تہٰ شرے", "familyMaritalTitle": "خاندانی پس منظر، ازدواجی حیثیت تہٰ شرے",
"familyMaritalEstimate": "20 منٹ"
"familyMaritalEstimate": "20 منٹ",
"notAPriority": "یہ موضوع چھ نہ میہ خاطرہ ترجیح۔",
"writeOtherTraits": "Write other options...",
"fromAge": "پؠٹھ",
"toAge": "تام"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/pt.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Outro"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Histórico familiar, estado civil e filhos", "familyMaritalTitle": "Histórico familiar, estado civil e filhos",
"familyMaritalEstimate": "20 minutos"
"familyMaritalEstimate": "20 minutos",
"notAPriority": "Este assunto não é uma prioridade para mim.",
"writeOtherTraits": "Write other options...",
"fromAge": "De",
"toAge": "Até"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/ru.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Другое"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Семейное положение, история брака и дети", "familyMaritalTitle": "Семейное положение, история брака и дети",
"familyMaritalEstimate": "20 минут"
"familyMaritalEstimate": "20 минут",
"notAPriority": "Эта тема не является приоритетом для меня.",
"writeOtherTraits": "Write other options...",
"fromAge": "От",
"toAge": "До"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

12
src/translations/locales/sw.json

@ -21,7 +21,8 @@
"totalPages": "Total Pages", "totalPages": "Total Pages",
"nextPage": "Next Page", "nextPage": "Next Page",
"previousPage": "Previous Page", "previousPage": "Previous Page",
"itemsPerPage": "Items Per Page"
"itemsPerPage": "Items Per Page",
"other": "Nyingine"
}, },
"intro": { "intro": {
"imageAlt": "heavenly marriage", "imageAlt": "heavenly marriage",
@ -69,7 +70,11 @@
"guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.", "guardianNoticeUnder27": "To preserve your peace of mind, security, and dignity, the acquaintance process on our platform follows authentic and respectful family traditions. Having a trusted person (preferably a father or mother) as a representative, in addition to reflecting your noble family background, encourages the other party to step forward with full seriousness, respect, and confidence.",
"guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.", "guardianNoticeOver27": "Our goal is to establish lasting bonds based on mutual trust. Although registering a representative is not mandatory for you, introducing a trusted person (such as a father, mother, or family elder) demonstrates your transparency and serious intent for marriage. Profiles that feature a trusted representative command significantly higher credibility and provide greater peace of mind to the other party's family.",
"familyMaritalTitle": "Historia ya Familia, Hali ya Ndoa na Watoto", "familyMaritalTitle": "Historia ya Familia, Hali ya Ndoa na Watoto",
"familyMaritalEstimate": "Dakika 20"
"familyMaritalEstimate": "Dakika 20",
"notAPriority": "Mada hii sio kipaumbele kwangu.",
"writeOtherTraits": "Write other options...",
"fromAge": "Kuanzia",
"toAge": "Hadi"
}, },
"match": { "match": {
"title": "New Match", "title": "New Match",
@ -154,5 +159,6 @@
"description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.", "description": "We will propose marriage on your behalf. If they accept, their contact details will be shared with you.",
"matchProfile": "Match Profile", "matchProfile": "Match Profile",
"profileLocked": "Profile is locked" "profileLocked": "Profile is locked"
}
},
"spouseCriteriaConfidentialNotice": "⚠️ This section is completely confidential and is only used for matching and review by advisors."
} }

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save