From ab8b055d8a2f702f6fd0cbe8506faa8633d83a52 Mon Sep 17 00:00:00 2001 From: ghorbani Date: Sun, 2 Aug 2026 15:42:49 +0330 Subject: [PATCH] feat: implement core matchmaking application structure and multi-language support --- AUTO_CLICK_SETUP_GUIDE.md | 2 +- src/app/[lang]/layout.tsx | 2 +- src/app/[lang]/test-webview/page.tsx | 2 +- src/app/candidate-contact/page.tsx | 8 +- src/app/finding-match/page.tsx | 6 +- src/app/intro/page.tsx | 6 +- src/app/layout.tsx | 4 +- src/app/new-match/page.tsx | 8 +- src/app/new-match/profile/page.tsx | 14 +- src/app/providers.tsx | 2 +- .../[slug]/answer-pace-sheet.tsx | 4 +- .../[slug]/question-detail-client.tsx | 20 +- .../[slug]/test-intro-client.tsx | 8 +- src/app/questions-list/page.tsx | 14 +- src/app/questions-list/sections-request.tsx | 4 +- src/app/request-accepted/page.tsx | 12 +- src/app/request-sent/page.tsx | 6 +- src/app/terms/page.tsx | 2 +- .../advisor-actions-card.tsx | 2 +- .../booking-terms-card.tsx | 0 src/components/{ui => Componentes}/button.tsx | 0 .../{ui => Componentes}/call-result-sheet.tsx | 0 .../conditional-questions.tsx | 0 .../dev-click-to-component.tsx | 0 .../dismiss-reason-sheet.tsx | 2 +- .../Componentes/explanation-ui-font.tsx | 29 ++ .../female-consent-sheet.tsx | 0 .../{utils => Componentes}/fixed-bottom.tsx | 0 .../habib_marriage_form.md | 0 .../{ui => Componentes}/help-modal.tsx | 2 +- .../info-progress-card.tsx | 0 .../{ui => Componentes}/information-sheet.tsx | 2 +- .../{ui => Componentes}/language-switcher.tsx | 0 .../{dev => Componentes}/locator-paths.ts | 0 .../{ui => Componentes}/navigation-button.tsx | 2 +- .../{ui => Componentes}/notice-box.tsx | 0 .../page-background.tsx | 0 src/components/Componentes/progress-helper.ts | 454 ++++++++++++++++++ .../question-answer-storage.tsx | 0 .../question-birthplace.tsx | 0 .../question-button.tsx | 0 .../question-card.tsx | 0 .../question-checkbox.tsx | 32 +- .../question-date.tsx | 0 .../question-dropdown.tsx | 168 ++++++- .../question-exit-navigation-button.tsx | 2 +- .../question-file.tsx | 0 .../question-number.tsx | 0 .../question-phone.tsx | 0 .../question-photo.tsx | 2 +- .../question-progress-tracker.tsx | 0 .../question-radio.tsx | 39 +- .../question-renderer.tsx | 33 +- .../question-section-flow.tsx | 4 +- .../Componentes/question-slider.tsx | 287 +++++++++++ .../question-snap-list.tsx | 0 .../question-text.tsx | 49 +- .../Componentes/question-textarea.tsx | 58 +++ src/components/Componentes/question-title.tsx | 96 ++++ .../report-actions-sheet.tsx | 2 +- .../required-steps-card.tsx | 2 +- .../{sliders => Componentes}/slider-page.tsx | 16 +- .../slider-slide-five.tsx | 2 +- .../slider-slide-four.tsx | 4 +- .../slider-slide-one.tsx | 4 +- .../slider-slide-three.tsx | 2 +- .../slider-slide-two.tsx | 2 +- .../{sliders => Componentes}/slider-slide.tsx | 0 .../{ui => Componentes}/sticky-header.tsx | 0 .../subscription-required-sheet.tsx | 2 +- .../test-intro-page.tsx | 2 +- .../test-loading-screen.tsx | 4 +- .../test-questions-flow.tsx | 8 +- .../{ui => Componentes}/token-switcher.tsx | 0 .../view-paddings-provider.tsx | 0 src/components/questions/progress-helper.ts | 228 --------- src/components/questions/question-slider.tsx | 138 ------ src/components/questions/question-title.tsx | 86 ---- .../utils/dev-click-to-component.tsx | 4 - src/data/question-data.ts | 60 +++ src/data/questions/en.json | 77 ++- src/data/questions/fa.json | 77 ++- src/translations/locales/ar.json | 14 +- src/translations/locales/az.json | 14 +- src/translations/locales/bn.json | 14 +- src/translations/locales/da.json | 14 +- src/translations/locales/de.json | 14 +- src/translations/locales/en.json | 14 +- src/translations/locales/es.json | 14 +- src/translations/locales/fa.json | 14 +- src/translations/locales/fr.json | 14 +- src/translations/locales/gu.json | 14 +- src/translations/locales/ha.json | 14 +- src/translations/locales/he.json | 14 +- src/translations/locales/hi.json | 14 +- src/translations/locales/id.json | 14 +- src/translations/locales/ks.json | 14 +- src/translations/locales/pt.json | 14 +- src/translations/locales/ru.json | 14 +- src/translations/locales/sw.json | 14 +- src/translations/locales/tg.json | 14 +- src/translations/locales/tr.json | 14 +- src/translations/locales/ul.json | 14 +- src/translations/locales/ur.json | 14 +- src/translations/locales/uz.json | 14 +- src/translations/locales/zh.json | 14 +- src/view-paddings.ts | 2 +- 107 files changed, 1710 insertions(+), 735 deletions(-) rename src/components/{ui => Componentes}/advisor-actions-card.tsx (97%) rename src/components/{questions => Componentes}/booking-terms-card.tsx (100%) rename src/components/{ui => Componentes}/button.tsx (100%) rename src/components/{ui => Componentes}/call-result-sheet.tsx (100%) rename src/components/{questions => Componentes}/conditional-questions.tsx (100%) rename src/components/{dev => Componentes}/dev-click-to-component.tsx (100%) rename src/components/{ui => Componentes}/dismiss-reason-sheet.tsx (99%) create mode 100644 src/components/Componentes/explanation-ui-font.tsx rename src/components/{ui => Componentes}/female-consent-sheet.tsx (100%) rename src/components/{utils => Componentes}/fixed-bottom.tsx (100%) rename src/components/{questions => Componentes}/habib_marriage_form.md (100%) rename src/components/{ui => Componentes}/help-modal.tsx (99%) rename src/components/{ui => Componentes}/info-progress-card.tsx (100%) rename src/components/{ui => Componentes}/information-sheet.tsx (99%) rename src/components/{ui => Componentes}/language-switcher.tsx (100%) rename src/components/{dev => Componentes}/locator-paths.ts (100%) rename src/components/{ui => Componentes}/navigation-button.tsx (98%) rename src/components/{ui => Componentes}/notice-box.tsx (100%) rename src/components/{utils => Componentes}/page-background.tsx (100%) create mode 100644 src/components/Componentes/progress-helper.ts rename src/components/{questions => Componentes}/question-answer-storage.tsx (100%) rename src/components/{questions => Componentes}/question-birthplace.tsx (100%) rename src/components/{questions => Componentes}/question-button.tsx (100%) rename src/components/{questions => Componentes}/question-card.tsx (100%) rename src/components/{questions => Componentes}/question-checkbox.tsx (73%) rename src/components/{questions => Componentes}/question-date.tsx (100%) rename src/components/{questions => Componentes}/question-dropdown.tsx (62%) rename src/components/{questions => Componentes}/question-exit-navigation-button.tsx (96%) rename src/components/{questions => Componentes}/question-file.tsx (100%) rename src/components/{questions => Componentes}/question-number.tsx (100%) rename src/components/{questions => Componentes}/question-phone.tsx (100%) rename src/components/{questions => Componentes}/question-photo.tsx (99%) rename src/components/{questions => Componentes}/question-progress-tracker.tsx (100%) rename src/components/{questions => Componentes}/question-radio.tsx (60%) rename src/components/{questions => Componentes}/question-renderer.tsx (80%) rename src/components/{questions => Componentes}/question-section-flow.tsx (97%) create mode 100644 src/components/Componentes/question-slider.tsx rename src/components/{questions => Componentes}/question-snap-list.tsx (100%) rename src/components/{questions => Componentes}/question-text.tsx (63%) create mode 100644 src/components/Componentes/question-textarea.tsx create mode 100644 src/components/Componentes/question-title.tsx rename src/components/{ui => Componentes}/report-actions-sheet.tsx (99%) rename src/components/{questions => Componentes}/required-steps-card.tsx (99%) rename src/components/{sliders => Componentes}/slider-page.tsx (94%) rename src/components/{sliders => Componentes}/slider-slide-five.tsx (94%) rename src/components/{sliders => Componentes}/slider-slide-four.tsx (97%) rename src/components/{sliders => Componentes}/slider-slide-one.tsx (97%) rename src/components/{sliders => Componentes}/slider-slide-three.tsx (98%) rename src/components/{sliders => Componentes}/slider-slide-two.tsx (96%) rename src/components/{sliders => Componentes}/slider-slide.tsx (100%) rename src/components/{ui => Componentes}/sticky-header.tsx (100%) rename src/components/{ui => Componentes}/subscription-required-sheet.tsx (97%) rename src/components/{questions => Componentes}/test-intro-page.tsx (98%) rename src/components/{questions => Componentes}/test-loading-screen.tsx (97%) rename src/components/{questions => Componentes}/test-questions-flow.tsx (96%) rename src/components/{ui => Componentes}/token-switcher.tsx (100%) rename src/components/{utils => Componentes}/view-paddings-provider.tsx (100%) delete mode 100644 src/components/questions/progress-helper.ts delete mode 100644 src/components/questions/question-slider.tsx delete mode 100644 src/components/questions/question-title.tsx delete mode 100644 src/components/utils/dev-click-to-component.tsx diff --git a/AUTO_CLICK_SETUP_GUIDE.md b/AUTO_CLICK_SETUP_GUIDE.md index 72ca0a4..4db7232 100644 --- a/AUTO_CLICK_SETUP_GUIDE.md +++ b/AUTO_CLICK_SETUP_GUIDE.md @@ -69,7 +69,7 @@ Next.js باید بتواند پلاگین ما را بخواند. یک فایل در نهایت، فایل اصلی پروژه خود (مثلاً `src/pages/_app.tsx` یا `src/app/layout.tsx`) را باز کنید و کامپوننت را در محیط توسعه (Development) فراخوانی کنید: ```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 }) { return ( diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx index 3929f2c..9ec5569 100644 --- a/src/app/[lang]/layout.tsx +++ b/src/app/[lang]/layout.tsx @@ -1,5 +1,5 @@ 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 { I18nProvider } from "@/translations/provider"; diff --git a/src/app/[lang]/test-webview/page.tsx b/src/app/[lang]/test-webview/page.tsx index f0ab110..34aab9c 100644 --- a/src/app/[lang]/test-webview/page.tsx +++ b/src/app/[lang]/test-webview/page.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import Button from "@/components/ui/button"; +import Button from "@/components/Componentes/button"; import { useFlutterBridge } from "@/hooks/useFlutterBridge"; export default function TestWebViewPage() { diff --git a/src/app/candidate-contact/page.tsx b/src/app/candidate-contact/page.tsx index 590cd9c..1605b6c 100644 --- a/src/app/candidate-contact/page.tsx +++ b/src/app/candidate-contact/page.tsx @@ -2,10 +2,10 @@ import Image from "next/image"; 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"; export default function CandidateContactPage() { diff --git a/src/app/finding-match/page.tsx b/src/app/finding-match/page.tsx index 12d6681..4ab2ab6 100644 --- a/src/app/finding-match/page.tsx +++ b/src/app/finding-match/page.tsx @@ -5,9 +5,9 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; 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 { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; diff --git a/src/app/intro/page.tsx b/src/app/intro/page.tsx index ae2f38c..bb20ef4 100644 --- a/src/app/intro/page.tsx +++ b/src/app/intro/page.tsx @@ -3,9 +3,9 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; 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 { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { authBridge } from "@/lib/auth-bridge"; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cf78adc..bdb7654 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,8 +4,8 @@ import localFont from "next/font/local"; import Script from "next/script"; import Providers from "./providers"; 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({ src: "../../public/fonts/Faminela/Faminela.otf", diff --git a/src/app/new-match/page.tsx b/src/app/new-match/page.tsx index 4c9b592..86d54f1 100644 --- a/src/app/new-match/page.tsx +++ b/src/app/new-match/page.tsx @@ -5,10 +5,10 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useMemo } from "react"; 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 { MarriageField, MarriageFieldValue, diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index 4d85ea6..1c40e85 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -3,13 +3,13 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; 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 { MarriageCaseStatus, MarriageField, diff --git a/src/app/providers.tsx b/src/app/providers.tsx index fdc2918..1c41755 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -6,7 +6,7 @@ import { useQueryClient, } from "@tanstack/react-query"; 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 }) { const queryClient = useQueryClient(); diff --git a/src/app/questions-list/[slug]/answer-pace-sheet.tsx b/src/app/questions-list/[slug]/answer-pace-sheet.tsx index c07c68d..4d93f9b 100644 --- a/src/app/questions-list/[slug]/answer-pace-sheet.tsx +++ b/src/app/questions-list/[slug]/answer-pace-sheet.tsx @@ -4,8 +4,8 @@ import { useEffect, useMemo, useState } from "react"; import { getQuestionAnswersStorageKey, 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 { MarriageField, MarriagePhoneFieldValue, diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index b28fd66..d21b484 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -6,18 +6,18 @@ import { hasQuestionAnswerValue, QuestionAnswersProvider, 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, { 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 { glasserFallbackQuestions } from "@/data/glasser-fallback"; import { diff --git a/src/app/questions-list/[slug]/test-intro-client.tsx b/src/app/questions-list/[slug]/test-intro-client.tsx index 70de7cc..c006a46 100644 --- a/src/app/questions-list/[slug]/test-intro-client.tsx +++ b/src/app/questions-list/[slug]/test-intro-client.tsx @@ -1,10 +1,10 @@ "use client"; 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 = { title: string; diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index 2288729..2ea3330 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -7,13 +7,13 @@ import { IoClose } from "react-icons/io5"; import { getLocalSectionProgress, 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 { getQuestionListItems, isQuestionListItemVisibleForProfile, diff --git a/src/app/questions-list/sections-request.tsx b/src/app/questions-list/sections-request.tsx index d588ae7..024bcbb 100644 --- a/src/app/questions-list/sections-request.tsx +++ b/src/app/questions-list/sections-request.tsx @@ -2,8 +2,8 @@ import { useEffect, useMemo, useState } from "react"; 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 { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; diff --git a/src/app/request-accepted/page.tsx b/src/app/request-accepted/page.tsx index 803667a..3e98344 100644 --- a/src/app/request-accepted/page.tsx +++ b/src/app/request-accepted/page.tsx @@ -5,12 +5,12 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; 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 { MarriageField, MarriagePhoneFieldValue, diff --git a/src/app/request-sent/page.tsx b/src/app/request-sent/page.tsx index 50683b7..4778812 100644 --- a/src/app/request-sent/page.tsx +++ b/src/app/request-sent/page.tsx @@ -4,9 +4,9 @@ import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/navigation"; 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 { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index 1bd8c75..d308264 100644 --- a/src/app/terms/page.tsx +++ b/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() { return ; diff --git a/src/components/ui/advisor-actions-card.tsx b/src/components/Componentes/advisor-actions-card.tsx similarity index 97% rename from src/components/ui/advisor-actions-card.tsx rename to src/components/Componentes/advisor-actions-card.tsx index 9834f83..23e42e3 100644 --- a/src/components/ui/advisor-actions-card.tsx +++ b/src/components/Componentes/advisor-actions-card.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import Button from "@/components/ui/button"; +import Button from "./button"; export type AdvisorAvatar = { id: string; diff --git a/src/components/questions/booking-terms-card.tsx b/src/components/Componentes/booking-terms-card.tsx similarity index 100% rename from src/components/questions/booking-terms-card.tsx rename to src/components/Componentes/booking-terms-card.tsx diff --git a/src/components/ui/button.tsx b/src/components/Componentes/button.tsx similarity index 100% rename from src/components/ui/button.tsx rename to src/components/Componentes/button.tsx diff --git a/src/components/ui/call-result-sheet.tsx b/src/components/Componentes/call-result-sheet.tsx similarity index 100% rename from src/components/ui/call-result-sheet.tsx rename to src/components/Componentes/call-result-sheet.tsx diff --git a/src/components/questions/conditional-questions.tsx b/src/components/Componentes/conditional-questions.tsx similarity index 100% rename from src/components/questions/conditional-questions.tsx rename to src/components/Componentes/conditional-questions.tsx diff --git a/src/components/dev/dev-click-to-component.tsx b/src/components/Componentes/dev-click-to-component.tsx similarity index 100% rename from src/components/dev/dev-click-to-component.tsx rename to src/components/Componentes/dev-click-to-component.tsx diff --git a/src/components/ui/dismiss-reason-sheet.tsx b/src/components/Componentes/dismiss-reason-sheet.tsx similarity index 99% rename from src/components/ui/dismiss-reason-sheet.tsx rename to src/components/Componentes/dismiss-reason-sheet.tsx index cb6bdad..d814cf0 100644 --- a/src/components/ui/dismiss-reason-sheet.tsx +++ b/src/components/Componentes/dismiss-reason-sheet.tsx @@ -2,7 +2,7 @@ import type { HTMLAttributes } from "react"; import { useEffect, useId, useState } from "react"; -import Button from "@/components/ui/button"; +import Button from "./button"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; diff --git a/src/components/Componentes/explanation-ui-font.tsx b/src/components/Componentes/explanation-ui-font.tsx new file mode 100644 index 0000000..5026307 --- /dev/null +++ b/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 ( + + {children} + + ); +} + +export default ExplanationUiFont; diff --git a/src/components/ui/female-consent-sheet.tsx b/src/components/Componentes/female-consent-sheet.tsx similarity index 100% rename from src/components/ui/female-consent-sheet.tsx rename to src/components/Componentes/female-consent-sheet.tsx diff --git a/src/components/utils/fixed-bottom.tsx b/src/components/Componentes/fixed-bottom.tsx similarity index 100% rename from src/components/utils/fixed-bottom.tsx rename to src/components/Componentes/fixed-bottom.tsx diff --git a/src/components/questions/habib_marriage_form.md b/src/components/Componentes/habib_marriage_form.md similarity index 100% rename from src/components/questions/habib_marriage_form.md rename to src/components/Componentes/habib_marriage_form.md diff --git a/src/components/ui/help-modal.tsx b/src/components/Componentes/help-modal.tsx similarity index 99% rename from src/components/ui/help-modal.tsx rename to src/components/Componentes/help-modal.tsx index 9ac4aed..9685fbe 100644 --- a/src/components/ui/help-modal.tsx +++ b/src/components/Componentes/help-modal.tsx @@ -3,7 +3,7 @@ import type { ReactNode } from "react"; import { useCallback, useEffect, useState } from "react"; import { createPortal } from "react-dom"; -import Button from "@/components/ui/button"; +import Button from "./button"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; diff --git a/src/components/ui/info-progress-card.tsx b/src/components/Componentes/info-progress-card.tsx similarity index 100% rename from src/components/ui/info-progress-card.tsx rename to src/components/Componentes/info-progress-card.tsx diff --git a/src/components/ui/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx similarity index 99% rename from src/components/ui/information-sheet.tsx rename to src/components/Componentes/information-sheet.tsx index 0cd06d1..22efa32 100644 --- a/src/components/ui/information-sheet.tsx +++ b/src/components/Componentes/information-sheet.tsx @@ -3,7 +3,7 @@ import Image, { type StaticImageData } from "next/image"; import type { HTMLAttributes, ReactNode } from "react"; import { useEffect, useState } from "react"; -import Button from "@/components/ui/button"; +import Button from "./button"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; diff --git a/src/components/ui/language-switcher.tsx b/src/components/Componentes/language-switcher.tsx similarity index 100% rename from src/components/ui/language-switcher.tsx rename to src/components/Componentes/language-switcher.tsx diff --git a/src/components/dev/locator-paths.ts b/src/components/Componentes/locator-paths.ts similarity index 100% rename from src/components/dev/locator-paths.ts rename to src/components/Componentes/locator-paths.ts diff --git a/src/components/ui/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx similarity index 98% rename from src/components/ui/navigation-button.tsx rename to src/components/Componentes/navigation-button.tsx index dbe9a91..f8749a4 100644 --- a/src/components/ui/navigation-button.tsx +++ b/src/components/Componentes/navigation-button.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/navigation"; import type { ButtonHTMLAttributes, ReactNode } from "react"; import { useState } from "react"; import { GoArrowLeft } from "react-icons/go"; -import HelpModal from "@/components/ui/help-modal"; +import HelpModal from "./help-modal"; import { useI18n } from "@/translations/provider"; type NavigationButtonIcon = diff --git a/src/components/ui/notice-box.tsx b/src/components/Componentes/notice-box.tsx similarity index 100% rename from src/components/ui/notice-box.tsx rename to src/components/Componentes/notice-box.tsx diff --git a/src/components/utils/page-background.tsx b/src/components/Componentes/page-background.tsx similarity index 100% rename from src/components/utils/page-background.tsx rename to src/components/Componentes/page-background.tsx diff --git a/src/components/Componentes/progress-helper.ts b/src/components/Componentes/progress-helper.ts new file mode 100644 index 0000000..6dbb435 --- /dev/null +++ b/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) => + 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) => + 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, + field: Record | 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[] = []; + 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; + } +} diff --git a/src/components/questions/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx similarity index 100% rename from src/components/questions/question-answer-storage.tsx rename to src/components/Componentes/question-answer-storage.tsx diff --git a/src/components/questions/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx similarity index 100% rename from src/components/questions/question-birthplace.tsx rename to src/components/Componentes/question-birthplace.tsx diff --git a/src/components/questions/question-button.tsx b/src/components/Componentes/question-button.tsx similarity index 100% rename from src/components/questions/question-button.tsx rename to src/components/Componentes/question-button.tsx diff --git a/src/components/questions/question-card.tsx b/src/components/Componentes/question-card.tsx similarity index 100% rename from src/components/questions/question-card.tsx rename to src/components/Componentes/question-card.tsx diff --git a/src/components/questions/question-checkbox.tsx b/src/components/Componentes/question-checkbox.tsx similarity index 73% rename from src/components/questions/question-checkbox.tsx rename to src/components/Componentes/question-checkbox.tsx index 72d4278..b3b14d4 100644 --- a/src/components/questions/question-checkbox.tsx +++ b/src/components/Componentes/question-checkbox.tsx @@ -87,9 +87,9 @@ export function QuestionCheckbox({ key={option} htmlFor={optionId} 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 - ? "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", isSelected ? "bg-[#F0445B] text-white shadow-[0_8px_20px_rgba(240,68,91,0.25)]" @@ -104,7 +104,33 @@ export function QuestionCheckbox({ onChange={() => toggleOption(option)} className="sr-only" /> - {option} + {!isShortOptions && ( +
+ {isSelected && ( + + + + )} +
+ )} + {option} ); })} diff --git a/src/components/questions/question-date.tsx b/src/components/Componentes/question-date.tsx similarity index 100% rename from src/components/questions/question-date.tsx rename to src/components/Componentes/question-date.tsx diff --git a/src/components/questions/question-dropdown.tsx b/src/components/Componentes/question-dropdown.tsx similarity index 62% rename from src/components/questions/question-dropdown.tsx rename to src/components/Componentes/question-dropdown.tsx index ead5f14..c63d6ce 100644 --- a/src/components/questions/question-dropdown.tsx +++ b/src/components/Componentes/question-dropdown.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; +import { ExplanationUiFont } from "./explanation-ui-font"; import { getCountryList } from "@/data/countries"; import { getLanguageList } from "@/data/languages"; import { getNationalityList } from "@/data/nationalities"; @@ -20,11 +21,14 @@ export function QuestionDropdown({ questionIndex, disabled, }: QuestionDropdownProps) { - const { locale } = useI18n(); + const { locale, dictionary: t } = useI18n(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); 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) ? rawValue : typeof rawValue === "string" && rawValue @@ -96,6 +100,23 @@ export function QuestionDropdown({ } }, [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 || []; const isNationalityDropdown = question.title === "Current Nationality / Citizenship" || @@ -124,27 +145,91 @@ export function QuestionDropdown({ 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) => 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 ? selectedList.length > 0 - ? selectedList.join(", ") + ? selectedList.map(getCleanLabel).join(", ") : question.extras.placeHolder || "Select" - : singleValue || question.extras.placeHolder || "Select"; + : singleValue + ? getCleanLabel(singleValue) + : question.extras.placeHolder || "Select"; const hasSelectedValue = isMulti ? selectedList.length > 0 : 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 isOtherOpt = isOtherQuestion && option === otherOptionName; let nextValue: string[]; - if (selectedList.includes(option)) { - nextValue = selectedList.filter((v) => v !== option); + + 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 { - nextValue = [...selectedList, option]; + if (selectedList.includes(option)) { + nextValue = selectedList.filter((v) => v !== option); + } else { + nextValue = [...selectedList, option]; + } } + setAnswerValue( question, 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 (
+ 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]" + /> +
+ ) : null} + {/* Dropdown Options Panel */} {isOpen && (
- {/* Search Input Bar (shown if options > 3 or search is active) */} - {options.length > 3 || searchQuery ? ( + {/* Search Input Bar */} + {showSearch ? (
)} - - {option} + + {option.includes(" - ") ? ( + (() => { + const parts = option.split(" - "); + const title = parts[0]; + const description = parts.slice(1).join(" - "); + return ( + + + {title} + + + {description} + + + ); + })() + ) : ( + {option} + )} ); diff --git a/src/components/questions/question-exit-navigation-button.tsx b/src/components/Componentes/question-exit-navigation-button.tsx similarity index 96% rename from src/components/questions/question-exit-navigation-button.tsx rename to src/components/Componentes/question-exit-navigation-button.tsx index 8a27d74..7449aa1 100644 --- a/src/components/questions/question-exit-navigation-button.tsx +++ b/src/components/Componentes/question-exit-navigation-button.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation"; import { useState } from "react"; import NavigationButton, { type NavigationButtonProps, -} from "@/components/ui/navigation-button"; +} from "./navigation-button"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; diff --git a/src/components/questions/question-file.tsx b/src/components/Componentes/question-file.tsx similarity index 100% rename from src/components/questions/question-file.tsx rename to src/components/Componentes/question-file.tsx diff --git a/src/components/questions/question-number.tsx b/src/components/Componentes/question-number.tsx similarity index 100% rename from src/components/questions/question-number.tsx rename to src/components/Componentes/question-number.tsx diff --git a/src/components/questions/question-phone.tsx b/src/components/Componentes/question-phone.tsx similarity index 100% rename from src/components/questions/question-phone.tsx rename to src/components/Componentes/question-phone.tsx diff --git a/src/components/questions/question-photo.tsx b/src/components/Componentes/question-photo.tsx similarity index 99% rename from src/components/questions/question-photo.tsx rename to src/components/Componentes/question-photo.tsx index ce8717a..7283710 100644 --- a/src/components/questions/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -170,7 +170,7 @@ export function QuestionPhoto({ ].join(" ")} > diff --git a/src/components/questions/question-progress-tracker.tsx b/src/components/Componentes/question-progress-tracker.tsx similarity index 100% rename from src/components/questions/question-progress-tracker.tsx rename to src/components/Componentes/question-progress-tracker.tsx diff --git a/src/components/questions/question-radio.tsx b/src/components/Componentes/question-radio.tsx similarity index 60% rename from src/components/questions/question-radio.tsx rename to src/components/Componentes/question-radio.tsx index 9ad92c8..02bca2a 100644 --- a/src/components/questions/question-radio.tsx +++ b/src/components/Componentes/question-radio.tsx @@ -1,5 +1,6 @@ "use client"; +import { ExplanationUiFont } from "./explanation-ui-font"; import type { QuestionField } from "@/data/question-data"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; @@ -52,9 +53,9 @@ export function QuestionRadio({ key={option} htmlFor={optionId} 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 - ? "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", isSelected ? "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)} className="sr-only" /> - {option} + {!isShortOptions && ( +
+ {isSelected && ( +
+ )} +
+ )} + {option.includes(" - ") + ? (() => { + const parts = option.split(" - "); + const title = parts[0]; + const description = parts.slice(1).join(" - "); + return ( + + {title} + + {description} + + + ); + })() + : {option}} ); })} diff --git a/src/components/questions/question-renderer.tsx b/src/components/Componentes/question-renderer.tsx similarity index 80% rename from src/components/questions/question-renderer.tsx rename to src/components/Componentes/question-renderer.tsx index 60a3fbb..8934dc5 100644 --- a/src/components/questions/question-renderer.tsx +++ b/src/components/Componentes/question-renderer.tsx @@ -1,18 +1,19 @@ "use client"; 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 = { question: QuestionField; @@ -152,6 +153,14 @@ export function QuestionRenderer({ disabled={disabled} /> ); + case "textarea": + return ( + + ); default: return null; } diff --git a/src/components/questions/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx similarity index 97% rename from src/components/questions/question-section-flow.tsx rename to src/components/Componentes/question-section-flow.tsx index 65746e2..e9de1f0 100644 --- a/src/components/questions/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -4,7 +4,7 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; import type { ReactNode } from "react"; import { useCallback, useState } from "react"; -import Button from "@/components/ui/button"; +import Button from "./button"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; @@ -13,7 +13,7 @@ import QuestionProgressTracker, { } from "./question-progress-tracker"; import QuestionSnapList from "./question-snap-list"; import type { QuestionField } from "@/data/question-data"; -import NoticeBox from "@/components/ui/notice-box"; +import NoticeBox from "./notice-box"; import { getStoredAge } from "./progress-helper"; type QuestionSectionFlowProps = { diff --git a/src/components/Componentes/question-slider.tsx b/src/components/Componentes/question-slider.tsx new file mode 100644 index 0000000..b0d8217 --- /dev/null +++ b/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(null); + const sliderRef = useRef(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 ( +
+ + + {/* First Slider (From Age) */} +
+
+ {t.questions.fromAge ?? "From"} + + {fromVal} + +
+
+ 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%)`, + }} + /> +
+
+ {steps.map((step) => { + const leftPct = ((step - min) / (max - min)) * 100; + return ( + + {step} + + ); + })} +
+
+ + {/* Second Slider (To Age) */} +
+
+ {t.questions.toAge ?? "To"} + + {toVal} + +
+
+ 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%)`, + }} + /> +
+
+ {steps.map((step) => { + const leftPct = ((step - min) / (max - min)) * 100; + return ( + + {step} + + ); + })} +
+
+
+ ); + } + + return ( +
+ +
+
+
+ {value} +
+ + 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%)`, + }} + /> +
+
+ {steps.map((step) => { + const leftPct = ((step - min) / (max - min)) * 100; + return ( + + {step} + + ); + })} +
+
+
+ ); +} + +export default QuestionSlider; diff --git a/src/components/questions/question-snap-list.tsx b/src/components/Componentes/question-snap-list.tsx similarity index 100% rename from src/components/questions/question-snap-list.tsx rename to src/components/Componentes/question-snap-list.tsx diff --git a/src/components/questions/question-text.tsx b/src/components/Componentes/question-text.tsx similarity index 63% rename from src/components/questions/question-text.tsx rename to src/components/Componentes/question-text.tsx index ef16319..63ed34f 100644 --- a/src/components/questions/question-text.tsx +++ b/src/components/Componentes/question-text.tsx @@ -1,6 +1,7 @@ "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"; @@ -17,11 +18,13 @@ export default function QuestionText({ questionIndex, description, disabled, - heightClassName, + heightClassName: _heightClassName, }: QuestionTextProps) { + const { dictionary: t } = useI18n(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); const value = getAnswerValue(question, questionIndex); + const isMuted = value === "-"; const stringValue = String(value ?? "").trim(); const isEmailQuestion = question.title.toLowerCase().includes("email") || @@ -36,6 +39,10 @@ export default function QuestionText({ ? isValidEmail || stringValue.length === 0 : isValidEmail && stringValue.length > 0; + const isMarjaQuestion = + question.title === "Marja' al-Taqlid (Religious Authority)" || + question.title === "مرجع تقلید"; + if (isEmailQuestion) { return (
setAnswerValue(question, questionIndex, e.target.value) } 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" />
{showInvalidState ? ( @@ -95,14 +102,42 @@ export default function QuestionText({ setAnswerValue(question, questionIndex, e.target.value) } 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 ? ( +
+ +
+ ) : null} {description ? ( {description} diff --git a/src/components/Componentes/question-textarea.tsx b/src/components/Componentes/question-textarea.tsx new file mode 100644 index 0000000..d2890e1 --- /dev/null +++ b/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 ( +
+ +