diff --git a/src/app/[lang]/questions-list/[slug]/page.tsx b/src/app/[lang]/questions-list/[slug]/page.tsx index 1c42417..7dc4bab 100644 --- a/src/app/[lang]/questions-list/[slug]/page.tsx +++ b/src/app/[lang]/questions-list/[slug]/page.tsx @@ -1,14 +1,3 @@ import QuestionDetailPage from "@/app/questions-list/[slug]/page"; -import { getQuestionListItems } from "@/data/question-data"; -import { locales } from "@/translations/config"; - -export function generateStaticParams() { - return locales.flatMap((lang) => - getQuestionListItems(lang).map((item) => ({ - lang, - slug: item.slug, - })), - ); -} export default QuestionDetailPage; diff --git a/src/app/questions-list/[slug]/page.tsx b/src/app/questions-list/[slug]/page.tsx index bf80a28..de7fc2f 100644 --- a/src/app/questions-list/[slug]/page.tsx +++ b/src/app/questions-list/[slug]/page.tsx @@ -1,23 +1,7 @@ -import { notFound } from "next/navigation"; - -import { - getQuestionListItemBySlug, - getQuestionListItems, -} from "@/data/question-data"; -import { defaultLocale, isLocale, locales } from "@/translations/config"; +import { defaultLocale, isLocale } from "@/translations/config"; import { getDictionary } from "@/translations/dictionaries"; import QuestionDetailClient from "./question-detail-client"; -export function generateStaticParams() { - const slugs = new Set( - locales.flatMap((locale) => - getQuestionListItems(locale).map((item) => item.slug), - ), - ); - - return Array.from(slugs).map((slug) => ({ slug })); -} - type QuestionDetailPageProps = { params: Promise<{ lang?: string; @@ -35,15 +19,10 @@ export default async function QuestionDetailPage({ ? `/${locale}/questions-list` : "/questions-list"; const t = getDictionary(locale); - const item = getQuestionListItemBySlug(slug, locale); - - if (!item) { - notFound(); - } return ( ({ + useRouter: vi.fn(() => ({ replace: vi.fn(), push: vi.fn() })), +})); + +vi.mock("@/translations/provider", () => ({ + useI18n: vi.fn(() => ({ locale: "en", dictionary: new Proxy({}, { get: (_, key) => key }) })), +})); + +vi.mock("@/hooks/marriage/use-cattell", () => ({ + useCattellQuestionsQuery: vi.fn(), + useSubmitCattellAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })), +})); + +vi.mock("@/hooks/marriage/use-glasser", () => ({ + useGlasserQuestionsQuery: vi.fn(), + useSubmitGlasserAssessmentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })), +})); + +vi.mock("@/hooks/marriage/use-profile-main", () => ({ + useMarriageProfileQuery: vi.fn(), +})); + +vi.mock("@/hooks/marriage/use-form-schema", () => ({ + useFormSchemaQuery: vi.fn(), +})); + +vi.mock("@/hooks/marriage/use-habcoin-payment", () => ({ + useHabcoinPaymentMutation: vi.fn(() => ({ mutateAsync: vi.fn() })), +})); + +vi.mock("@/lib/schema-adapter", () => ({ + convertSchemaToFrontendItems: vi.fn(), +})); + +describe("QuestionDetailClient Validation", () => { + const mockCattellRefetch = vi.fn(); + const mockGlasserRefetch = vi.fn(); + + afterEach(() => { + cleanup(); + }); + + beforeEach(() => { + vi.clearAllMocks(); + + (useMarriageProfileQuery as any).mockReturnValue({ + data: { age: 30, gender: "male" }, + isLoading: false, + }); + + (useFormSchemaQuery as any).mockReturnValue({ + data: {}, + isLoading: false, + }); + }); + + const setupTest = (slug: string, cattellData: any, glasserData: any) => { + // Mock schema adapter to return an item for the requested slug + (convertSchemaToFrontendItems as any).mockReturnValue([ + { + slug: slug, + title: "Test", + questions: [] + } + ]); + + (useCattellQuestionsQuery as any).mockReturnValue({ + data: cattellData, + isLoading: false, + isError: false, + refetch: mockCattellRefetch, + }); + + (useGlasserQuestionsQuery as any).mockReturnValue({ + data: glasserData, + isLoading: false, + isError: false, + refetch: mockGlasserRefetch, + }); + + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }); + + render( + + + + ); + + // Intro screen might render first; if Start is available, click it to mount questions flow + const startButton = screen.queryByText("Start"); + if (startButton) { + fireEvent.click(startButton); + } + }; + + it("should render correctly when Cattell API data is completely valid", () => { + setupTest("personality_test", { + questions: [ + { + question_number: 1, + text: "Valid Question Cattell", + options: [ + { id: "opt_a", label: "Opt1", value: "A" }, + { id: "opt_b", label: "Opt2", value: "B" }, + { id: "opt_c", label: "Opt3", value: "C" }, + ], + } + ] + }, null); + + // Retry UI should NOT be present + expect(screen.queryByText("Retry")).toBeNull(); + // Question text should be visible + expect(screen.getByText("Valid Question Cattell")).toBeDefined(); + }); + + it("should render Retry UI when Cattell API data is empty", () => { + setupTest("personality_test", { questions: [] }, null); + + expect(screen.getAllByText("No questions found for this test.")).toBeDefined(); + expect(screen.getAllByText("Retry")).toBeDefined(); + }); + + it("should render Retry UI when Cattell options are invalid (schema failure) and trigger refetch on Retry", async () => { + setupTest("personality_test", { + questions: [ + { + question_number: 1, + text: "Invalid Question", + options: [{ label: "Opt1", value: "A" }], // Invalid schema + } + ] + }, null); + + expect(screen.getAllByText("No questions found for this test.")).toBeDefined(); + const retryBtn = screen.getAllByText("Retry")[0]; + + fireEvent.click(retryBtn); + await waitFor(() => { + expect(mockCattellRefetch).toHaveBeenCalled(); + }); + }); + + it("should render correctly when Glasser API data is completely valid", () => { + setupTest("glasser_5_needs_test", null, { + questions: [ + { + question_number: 1, + text: "Valid Question Glasser", + factor_code: "SUR", + options: [ + { id: "o1", label: "O1", value: 1 }, + { id: "o2", label: "O2", value: 2 }, + { id: "o3", label: "O3", value: 3 }, + { id: "o4", label: "O4", value: 4 }, + { id: "o5", label: "O5", value: 5 }, + ], + } + ] + }); + + expect(screen.queryByText("Retry")).toBeNull(); + expect(screen.getByText("Valid Question Glasser")).toBeDefined(); + }); + + it("should render Retry UI when Glasser options are invalid (schema failure) and trigger refetch on Retry", async () => { + setupTest("glasser_5_needs_test", null, { + questions: [ + { + question_number: 1, + text: "Invalid Question Glasser", + factor_code: "SUR", + options: [ + { label: "O1", value: 1 }, + { label: "O2", value: 2 }, + { label: "O3", value: 3 }, + { label: "O4", value: 4 }, + ], + } + ] + }); + expect(screen.getAllByText("No questions found for this test.")).toBeDefined(); + const retryBtn = screen.getAllByText("Retry")[0]; + + fireEvent.click(retryBtn); + await waitFor(() => { + expect(mockGlasserRefetch).toHaveBeenCalled(); + }); + }); + + it("should render profile questions using ID-based data flow", () => { + (convertSchemaToFrontendItems as any).mockReturnValue([ + { + slug: "profile_test", + title: "Profile Form", + questions: [ + { + id: "q_123", + title: "Dynamic ID Question", + type: "text", + order: 1, + required: true, + isVisible: true, + private: false, + description: "", + tooltip: "", + extras: {}, + options: [] + }, + { + id: "q_456", + title: "Another ID Question", + type: "radio", + order: 2, + required: false, + isVisible: true, + private: false, + description: "", + tooltip: "", + extras: {}, + options: [ + { id: "opt_1", label: "Yes", value: "yes", order: 1 }, + { id: "opt_2", label: "No", value: "no", order: 2 } + ] + } + ] + } + ]); + + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + render( + + + + ); + + // Profile questions render directly, no start button + expect(screen.getByText(/Dynamic ID/)).toBeDefined(); + }); +}); diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx index 4d7976f..79f2d81 100644 --- a/src/app/questions-list/[slug]/question-detail-client.tsx +++ b/src/app/questions-list/[slug]/question-detail-client.tsx @@ -18,24 +18,16 @@ import TestIntroPage from "@/components/Componentes/test-intro-page"; import TestQuestionsFlow, { type TestQuestion, } from "@/components/Componentes/test-questions-flow"; -import { cattellFallbackQuestions } from "@/data/cattell-fallback"; -import { glasserFallbackQuestions } from "@/data/glasser-fallback"; import { - isQuestionListItemVisibleForProfile, - type QuestionField, -} from "@/data/question-data"; -import type { MarriageGender } from "@/hooks/marriage/types"; + useGlasserQuestionsQuery, + useSubmitGlasserAssessmentMutation, +} from "@/hooks/marriage/use-glasser"; import { useCattellQuestionsQuery, useSubmitCattellAssessmentMutation, } from "@/hooks/marriage/use-cattell"; -import { - useGlasserQuestionsQuery, - useSubmitGlasserAssessmentMutation, -} from "@/hooks/marriage/use-glasser"; -import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; -import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; +import { convertSchemaToFrontendItems, type QuestionField } from "@/lib/schema-adapter"; import { defaultLocale, type Locale } from "@/translations/config"; import { useI18n } from "@/translations/provider"; @@ -69,102 +61,16 @@ function getQuestionStorageKey(slug: string) { return `marriage:sections:${slug}:answers`; } -function parseStoredAge(value: unknown) { - if (typeof value === "number" && Number.isFinite(value)) { - return value; - } - - if (typeof value === "string") { - const trimmedValue = value.trim(); - - if (!trimmedValue) { - return null; - } - - const numericAge = Number(trimmedValue); - - if (Number.isFinite(numericAge)) { - return numericAge; - } - - const dateOfBirth = new Date(trimmedValue); - - if (Number.isNaN(dateOfBirth.getTime())) { - return null; - } - - const today = new Date(); - let age = today.getFullYear() - dateOfBirth.getFullYear(); - const hasBirthdayPassed = - today.getMonth() > dateOfBirth.getMonth() || - (today.getMonth() === dateOfBirth.getMonth() && - today.getDate() >= dateOfBirth.getDate()); - - if (!hasBirthdayPassed) { - age -= 1; - } - - return age >= 0 ? age : null; - } - - return null; -} - -function getStoredAge() { - try { - const rawValue = window.localStorage.getItem( - getQuestionStorageKey("personal_info"), - ); - - if (!rawValue) { - return null; - } - - const storedAnswers = JSON.parse(rawValue) as StoredAnswers; - const ageField = storedAnswers.fields?.find((field) => { - const f = field as { key?: string; type?: string; label?: string }; - return ( - f.type === "number" || - f.label === "Age" || - f.label === "ط³ظ†" || - (typeof f.key === "string" && - (f.key.endsWith("_age") || f.key.endsWith("_sn"))) - ); - }); - - if (ageField) { - return parseStoredAge(ageField.value); - } - - const dateOfBirthField = storedAnswers.fields?.find((field) => { - const f = field as { key?: string; type?: string; label?: string }; - return ( - 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"))) - ); - }); - - return parseStoredAge(dateOfBirthField?.value); - } catch { - return null; - } -} - function QuestionFlowWrapper({ visibleQuestions, itemSlug, dobQuestion, - dobQuestionIndex, continueLabel, questionsListHref, }: { visibleQuestions: QuestionField[]; itemSlug: string; dobQuestion?: QuestionField; - dobQuestionIndex?: number; requiredQuestionsCount: number; continueLabel: string; questionsListHref: string; @@ -191,22 +97,12 @@ function QuestionFlowWrapper({ questions={dynamicQuestions} > {dynamicQuestions.map((question, index) => { - let originalIndex = visibleQuestions.indexOf(question); - if (originalIndex === -1) { - originalIndex = visibleQuestions.findIndex( - (q) => - (q.englishTitle || q.title) === - (question.englishTitle || question.title), - ); - } - const answer = getAnswerValue(question, originalIndex); + const answer = getAnswerValue(question); const hasAnswer = hasQuestionAnswerValue(answer ?? null); let isAnswered = hasAnswer; if (hasAnswer) { - const isEmailQuestion = (question.englishTitle || question.title) - .toLowerCase() - .includes("email"); + const isEmailQuestion = question.type === "email" || question.validation?.format === "email"; if (isEmailQuestion) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; isAnswered = emailRegex.test(String(answer).trim()); @@ -222,19 +118,17 @@ function QuestionFlowWrapper({ return (
); @@ -280,11 +174,7 @@ export default function QuestionDetailClient({ } }, [itemSlug, isTestStarted]); - const { data: profile, isLoading: isProfileLoading } = - useMarriageProfileQuery(); - const profileGender = profile?.gender; - const age = getStoredAge(); - const { data: schema } = useFormSchemaQuery("profile", locale); + const { data: schema, isLoading: isSchemaLoading } = useFormSchemaQuery("profile", locale); const items = useMemo(() => convertSchemaToFrontendItems(schema, locale), [schema, locale]); const item = items.find((i) => i.slug === itemSlug); @@ -303,58 +193,44 @@ export default function QuestionDetailClient({ }); const submitGlasserMutation = useSubmitGlasserAssessmentMutation(); - const profileContext = useMemo( - () => ({ - age, - gender: profileGender as MarriageGender | null | undefined, - }), - [age, profileGender], - ); - const cattellTestQuestions: TestQuestion[] = useMemo(() => { - const questionsList = - cattellQuery.data?.questions && cattellQuery.data.questions.length > 0 - ? cattellQuery.data.questions - : cattellFallbackQuestions; - - return questionsList.map((q) => { - const rawOptions = - q.options && q.options.length > 0 - ? q.options - : locale === "fa" - ? ["ط¨ظ„ظ‡", "ط¨ظ‡ ط§ظ†ط¯ط§ط²ظ‡ ع©ط§ظپغŒ ظˆط§ط¶ط­ ظ†غŒط³طھ", "ظ†ظ‡"] - : ["Yes", "Not clear enough", "No"]; - const mappedOptions = rawOptions.map((optText, idx) => ({ - label: optText, - value: idx === 0 ? "A" : idx === 1 ? "B" : "C", - })); - return { - id: q.question_number, - text: q.text, - options: mappedOptions, - }; - }); - }, [cattellQuery.data, locale]); + const questionsList = cattellQuery.data?.questions || []; + + // Strict schema validation for Cattell + const isValidCattell = (q: any) => + q.question_number && + q.text && + q.options && + q.options.length === 3 && + q.options.every((o: any) => o.id && o.label && o.value); + + if (questionsList.length > 0 && !questionsList.every(isValidCattell)) { + console.error("Invalid Cattell API response schema"); + return []; + } + + return questionsList.map((q) => ({ + id: q.question_number, + text: q.text, + options: q.options || [], + })); + }, [cattellQuery.data]); const glasserTestQuestions: TestQuestion[] = useMemo(() => { - const questionsList = - glasserQuery.data?.questions && glasserQuery.data.questions.length > 0 - ? glasserQuery.data.questions - : glasserFallbackQuestions; - - const defaultGlasserOptions = [ - { - label: locale === "fa" ? "خیلی کم" : "Very Low", - value: 1, - }, - { label: locale === "fa" ? "کم" : "Low", value: 2 }, - { label: locale === "fa" ? "متوسط" : "Moderate", value: 3 }, - { label: locale === "fa" ? "زیاد" : "High", value: 4 }, - { - label: locale === "fa" ? "خیلی زیاد" : "Very High", - value: 5, - }, - ]; + const questionsList = glasserQuery.data?.questions || []; + + // Strict schema validation for Glasser + const isValidGlasser = (q: any) => + q.question_number && + q.text && + q.options && + q.options.length === 5 && + q.options.every((o: any) => o.id && o.label && typeof o.value === 'number' && o.value >= 1 && o.value <= 5); + + if (questionsList.length > 0 && !questionsList.every(isValidGlasser)) { + console.error("Invalid Glasser API response schema"); + return []; + } return questionsList.map((q) => ({ id: q.question_number, @@ -365,9 +241,9 @@ export default function QuestionDetailClient({ : "factor_code" in q ? (q.factor_code as string) : undefined, - options: defaultGlasserOptions, + options: q.options || [], })); - }, [glasserQuery.data, locale]); + }, [glasserQuery.data]); const visibleQuestions = useMemo(() => { if (!item) { @@ -388,28 +264,19 @@ export default function QuestionDetailClient({ ); useEffect(() => { - if (isProfileLoading) { - return; + if (!isSchemaLoading && !item) { + router.replace(questionsListHref); } + }, [isSchemaLoading, item, questionsListHref, router]); - if (!item || isQuestionListItemVisibleForProfile(item, profileContext)) { - return; - } - - router.replace(questionsListHref); - }, [isProfileLoading, item, profileContext, questionsListHref, router]); - - if (!profile && isProfileLoading && item) { + if (isSchemaLoading) { return ( ); - } else if ( - !item || - !isQuestionListItemVisibleForProfile(item, profileContext) - ) { + } else if (!item) { return null; } @@ -706,10 +573,7 @@ export default function QuestionDetailClient({ } const dobQuestion = visibleQuestions.find( - (question) => question.title === "Date of Birth", - ); - const dobQuestionIndex = visibleQuestions.findIndex( - (question) => question.title === "Date of Birth", + (question) => question.ui_config?.isDob === true || question.type === "date", ); return ( @@ -746,7 +610,6 @@ export default function QuestionDetailClient({ visibleQuestions={visibleQuestions} itemSlug={item.slug} dobQuestion={dobQuestion} - dobQuestionIndex={dobQuestionIndex} requiredQuestionsCount={requiredQuestionsCount} continueLabel={continueLabel} questionsListHref={questionsListHref} diff --git a/src/app/questions-list/page.tsx b/src/app/questions-list/page.tsx index 892c528..11bfa7e 100644 --- a/src/app/questions-list/page.tsx +++ b/src/app/questions-list/page.tsx @@ -5,15 +5,12 @@ import { useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { IoClose } from "react-icons/io5"; import { getSubmitPath } from "@/lib/get-submit-path"; -import { - getLocalSectionProgress, - getStoredAge, -} 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 { getQuestionAnswersStorageKey } from "@/components/Componentes/question-answer-storage"; import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end"; import { LoadingSkeleton } from "@/components/Componentes/loading-skeleton"; import { PageBackground } from "@/components/Componentes/page-background"; @@ -24,7 +21,7 @@ import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageSectionsQuery } from "@/hooks/marriage/use-sections"; import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; -import type { QuestionListItem } from "@/data/question-data"; +import type { QuestionListItem } from "@/lib/schema-adapter"; import { useCloseServiceOnBack } from "@/hooks/use-close-service-on-back"; import { clearMatchStartGrace, @@ -155,21 +152,11 @@ export default function QuestionsListPage() { setIsSyncError(false); try { - const slugsToCheck = [ - "personal_info", - "contact_residence_family_communication", - "appearance_health_activity", - "education_career_economic_status", - "family_marital_history", - "beliefs_lifestyle_boundaries", - "future_spouse_criteria", - "identity_verification", - ]; + const slugsToCheck = questionListItems.map(item => item.slug); for (const slug of slugsToCheck) { - const rawValue = window.localStorage.getItem( - `marriage:sections:${slug}:answers`, - ); + const storageKey = getQuestionAnswersStorageKey(slug); + const rawValue = window.localStorage.getItem(storageKey); if (rawValue) { const storedValue = JSON.parse(rawValue); if (storedValue.pending_sync && storedValue.fields) { @@ -184,7 +171,7 @@ export default function QuestionsListPage() { storedValue.pending_sync = false; window.localStorage.setItem( - `marriage:sections:${slug}:answers`, + storageKey, JSON.stringify(storedValue), ); } diff --git a/src/app/questions-list/sections-request.tsx b/src/app/questions-list/sections-request.tsx index d3e429f..7f7aa57 100644 --- a/src/app/questions-list/sections-request.tsx +++ b/src/app/questions-list/sections-request.tsx @@ -4,9 +4,13 @@ import { useEffect, useMemo, useState } from "react"; import { IoClose } from "react-icons/io5"; 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"; +const bookingTerms = [ + "All provided information is held in strict confidence.", + "Data is utilized exclusively to ensure optimal matchmaking accuracy.", +]; + const FIRST_ENTRY_TERMS_SEEN_KEY = "marriage:first-entry-terms-seen"; const FIRST_ENTRY_TERMS = [ diff --git a/src/components/Componentes/conditional-questions.tsx b/src/components/Componentes/conditional-questions.tsx deleted file mode 100644 index 88240b1..0000000 --- a/src/components/Componentes/conditional-questions.tsx +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { useQuestionAnswers } from "./question-answer-storage"; -import type { QuestionField } from "@/data/question-data"; -import QuestionRenderer from "./question-renderer"; - -type ConditionalQuestionsProps = { - parentQuestion: QuestionField; - parentQuestionIndex: number; - rules: { - values: string[]; - subQuestions: QuestionField[]; - }[]; - dobQuestion?: QuestionField; - dobQuestionIndex?: number; -}; - -export function ConditionalQuestions({ - parentQuestion, - parentQuestionIndex, - rules, - dobQuestion, - dobQuestionIndex, -}: ConditionalQuestionsProps) { - const { getAnswerValue } = useQuestionAnswers(); - const parentAnswer = getAnswerValue(parentQuestion, parentQuestionIndex); - - // Find if there is a matching rule for the current parent answer - const matchedRule = rules.find((rule) => { - return rule.values.includes(String(parentAnswer)); - }); - - if (!matchedRule) { - return ( - - ); - } - - return ( -
- -
- {matchedRule.subQuestions.map((subQuestion, index) => { - // Sub-question index is calculated uniquely to prevent conflicts - const subIndex = parentQuestionIndex * 100 + index + 1; - return ( -
- -
- ); - })} -
-
- ); -} - -export default ConditionalQuestions; diff --git a/src/components/Componentes/progress-helper.ts b/src/components/Componentes/progress-helper.ts deleted file mode 100644 index 1ab1b54..0000000 --- a/src/components/Componentes/progress-helper.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { type QuestionListItem } from "@/data/question-data"; -import type { MarriageGender } from "@/hooks/marriage/types"; - -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; -} - -export function getLocalSectionProgress( - item: QuestionListItem, - profile: { gender?: MarriageGender | null } | null | undefined, - age: number | null, -): number | null { - return null; -} diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index 2fbf591..3f3465d 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -11,8 +11,7 @@ import { useRef, useState, } from "react"; -import type { QuestionField } from "@/data/question-data"; -import { toBackendSlug } from "@/data/section-slug-map"; +import type { QuestionField } from "@/lib/schema-adapter"; import { pathParam } from "@/hooks/marriage/path-param"; import { marriageQueryKeys } from "@/hooks/marriage/query-keys"; import type { @@ -28,7 +27,7 @@ import { } from "@/hooks/marriage/use-section-data"; import { getApiRequestUrl } from "@/lib/http"; -const STORAGE_VERSION = 1; +const STORAGE_VERSION = 2; type QuestionAnswersByKey = Record; @@ -49,14 +48,12 @@ type QuestionAnswersContextValue = { flushAnswers: (options?: FlushAnswersOptions) => Promise; getAnswerValue: ( question: QuestionField, - questionIndex: number, ) => MarriageFieldValue | undefined; hasPendingSync: boolean; isSaving: boolean; isLoading: boolean; setAnswerValue: ( question: QuestionField, - questionIndex: number, value: MarriageFieldValue, ) => void; backendFields: MarriageField[]; @@ -71,37 +68,8 @@ type QuestionAnswersProviderProps = { const QuestionAnswersContext = createContext(null); -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 slugifyQuestionTitle(title: string) { - const slug = title - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - - return slug || `field_${hashString(title)}`; -} - -function getQuestionFieldKey(question: QuestionField, questionIndex: number) { - const index = - question.originalIndex !== undefined - ? question.originalIndex - : questionIndex; - return `q${index + 1}_${slugifyQuestionTitle(question.englishTitle || question.title)}`; -} - export function getQuestionAnswersStorageKey(slug: string) { - return `marriage:sections:${slug}:answers`; + return `marriage:sections:${slug}:answers:v${STORAGE_VERSION}`; } export function hasQuestionAnswerValue(value: MarriageFieldValue) { @@ -151,70 +119,26 @@ function isMarriagePhoneFieldValue( ); } -function findQuestionFieldKey( - question: QuestionField, - questionIndex: number, - answers?: QuestionAnswersByKey, - backendFields?: MarriageField[], -): string { - const legacyKey = getQuestionFieldKey(question, questionIndex); - - if (backendFields && backendFields.length > 0) { - const engSlug = slugifyQuestionTitle(question.englishTitle || question.title); - - // 1. Try to match by slug - const matchBySlug = backendFields.find((f) => - typeof f.key === "string" && (f.key.endsWith(`.${engSlug}`) || f.key.endsWith(`_${engSlug}`)) - ); - if (matchBySlug) return matchBySlug.key; - - // 2. Try to match by label - const matchByLabel = backendFields.find((f) => - f.label === question.title || f.label === question.englishTitle - ); - if (matchByLabel) return matchByLabel.key; - - // 3. Fallback to index - if (backendFields[questionIndex]) { - return backendFields[questionIndex].key; - } - } - - if (!answers) return legacyKey; - - const engSlug = slugifyQuestionTitle(question.englishTitle || question.title); - const foundEntry = Object.values(answers).find( - (f) => - f && - (f.key === legacyKey || - f.label === question.title || - f.label === question.englishTitle || - (typeof f.key === "string" && - (f.key.endsWith(`.${engSlug}`) || f.key.endsWith(`_${engSlug}`)))), - ); - return foundEntry?.key || legacyKey; -} function createQuestionField( question: QuestionField, - questionIndex: number, value: MarriageFieldValue, - currentAnswers?: QuestionAnswersByKey, - backendFields?: MarriageField[], ): MarriageField { - const backendId = (question as any).backendId; - const backendOptions = (question as any).backendOptions; - let option_id = undefined; - if (backendOptions && Array.isArray(backendOptions)) { - const selectedOpt = backendOptions.find((opt: any) => opt.label === value || opt.id === value || opt.value === value); - if (selectedOpt) { - option_id = selectedOpt.id; + + if (question.options && Array.isArray(question.options)) { + if (question.type === "checkbox" && Array.isArray(value)) { + option_id = value; + } else { + const selectedOpt = question.options.find((opt) => opt.id === value); + if (selectedOpt) { + option_id = selectedOpt.id; + } } } - const key = backendId || findQuestionFieldKey(question, questionIndex, currentAnswers, backendFields); + const key = question.id; return { key, @@ -234,8 +158,8 @@ function getOrderedFields( const orderedFields: MarriageField[] = []; const orderedKeys = new Set(); - questions.forEach((question, index) => { - const key = findQuestionFieldKey(question, index, answers, backendFields); + questions.forEach((question) => { + const key = question.id; const field = answers[key]; if (field) { @@ -258,16 +182,11 @@ function getCurrentStep( questions: readonly QuestionField[], backendFields?: MarriageField[], ) { - return questions.filter((question, index) => { - if (!question.required || question.logic?.dependsOn) { + return questions.filter((question) => { + if (!question.required || !question.isVisible) { return false; } - const key = findQuestionFieldKey( - question, - index, - fieldsToAnswers(fields), - backendFields - ); + const key = question.id; const field = fields.find((f) => f.key === key); return field && hasQuestionAnswerValue(field.value); }).length; @@ -280,7 +199,7 @@ function createPayload( ): UpdateMarriageSectionDataPayload { const fields = getOrderedFields(answers, questions, backendFields); const targetQuestions = questions.filter( - (q) => q.required && !q.logic?.dependsOn, + (q) => q.required && q.isVisible, ); return { @@ -292,7 +211,14 @@ function createPayload( function fieldsToAnswers(fields: MarriageField[]) { return fields.reduce((nextAnswers, field) => { - nextAnswers[field.key] = field; + if (field.option_id !== undefined && field.option_id !== null) { + nextAnswers[field.key] = { + ...field, + value: field.option_id, + }; + } else { + nextAnswers[field.key] = field; + } return nextAnswers; }, {}); } @@ -359,9 +285,8 @@ function writeStoredAnswers( } function getKeepalivePatchUrl(slug: string) { - const backendSlug = toBackendSlug(slug); return getApiRequestUrl( - `/api/marriage/sections/${pathParam(backendSlug)}/data/`, + `/api/marriage/forms/profile/answers/`, ); } @@ -466,29 +391,22 @@ export function QuestionAnswersProvider({ }, []); const getAnswerValue = useCallback( - (question: QuestionField, questionIndex: number) => { - const key = findQuestionFieldKey(question, questionIndex, answers, serverSectionData?.data || undefined); + (question: QuestionField) => { + const key = question.id; return answers[key]?.value; }, - [answers, serverSectionData?.data], + [answers], ); const setAnswerValue = useCallback( ( question: QuestionField, - questionIndex: number, value: MarriageFieldValue, ) => { if (!canEdit) { return; } - const field = createQuestionField( - question, - questionIndex, - value, - answersRef.current, - serverSectionData?.data || undefined, - ); + const field = createQuestionField(question, value); setAnswers((currentAnswers) => { const nextAnswers = { @@ -597,14 +515,7 @@ export function QuestionAnswersProvider({ return; } - // The combined family card must be split across two backend endpoints by - // updateMarriageSectionData. Sending its full payload to either endpoint - // would overwrite the other half of the profile. The local pending draft - // remains available and is retried on the next visit. - if ( - slugRef.current === "family_marital_history" || - flushPromiseRef.current - ) { + if (flushPromiseRef.current) { return; } @@ -624,12 +535,18 @@ export function QuestionAnswersProvider({ headers["X-CSRFToken"] = csrfToken; } + const answersPayload = payload.fields.map((f) => ({ + question_id: f.key, + value: f.value, + option_id: (f as any).option_id || undefined, + })); + fetch(getKeepalivePatchUrl(slugRef.current), { - body: JSON.stringify(payload), + body: JSON.stringify({ answers: answersPayload }), credentials: "include", headers, keepalive: true, - method: "PATCH", + method: "PUT", }) .then((response) => { if (!response.ok) { @@ -715,14 +632,13 @@ export function useQuestionAnswers() { export function useQuestionAnswer( question: QuestionField, - questionIndex: number, ) { const context = useContext(QuestionAnswersContext); return { setValue: (value: MarriageFieldValue) => { - context?.setAnswerValue(question, questionIndex, value); + context?.setAnswerValue(question, value); }, - value: context?.getAnswerValue(question, questionIndex), + value: context?.getAnswerValue(question), }; } diff --git a/src/components/Componentes/question-answer.test.tsx b/src/components/Componentes/question-answer.test.tsx new file mode 100644 index 0000000..fee2389 --- /dev/null +++ b/src/components/Componentes/question-answer.test.tsx @@ -0,0 +1,260 @@ +import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; +import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QuestionAnswersProvider, useQuestionAnswers } from "./question-answer-storage"; +import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; +import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; +import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { useMarriageSectionDataQuery, useUpdateMarriageSectionDataMutation } from "@/hooks/marriage/use-section-data"; + +vi.mock("@/hooks/marriage/use-form-schema", () => ({ + useFormSchemaQuery: vi.fn(), +})); +vi.mock("@/hooks/marriage/use-profile-main", () => ({ + useMarriageProfileQuery: vi.fn(), +})); +vi.mock("@/hooks/marriage/use-section-data", () => ({ + useMarriageSectionDataQuery: vi.fn(), + useUpdateMarriageSectionDataMutation: vi.fn(), +})); + +// Dummy component to interact with the context +function TestComponent({ slug }: { slug: string }) { + const { setAnswerValue, flushAnswers } = useQuestionAnswers(); + + return ( +
+ + + + + +
+ ); +} + +describe("Question Answer & Schema Integration", () => { + let capturedPayload: any = null; + + beforeEach(() => { + capturedPayload = null; + const updateMutateAsync = vi.fn(async (payload) => { + capturedPayload = payload; + return payload; + }); + (useUpdateMarriageSectionDataMutation as any).mockReturnValue({ + mutateAsync: updateMutateAsync, + isPending: false, + }); + (useMarriageProfileQuery as any).mockReturnValue({ + data: { can_edit_profile: true }, + }); + (useMarriageSectionDataQuery as any).mockReturnValue({ + data: [], + isLoading: false, + }); + (useFormSchemaQuery as any).mockReturnValue({ + data: { is_completed: false }, + isLoading: false, + isFetching: false, + }); + }); + + afterEach(() => { + cleanup(); + }); + + it("should send option_id instead of label/value for radio", async () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + render( + + + + + + ); + + fireEvent.click(screen.getByTestId("set-radio")); + fireEvent.click(screen.getByTestId("save")); + + await waitFor(() => { + expect(capturedPayload).not.toBeNull(); + const field = capturedPayload.fields.find((f: any) => f.key === "q1"); + expect(field.option_id).toBe("opt1"); + expect(field.value).toBe("opt1"); // ui value is option_id + }); + }); + + it("should send array of option_ids for checkbox", async () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + render( + + + + + + ); + + fireEvent.click(screen.getByTestId("set-checkbox")); + fireEvent.click(screen.getByTestId("save")); + + await waitFor(() => { + expect(capturedPayload).not.toBeNull(); + const field = capturedPayload.fields.find((f: any) => f.key === "q2"); + expect(field.option_id).toEqual(["opt2", "opt3"]); + }); + }); + + it("should sort schema questions and options by order correctly", () => { + const mockSchema = { + sections: [ + { + id: "sec1", + title: "Section", + order: 1, + icon: "user-circle", + is_required: true, + estimated_minutes: 5, + cards: [ + { + id: "card1", + title: "Card", + order: 2, + questions: [ + { id: "q1", title: "Q1", type: "text", order: 10, required: true, is_visible: true, ui_config: {}, options: [] }, + { id: "q2", title: "Q2", type: "text", order: 5, required: true, is_visible: true, ui_config: {}, options: [ + { id: "opt1", value: "A", label: "Option A", order: 2 }, + { id: "opt2", value: "B", label: "Option B", order: 1 } + ] }, + ] + }, + { + id: "card2", + title: "Card 2", + order: 1, + questions: [ + { id: "q3", title: "Q3", type: "text", order: 1, required: true, is_visible: true, ui_config: {}, options: [] }, + ] + } + ] + } + ], + progress: { sections_progress: {} } + } as any; + + const frontendItems = convertSchemaToFrontendItems(mockSchema, "en"); + const questions = frontendItems[0].questions; + + expect(questions[0].id).toBe("q3"); + expect(questions[1].id).toBe("q2"); + expect(questions[2].id).toBe("q1"); + + expect(questions[1].options[0].id).toBe("opt2"); + expect(questions[1].options[1].id).toBe("opt1"); + }); + + it("should hydrate radio/dropdown/checkbox with option_id instead of canonical value", async () => { + // Mock the backend sending canonical value but keeping option_id + (useMarriageSectionDataQuery as any).mockReturnValue({ + data: { + slug: "test_slug", + data: [ + { key: "q_radio", type: "radio", value: "Server Canonical Value", option_id: "opt_radio" }, + { key: "q_check", type: "checkbox", value: ["Server Val 1", "Server Val 2"], option_id: ["opt_check1", "opt_check2"] }, + ], + }, + isLoading: false, + }); + + let capturedValues: any = {}; + + function HydrationTestComponent() { + const { getAnswerValue } = useQuestionAnswers(); + capturedValues.radio = getAnswerValue({ id: "q_radio" } as any); + capturedValues.check = getAnswerValue({ id: "q_check" } as any); + return
; + } + + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + render( + + + + + + ); + + // Give it a moment to reconcile useEffect in QuestionAnswersProvider + await waitFor(() => { + expect(capturedValues.radio).toBe("opt_radio"); + expect(capturedValues.check).toEqual(["opt_check1", "opt_check2"]); + }); + }); + + it("should respect bq.is_required over bq.required for required state", () => { + const mockSchema = { + sections: [ + { + id: "sec1", + title: "Section", + order: 1, + icon: "user-circle", + is_required: true, + estimated_minutes: 5, + cards: [ + { + id: "card1", + title: "Card", + order: 1, + questions: [ + { id: "q1", title: "Q1", type: "text", order: 1, required: false, is_required: true, is_visible: true, ui_config: {}, options: [] }, + ] + } + ] + } + ], + progress: { sections_progress: {} } + } as any; + + const frontendItems = convertSchemaToFrontendItems(mockSchema, "en"); + const question = frontendItems[0].questions[0]; + + expect(question.baseRequired).toBe(false); + expect(question.required).toBe(true); + }); +}); diff --git a/src/components/Componentes/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx index dc0c42c..8c6337c 100644 --- a/src/components/Componentes/question-birthplace.tsx +++ b/src/components/Componentes/question-birthplace.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { getCountryList, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; @@ -10,7 +10,6 @@ import { LoadingThreeDot } from "./loading-three-dot"; type QuestionBirthplaceProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; @@ -78,12 +77,11 @@ function parseValue(rawValue: unknown): { country: string; city: string } { export function QuestionBirthplace({ question, - questionIndex, disabled, }: QuestionBirthplaceProps) { const { dictionary: t, locale } = useI18n(); const { getAnswerValue, setAnswerValue, isLoading } = useQuestionAnswers(); - const rawValue = getAnswerValue(question, questionIndex); + const rawValue = getAnswerValue(question); const initial = parseValue(rawValue); const [selectedCountry, setSelectedCountry] = useState(initial.country); @@ -95,9 +93,7 @@ export function QuestionBirthplace({ const listRef = useRef(null); const searchInputRef = useRef(null); - const isResidence = - question.title.toLowerCase().includes("residence") || - question.title.includes("سکونت"); + const isResidence = question.ui_config?.enable_geoip === true; const [mode, setMode] = useState<"auto" | "manual">("auto"); const [isDetecting, setIsDetecting] = useState(false); @@ -106,7 +102,7 @@ export function QuestionBirthplace({ const updateAnswers = (country: string, city: string) => { const formatted = city && country ? `${city}, ${country}` : city || country || null; - setAnswerValue(question, questionIndex, formatted); + setAnswerValue(question, formatted); }; // GeoIP detection logic diff --git a/src/components/Componentes/question-button.tsx b/src/components/Componentes/question-button.tsx index 93f7fe9..614444c 100644 --- a/src/components/Componentes/question-button.tsx +++ b/src/components/Componentes/question-button.tsx @@ -1,22 +1,20 @@ "use client"; import { IoInformation } from "react-icons/io5"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useQuestionAnswer } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionButtonProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; export function QuestionButton({ question, - questionIndex, disabled, }: QuestionButtonProps) { - const { setValue, value } = useQuestionAnswer(question, questionIndex); + const { setValue, value } = useQuestionAnswer(question); const isAnswered = value === true; return ( diff --git a/src/components/Componentes/question-card.tsx b/src/components/Componentes/question-card.tsx index 36435bb..ac04c1c 100644 --- a/src/components/Componentes/question-card.tsx +++ b/src/components/Componentes/question-card.tsx @@ -9,7 +9,7 @@ import { IoPerson, IoSchool, } from "react-icons/io5"; -import type { QuestionCardIcon, QuestionListItem } from "@/data/question-data"; +import type { QuestionCardIcon, QuestionListItem } from "@/lib/schema-adapter"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; diff --git a/src/components/Componentes/question-checkbox.tsx b/src/components/Componentes/question-checkbox.tsx index 98ae73f..5a784c6 100644 --- a/src/components/Componentes/question-checkbox.tsx +++ b/src/components/Componentes/question-checkbox.tsx @@ -1,23 +1,21 @@ "use client"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionCheckboxProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; export function QuestionCheckbox({ question, - questionIndex, disabled, }: QuestionCheckboxProps) { - const options = question.extras.options || []; + const options = question.options || []; const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const rawValue = getAnswerValue(question, questionIndex); + const rawValue = getAnswerValue(question); const value = Array.isArray(rawValue) ? rawValue @@ -29,40 +27,21 @@ export function QuestionCheckbox({ return null; } - const toggleOption = (option: string) => { + const toggleOption = (optionId: string) => { let nextValue: string[]; - if (value.includes(option)) { - nextValue = value.filter((v) => v !== option); + if (value.includes(optionId)) { + nextValue = value.filter((v) => v !== optionId); } else { - nextValue = [...value, option]; - } - - const exclusiveOption = options.find( - (o) => - o.includes("مهم نیست") || - o.includes("Doesn't matter") || - o.includes("No children") || - o.includes("فرزندی ندارم.") || - o.includes("مسئولیت مستمری ندارم") || - o.includes("do not have any ongoing responsibility"), - ); - if (exclusiveOption) { - if (option === exclusiveOption) { - nextValue = [exclusiveOption]; - } else if (nextValue.includes(exclusiveOption)) { - nextValue = nextValue.filter((v) => v !== exclusiveOption); - } + nextValue = [...value, optionId]; } setAnswerValue( - question, - questionIndex, - nextValue.length > 0 ? nextValue : null, + question, nextValue.length > 0 ? nextValue : null, ); }; const isShortOptions = - options.length <= 4 && options.every((opt) => opt.length <= 15); + options.length <= 4 && options.every((opt) => opt.label.length <= 15); return (
{options.map((option) => { - const optionId = `checkbox-${questionIndex}-${option}`; - const isSelected = value.includes(option); + const optionId = `checkbox-${question.id}-${option.id}`; + const isSelected = value.includes(option.id); return (
)} - {option} + {option.label} ); })} diff --git a/src/components/Componentes/question-date.tsx b/src/components/Componentes/question-date.tsx index 57ab4d9..07de95f 100644 --- a/src/components/Componentes/question-date.tsx +++ b/src/components/Componentes/question-date.tsx @@ -1,14 +1,13 @@ "use client"; import { useEffect, useMemo, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionDateProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; @@ -42,12 +41,11 @@ const YEARS = Array.from({ length: 80 }, (_, i) => export function QuestionDate({ question, - questionIndex, disabled, }: QuestionDateProps) { const { locale } = useI18n(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const value = getAnswerValue(question, questionIndex); + const value = getAnswerValue(question); const dateValue = typeof value === "string" ? value : ""; const [selectedYear, setSelectedYear] = useState(() => { @@ -113,12 +111,10 @@ export function QuestionDate({ const formattedMonth = m.padStart(2, "0"); const formattedDay = d.padStart(2, "0"); setAnswerValue( - question, - questionIndex, - `${y}-${formattedMonth}-${formattedDay}`, + question, `${y}-${formattedMonth}-${formattedDay}`, ); } else { - setAnswerValue(question, questionIndex, ""); + setAnswerValue(question, ""); } }; diff --git a/src/components/Componentes/question-dropdown.tsx b/src/components/Componentes/question-dropdown.tsx index bc4539a..7033bb6 100644 --- a/src/components/Componentes/question-dropdown.tsx +++ b/src/components/Componentes/question-dropdown.tsx @@ -2,33 +2,29 @@ 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"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionDropdownProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; export function QuestionDropdown({ question, - questionIndex, disabled, }: QuestionDropdownProps) { - const { locale, dictionary: t } = useI18n(); + const { dictionary: t } = useI18n(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const rawValue = getAnswerValue(question, questionIndex); + const rawValue = getAnswerValue(question); 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 @@ -100,154 +96,49 @@ 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["Other"] || "Other"; - - let options = question.extras.options || []; - const isNationalityDropdown = - question.title === "Current Nationality / Citizenship" || - question.title === "ملیت / تابعیت فعلی"; - - const isCountryDropdown = - !isNationalityDropdown && - (options.includes("United States") || - options.includes("ایالات متحده آمریکا") || - options.includes("Iran") || - options.includes("ایران")); - - if (isNationalityDropdown) { - options = getNationalityList(locale); - } else if (isCountryDropdown) { - options = getCountryList(locale); - } - - const isLanguageDropdown = - options.includes("Persian") || - options.includes("فارسی") || - options.includes("English") || - options.includes("انگلیسی"); - - if (isLanguageDropdown) { - options = getLanguageList(locale); - } - - if (isOtherQuestion) { - options = [...options, otherOptionName]; - } + const options = question.options || []; 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 - ); + if (question.extras?.noSearch) return false; + return options.length > 10; })(); const filteredOptions = options.filter((option) => - option.toLowerCase().includes(searchQuery.toLowerCase()), + option.label.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 getCleanLabel = (optId: string) => { + const opt = options.find((o) => o.id === optId); + if (!opt) return optId; + return opt.label.split(" - ")[0]; }; const displayLabel = isMulti ? selectedList.length > 0 ? selectedList.map(getCleanLabel).join(", ") - : question.extras.placeHolder || "Select" + : question.extras?.placeHolder || "Select" : singleValue ? getCleanLabel(singleValue) - : question.extras.placeHolder || "Select"; + : 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; + const toggleMultiOption = (optionId: 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]; - } + if (selectedList.includes(optionId)) { + nextValue = selectedList.filter((v) => v !== optionId); } else { - if (selectedList.includes(option)) { - nextValue = selectedList.filter((v) => v !== option); - } else { - nextValue = [...selectedList, option]; - } + nextValue = [...selectedList, optionId]; } setAnswerValue( - question, - questionIndex, - nextValue.length > 0 ? nextValue : null, + question, nextValue.length > 0 ? nextValue : null, ); }; - 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 (
- {/* RENDER TEXT FIELD IF OTHER IS SELECTED */} - {isOtherQuestion && isOtherSelected ? ( -
- handleOtherTextChange(e.target.value)} - placeholder={ - isPersonalityTraits - ? (t["Write other options..."] ?? "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 && (
@@ -383,20 +251,18 @@ export function QuestionDropdown({ {filteredOptions.length > 0 ? ( filteredOptions.map((option) => { const isSelected = isMulti - ? isOtherQuestion && option === otherOptionName - ? isOtherSelected - : selectedList.includes(option) - : singleValue === option; + ? selectedList.includes(option.id) + : singleValue === option.id; return ( diff --git a/src/components/Componentes/question-file.tsx b/src/components/Componentes/question-file.tsx index 61981c1..48e8d8d 100644 --- a/src/components/Componentes/question-file.tsx +++ b/src/components/Componentes/question-file.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { useCallback, useEffect, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; import { getApiRequestUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; @@ -12,7 +12,6 @@ import { LoadingSkeleton } from "./loading-skeleton"; type QuestionFileProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; @@ -59,11 +58,10 @@ function isImageFile( export function QuestionFile({ question, - questionIndex, disabled, }: QuestionFileProps) { const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const storedValue = getAnswerValue(question, questionIndex); + const storedValue = getAnswerValue(question); const initialFileName = typeof storedValue === "string" && storedValue.trim().length > 0 @@ -94,7 +92,7 @@ export function QuestionFile({ const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { if (response.path) { - setAnswerValue(question, questionIndex, response.path); + setAnswerValue(question, response.path); } }, onError: (error) => { @@ -120,7 +118,7 @@ export function QuestionFile({ const fileName = event.data.files[0].name ?? null; setSelectedFileName(fileName); if (fileName) { - setAnswerValue(question, questionIndex, fileName); + setAnswerValue(question, fileName); } } break; @@ -130,7 +128,7 @@ export function QuestionFile({ setIsFlutterPicking(false); const file = event.data?.files?.[0]; if (file?.url) { - setAnswerValue(question, questionIndex, file.url); + setAnswerValue(question, file.url); setSelectedFileName(file.name ?? "uploaded"); setFilePreviewUrl(file.url); } else if (file?.base64) { @@ -161,7 +159,7 @@ export function QuestionFile({ return () => { unsubscribe?.(); }; - }, [question, questionIndex, setAnswerValue, uploadTmpMediaMutation]); + }, [question, setAnswerValue, uploadTmpMediaMutation]); /** Handle file pick in Flutter WebView via upload_file action. */ const handleFlutterPick = useCallback(() => { @@ -190,12 +188,12 @@ export function QuestionFile({ if (!file) { setSelectedFileName(null); setFilePreviewUrl(null); - setAnswerValue(question, questionIndex, null); + setAnswerValue(question, null); return; } setSelectedFileName(file.name); - setAnswerValue(question, questionIndex, file.name); + setAnswerValue(question, file.name); if (file.type.startsWith("image/")) { const objectUrl = URL.createObjectURL(file); @@ -212,7 +210,7 @@ export function QuestionFile({ e.preventDefault(); setSelectedFileName(null); setFilePreviewUrl(null); - setAnswerValue(question, questionIndex, null); + setAnswerValue(question, null); }; const inWebView = isInFlutterWebView(); diff --git a/src/components/Componentes/question-number.tsx b/src/components/Componentes/question-number.tsx index 990f972..7270c84 100644 --- a/src/components/Componentes/question-number.tsx +++ b/src/components/Componentes/question-number.tsx @@ -1,14 +1,13 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionNumberProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; derivedFromQuestion?: QuestionField; derivedFromQuestionIndex?: number; @@ -18,32 +17,30 @@ const NUMBER_INPUT_PATTERN = /^-?\d*\.?\d*$/; export default function QuestionNumber({ question, - questionIndex, disabled, derivedFromQuestion, derivedFromQuestionIndex, }: QuestionNumberProps) { const { dictionary: t, locale } = useI18n(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const value = getAnswerValue(question, questionIndex); + const value = getAnswerValue(question); const derivedValue = derivedFromQuestion && derivedFromQuestionIndex !== undefined - ? getAnswerValue(derivedFromQuestion, derivedFromQuestionIndex) + ? getAnswerValue(derivedFromQuestion) : null; useEffect(() => { if (derivedFromQuestion && typeof derivedValue === "string") { const age = calculateAge(derivedValue); if (age !== String(value)) { - setAnswerValue(question, questionIndex, age); + setAnswerValue(question, age); } } }, [ derivedFromQuestion, derivedValue, question, - questionIndex, setAnswerValue, value, ]); @@ -54,9 +51,9 @@ export default function QuestionNumber({ value.length > 0 && !NUMBER_INPUT_PATTERN.test(value) ) { - setAnswerValue(question, questionIndex, null); + setAnswerValue(question, null); } - }, [question, questionIndex, setAnswerValue, value]); + }, [question, setAnswerValue, value]); const [min, max] = question.extras.range; @@ -78,15 +75,14 @@ export default function QuestionNumber({ ? rawInputValue : ""; - const isMonthlyIncome = - question.title === "Monthly Income" || - question.title === "میزان درآمد ماهانه"; + const isMonthlyIncome = question.ui_config?.currency_enabled === true; + const currencyStorageKey = question.ui_config?.currency_storage_key || "marriage:income:currency"; const countryName = useMemo(() => getCountryFromStorage(), []); const [currencyCode, setCurrencyCode] = useState(() => { if (typeof window !== "undefined") { - const stored = window.localStorage.getItem("marriage:income:currency"); + const stored = window.localStorage.getItem(currencyStorageKey); if (stored) return stored; } return getCurrencyForCountry(countryName); @@ -99,7 +95,7 @@ export default function QuestionNumber({ useEffect(() => { if (typeof window !== "undefined") { - const stored = window.localStorage.getItem("marriage:income:currency"); + const stored = window.localStorage.getItem(currencyStorageKey); if (stored) { setCurrencyCode(stored); return; @@ -215,13 +211,11 @@ export default function QuestionNumber({ setLocalTextValue(finalFormatted); if (cleanValue === "") { - setAnswerValue(question, questionIndex, null); + setAnswerValue(question, null); } else { const parsed = parseFloat(cleanValue); setAnswerValue( - question, - questionIndex, - Number.isNaN(parsed) ? cleanValue : parsed, + question, Number.isNaN(parsed) ? cleanValue : parsed, ); } }} @@ -330,7 +324,7 @@ export default function QuestionNumber({ setCurrencyCode(c.code); if (typeof window !== "undefined") { window.localStorage.setItem( - "marriage:income:currency", + currencyStorageKey, c.code, ); } @@ -398,13 +392,11 @@ export default function QuestionNumber({ } if (nextValue === "") { - setAnswerValue(question, questionIndex, null); + setAnswerValue(question, null); } else { const parsed = parseFloat(nextValue); setAnswerValue( - question, - questionIndex, - Number.isNaN(parsed) ? nextValue : parsed, + question, Number.isNaN(parsed) ? nextValue : parsed, ); } }} diff --git a/src/components/Componentes/question-phone.tsx b/src/components/Componentes/question-phone.tsx index ff5a91c..9642162 100644 --- a/src/components/Componentes/question-phone.tsx +++ b/src/components/Componentes/question-phone.tsx @@ -2,7 +2,7 @@ import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; import { useEffect, useRef, useState, useMemo, useCallback } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; @@ -11,7 +11,6 @@ import { useI18n } from "@/translations/provider"; type QuestionPhoneProps = { question: QuestionField; - questionIndex: number; countryCode?: string; disabled?: boolean; }; @@ -224,13 +223,12 @@ function getNormalizedPhoneValue(codeValue: string, phoneValue: string) { export function QuestionPhone({ question, - questionIndex, countryCode = "+44", disabled, }: QuestionPhoneProps) { const { locale } = useI18n(); const { getAnswerValue, setAnswerValue, isLoading } = useQuestionAnswers(); - const value = getAnswerValue(question, questionIndex); + const value = getAnswerValue(question); const defaultCodeValue = countryCode.trim() || "+44"; const getCachedOrSavedCode = useCallback((): string | null => { @@ -548,7 +546,7 @@ export function QuestionPhone({ : null; lastCommittedValueRef.current = nextValue; - setAnswerValue(question, questionIndex, nextValue); + setAnswerValue(question, nextValue); }; const handleSelectCountryCode = (selectedCode: string) => { diff --git a/src/components/Componentes/question-photo.tsx b/src/components/Componentes/question-photo.tsx index e0c2808..374b127 100644 --- a/src/components/Componentes/question-photo.tsx +++ b/src/components/Componentes/question-photo.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { type ReactNode, useCallback, useEffect, useId, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useUploadTmpMediaMutation } from "@/hooks/marriage/use-upload-tmp-media"; import { getApiRequestUrl } from "@/lib/http"; import { isInFlutterWebView, uploadFile } from "@/lib/webview-actions"; @@ -12,14 +12,12 @@ import { LoadingSkeleton } from "./loading-skeleton"; type QuestionPhotoProps = { question: QuestionField; - questionIndex: number; description?: ReactNode; disabled?: boolean; }; export function QuestionPhoto({ question, - questionIndex, description, disabled, }: QuestionPhotoProps) { @@ -34,12 +32,12 @@ export function QuestionPhoto({ const descriptionContent = description ?? question.description; const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const storedValue = getAnswerValue(question, questionIndex); + const storedValue = getAnswerValue(question); const uploadTmpMediaMutation = useUploadTmpMediaMutation({ onSuccess: (response) => { if (response.path) { - setAnswerValue(question, questionIndex, response.path); + setAnswerValue(question, response.path); } }, onError: (error) => { @@ -64,19 +62,19 @@ export function QuestionPhoto({ if (event.data?.files?.[0]?.base64) { const b64 = event.data.files[0].base64; setLocalPreviewUrl(b64); - setAnswerValue(question, questionIndex, b64); + setAnswerValue(question, b64); } break; case "completed": { setIsFlutterPicking(false); const file = event.data?.files?.[0]; if (file?.url) { - setAnswerValue(question, questionIndex, file.url); + setAnswerValue(question, file.url); setLocalPreviewUrl(file.url); } else if (file?.base64) { const b64 = file.base64; setLocalPreviewUrl(b64); - setAnswerValue(question, questionIndex, b64); + setAnswerValue(question, b64); fetch(b64) .then((res) => res.blob()) .then((blob) => { @@ -98,7 +96,7 @@ export function QuestionPhoto({ return () => { unsubscribe?.(); }; - }, [question, questionIndex, setAnswerValue, uploadTmpMediaMutation]); + }, [question, setAnswerValue, uploadTmpMediaMutation]); const handleFlutterPick = useCallback(() => { const extensions = (question.extras?.options ?? []).map((o) => @@ -126,7 +124,7 @@ export function QuestionPhoto({ // Create synchronous object URL for instant preview & immediate state update const objectUrl = URL.createObjectURL(file); setLocalPreviewUrl(objectUrl); - setAnswerValue(question, questionIndex, objectUrl); + setAnswerValue(question, objectUrl); // Trigger background upload uploadTmpMediaMutation.mutate(file); diff --git a/src/components/Componentes/question-radio.tsx b/src/components/Componentes/question-radio.tsx index 392b8d9..327e133 100644 --- a/src/components/Componentes/question-radio.tsx +++ b/src/components/Componentes/question-radio.tsx @@ -1,24 +1,22 @@ "use client"; import { ExplanationUiFont } from "./explanation-ui-font"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionRadioProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; }; export function QuestionRadio({ question, - questionIndex, disabled, }: QuestionRadioProps) { - const options = question.extras.options || []; + const options = question.options || []; const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const value = getAnswerValue(question, questionIndex); + const value = getAnswerValue(question); if (options.length === 0) { return null; @@ -26,7 +24,7 @@ export function QuestionRadio({ // Render horizontally if all options are short (e.g. Single, Divorced, Widowed) const isShortOptions = - options.length <= 4 && options.every((opt) => opt.length <= 15); + options.length <= 4 && options.every((opt) => opt.label.length <= 15); return (
{options.map((option) => { - const optionId = `question-${questionIndex}-${option}`; - const isSelected = String(value) === option; + const optionId = `question-${question.id}-${option.id}`; + const isSelected = String(value) === option.id; return (
)} - {option.includes(" - ") ? ( + {option.label.includes(" - ") ? ( (() => { - const parts = option.split(" - "); + const parts = option.label.split(" - "); const title = parts[0]; const description = parts.slice(1).join(" - "); return ( @@ -105,7 +103,7 @@ export function QuestionRadio({ ); })() ) : ( - {option} + {option.label} )} ); diff --git a/src/components/Componentes/question-renderer.tsx b/src/components/Componentes/question-renderer.tsx index 9b37965..f9843b1 100644 --- a/src/components/Componentes/question-renderer.tsx +++ b/src/components/Componentes/question-renderer.tsx @@ -1,6 +1,6 @@ "use client"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import QuestionBirthplace from "./question-birthplace"; import QuestionButton from "./question-button"; import QuestionCheckbox from "./question-checkbox"; @@ -17,22 +17,17 @@ import QuestionTextarea from "./question-textarea"; type QuestionRendererProps = { question: QuestionField; - questionIndex: number; disabled?: boolean; dobQuestion?: QuestionField; - dobQuestionIndex?: number; }; export function QuestionRenderer({ question, - questionIndex, disabled, dobQuestion, - dobQuestionIndex, }: QuestionRendererProps) { - const enTitle = (question.englishTitle || question.title).toLowerCase(); const compactTextHeight = - enTitle.includes("email") || enTitle.includes("duration") + question.type === "email" || question.ui_config?.compact === true ? "h-[54px]" : undefined; @@ -41,7 +36,6 @@ export function QuestionRenderer({ return ( ); @@ -49,7 +43,6 @@ export function QuestionRenderer({ return ( ); @@ -57,7 +50,6 @@ export function QuestionRenderer({ return ( ); @@ -65,7 +57,6 @@ export function QuestionRenderer({ return ( ); @@ -73,7 +64,6 @@ export function QuestionRenderer({ return ( ); @@ -81,22 +71,18 @@ export function QuestionRenderer({ return ( ); case "number": if ( - question.title === "Age" && - dobQuestion && - dobQuestionIndex !== undefined + question.ui_config?.derivedFromDob === true && + dobQuestion ) { return ( ); @@ -105,7 +91,6 @@ export function QuestionRenderer({ return ( ); @@ -113,7 +98,6 @@ export function QuestionRenderer({ return ( ); @@ -121,7 +105,6 @@ export function QuestionRenderer({ return ( ); @@ -129,7 +112,6 @@ export function QuestionRenderer({ return ( ); @@ -138,7 +120,6 @@ export function QuestionRenderer({ return ( ); @@ -146,7 +127,6 @@ export function QuestionRenderer({ return ( @@ -155,7 +135,6 @@ export function QuestionRenderer({ return ( ); diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx index db48d33..0561fa0 100644 --- a/src/components/Componentes/question-section-flow.tsx +++ b/src/components/Componentes/question-section-flow.tsx @@ -12,10 +12,8 @@ import QuestionProgressTracker, { useQuestionProgress, } from "./question-progress-tracker"; import QuestionSnapList from "./question-snap-list"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import NoticeBox from "./notice-box"; -import { getStoredAge } from "./progress-helper"; -import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import AnswerPaceSheet from "@/app/questions-list/[slug]/answer-pace-sheet"; import { FixToTheEnd } from "./fix-to-the-end"; import Button from "./button"; @@ -85,13 +83,9 @@ function SectionFlowContent({ [markQuestionPassed, optionalQuestionIndexes], ); - const { data: profile } = useMarriageProfileQuery(); - const isFemale = profile?.gender === "female"; - const age = getStoredAge(); const activeQuestion = questions?.[activeQuestionIndex]; const showNotice = activeQuestion?.showGuardianNotice; - const isUnder27 = - age !== null ? isFemale && age < 27 : (activeQuestion?.required ?? false); + const isUnder27 = activeQuestion?.required ?? false; return ( <> diff --git a/src/components/Componentes/question-slider.tsx b/src/components/Componentes/question-slider.tsx index 91c8240..2797c2a 100644 --- a/src/components/Componentes/question-slider.tsx +++ b/src/components/Componentes/question-slider.tsx @@ -1,27 +1,25 @@ "use client"; import { useLayoutEffect, useRef, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; 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 storedValue = getAnswerValue(question); const isDesiredAgeRange = false; @@ -115,12 +113,12 @@ export function QuestionSlider({ if (isDesiredAgeRange) { const handleFromChange = (newFrom: number) => { const val = Math.min(newFrom, toVal); - setAnswerValue(question, questionIndex, `${val}-${toVal}`); + setAnswerValue(question, `${val}-${toVal}`); }; const handleToChange = (newTo: number) => { const val = Math.max(newTo, fromVal); - setAnswerValue(question, questionIndex, `${fromVal}-${val}`); + setAnswerValue(question, `${fromVal}-${val}`); }; const progressFrom = @@ -246,9 +244,7 @@ export function QuestionSlider({ value={value} onChange={(event) => setAnswerValue( - question, - questionIndex, - Number(event.target.value), + question, Number(event.target.value), ) } disabled={disabled} diff --git a/src/components/Componentes/question-text.tsx b/src/components/Componentes/question-text.tsx index 1e19ea4..940198a 100644 --- a/src/components/Componentes/question-text.tsx +++ b/src/components/Componentes/question-text.tsx @@ -1,14 +1,13 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; type QuestionTextProps = { question: QuestionField; - questionIndex: number; description?: string; disabled?: boolean; heightClassName?: string; @@ -22,14 +21,13 @@ function toEnglishDigits(str: string): string { export default function QuestionText({ question, - questionIndex, description, disabled, heightClassName: _heightClassName, }: QuestionTextProps) { - const { dictionary: t } = useI18n(); + const { dictionary: t, locale } = useI18n(); const { getAnswerValue, setAnswerValue } = useQuestionAnswers(); - const value = getAnswerValue(question, questionIndex); + const value = getAnswerValue(question); const isMuted = value === "-"; const [localValue, setLocalValue] = useState( @@ -41,9 +39,7 @@ export default function QuestionText({ setLocalValue(isMuted ? "" : String(value ?? "")); }, [value, isMuted]); - const enTitleLower = (question.englishTitle || question.title).toLowerCase(); - const isNumericQuestion = - question.type === "number" || enTitleLower.includes("duration"); + const isNumericQuestion = question.type === "number" || question.validation?.format === "number"; const handleChange = (val: string) => { let nextVal = val; @@ -58,7 +54,7 @@ export default function QuestionText({ } debounceTimerRef.current = setTimeout(() => { - setAnswerValue(question, questionIndex, nextVal); + setAnswerValue(question, nextVal); }, 300); }; @@ -66,13 +62,11 @@ export default function QuestionText({ if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } - setAnswerValue(question, questionIndex, localValue); + setAnswerValue(question, localValue); }; const stringValue = localValue.trim(); - const isEmailQuestion = (question.englishTitle || question.title) - .toLowerCase() - .includes("email"); + const isEmailQuestion = question.type === "email" || question.validation?.format === "email"; const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const isValidEmail = !isEmailQuestion || emailRegex.test(stringValue); @@ -83,9 +77,7 @@ export default function QuestionText({ ? isValidEmail || stringValue.length === 0 : isValidEmail && stringValue.length > 0; - const isMarjaQuestion = - (question.englishTitle || question.title) === - "Marja' al-Taqlid (Religious Authority)"; + const isMarjaQuestion = question.ui_config?.showNotPriorityCheckbox === true; if (isEmailQuestion) { return ( @@ -119,9 +111,10 @@ export default function QuestionText({
{showInvalidState ? ( - {/[\u0600-\u06FF]/.test(question.title) - ? "یک آدرس ایمیل معتبر وارد کنید." - : "Enter a valid email address."} + {question.validation?.errorMessage || + (locale === "fa" + ? "یک آدرس ایمیل معتبر وارد کنید." + : "Enter a valid email address.")} ) : null} {description ? ( @@ -172,9 +165,9 @@ export default function QuestionText({ clearTimeout(debounceTimerRef.current); } if (e.target.checked) { - setAnswerValue(question, questionIndex, "-"); + setAnswerValue(question, "-"); } else { - setAnswerValue(question, questionIndex, ""); + setAnswerValue(question, ""); } }} className="h-[18px] w-[18px] shrink-0 rounded border-[#D0D5DD] text-[#F2465F] focus:ring-[#F2465F] accent-[#F2465F] cursor-pointer" diff --git a/src/components/Componentes/question-textarea.tsx b/src/components/Componentes/question-textarea.tsx index 0fc0dc0..3302e36 100644 --- a/src/components/Componentes/question-textarea.tsx +++ b/src/components/Componentes/question-textarea.tsx @@ -1,27 +1,25 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; 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 value = getAnswerValue(question); const [localValue, setLocalValue] = useState(String(value ?? "")); const debounceTimerRef = useRef(null); @@ -39,7 +37,7 @@ export function QuestionTextarea({ } debounceTimerRef.current = setTimeout(() => { - setAnswerValue(question, questionIndex, val); + setAnswerValue(question, val); }, 300); }; @@ -47,7 +45,7 @@ export function QuestionTextarea({ if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current); } - setAnswerValue(question, questionIndex, localValue); + setAnswerValue(question, localValue); }; const stringValue = localValue.trim(); diff --git a/src/components/Componentes/question-title.tsx b/src/components/Componentes/question-title.tsx index 2ad40b5..a157a86 100644 --- a/src/components/Componentes/question-title.tsx +++ b/src/components/Componentes/question-title.tsx @@ -3,9 +3,8 @@ import { useState } from "react"; import { IoEyeOff } from "react-icons/io5"; import HelpModal from "./help-modal"; -import type { QuestionField } from "@/data/question-data"; +import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; -import pathMap from "@/translations/path_to_english.json"; type QuestionTitleProps = { question: QuestionField; @@ -16,32 +15,18 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) { const { dictionary: t, locale } = useI18n(); const [isHelpOpen, setIsHelpOpen] = useState(false); - const isMonthlyIncome = - question.title === "Monthly Income" || - question.title === "میزان درآمد ماهانه"; + let suffix = ""; + if (question.ui_config?.suffix) { + suffix = ` ${question.ui_config.suffix}`; + } else if (question.ui_config?.suffix_i18n) { + suffix = ` ${question.ui_config.suffix_i18n[locale] || question.ui_config.suffix_i18n.en || ""}`; + } else if (question.ui_config?.showApproximateSuffix) { + suffix = locale === "fa" ? " (تقریبی)" : " (approximately)"; + } else if (question.ui_config?.showYearsSuffix) { + suffix = locale === "fa" ? " (سال)" : " (Years)"; + } - const isPreviousMarriageDuration = - question.title === "Previous Marriage Duration" || - question.title === "مدت ازدواج یا عقد قبلی" || - (question.englishTitle || question.title) - .toLowerCase() - .includes("marriage duration"); - - const alreadyHasUnit = - question.title.includes("Years") || - question.title.includes("سال"); - - const titleText = - question.title + - (isMonthlyIncome - ? locale === "fa" - ? " (تقریبی)" - : " (approximately)" - : isPreviousMarriageDuration && !alreadyHasUnit - ? locale === "fa" - ? " (سال)" - : " (Years)" - : ""); + const titleText = question.title + suffix; const words = titleText.split(" "); const lastWord = words[words.length - 1]; const remainingTitle = words.slice(0, -1).join(" "); @@ -93,15 +78,7 @@ export function QuestionTitle({ question, className }: QuestionTitleProps) { setIsHelpOpen(false)} - description={(() => { - const fullPath = "questions." + question.tooltip; - const englishVal = (pathMap as Record)[ - fullPath - ]; - return englishVal - ? (t as any)[englishVal] || englishVal - : question.tooltip; - })()} + description={question.tooltip} /> ) : null} diff --git a/src/components/Componentes/required-steps-card.tsx b/src/components/Componentes/required-steps-card.tsx index f943252..c03fc4c 100644 --- a/src/components/Componentes/required-steps-card.tsx +++ b/src/components/Componentes/required-steps-card.tsx @@ -2,11 +2,10 @@ import { useMemo } from "react"; import { IoAlert, IoCheckmark } from "react-icons/io5"; -import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; -import type { QuestionListItem } from "@/data/question-data"; +import type { QuestionListItem } from "@/lib/schema-adapter"; type RequiredStepsCardProps = { items?: QuestionListItem[]; @@ -34,7 +33,6 @@ export default function RequiredStepsCard({ progressBySlug, }: RequiredStepsCardProps = {}) { const { dictionary: t, locale } = useI18n(); - const { data: profile } = useMarriageProfileQuery(); const { data: schema } = useFormSchemaQuery("profile", locale); const questionListItems = useMemo( diff --git a/src/components/Componentes/schema-question-flow.integration.test.tsx b/src/components/Componentes/schema-question-flow.integration.test.tsx new file mode 100644 index 0000000..fd9b3bb --- /dev/null +++ b/src/components/Componentes/schema-question-flow.integration.test.tsx @@ -0,0 +1,235 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor, cleanup } from "@testing-library/react"; +import { describe, it, expect, afterEach, beforeEach, vi } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QuestionAnswersProvider, useQuestionAnswers } from "./question-answer-storage"; +import { convertSchemaToFrontendItems } from "@/lib/schema-adapter"; +import { useFormSchemaQuery } from "@/hooks/marriage/use-form-schema"; +import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { useMarriageSectionDataQuery, useUpdateMarriageSectionDataMutation } from "@/hooks/marriage/use-section-data"; +import { QuestionRadio } from "./question-radio"; +import { QuestionCheckbox } from "./question-checkbox"; + +import QuestionDetailClient from "@/app/questions-list/[slug]/question-detail-client"; + +vi.mock("@/hooks/marriage/use-form-schema", () => ({ + useFormSchemaQuery: vi.fn(), +})); +vi.mock("@/hooks/marriage/use-profile-main", () => ({ + useMarriageProfileQuery: vi.fn(), +})); +vi.mock("@/hooks/marriage/use-section-data", () => ({ + useMarriageSectionDataQuery: vi.fn(), + useUpdateMarriageSectionDataMutation: vi.fn(), +})); + +export const replaceMock = vi.fn(); +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: (...args: any[]) => replaceMock(...args), + back: vi.fn(), + }), +})); + +const mockSchema = { + form_id: "profile", + version: 1, + answers: { + "q_radio": { value: "A", option_id: "opt_a" }, + "q_check": { value: ["B", "C"], option_id: ["opt_b", "opt_c"] } + }, + sections: [ + { + id: "sec1", + title: "Section", + order: 1, + icon: "user-circle", + is_required: true, + estimated_minutes: 5, + cards: [ + { + id: "card1", + title: "Card", + order: 2, // Messed up order + questions: [ + { + id: "q_child", title: "Child Q", type: "text", order: 20, + required: false, is_required: true, is_visible: false, + ui_config: {}, options: [] + }, + { + id: "q_radio", title: "Radio Q", type: "radio", order: 5, + required: true, is_required: true, is_visible: true, + ui_config: {}, + options: [ + { id: "opt_a_dummy", value: "A_DUMMY", label: "Option A Dummy", order: 2 }, + { id: "opt_a", value: "A", label: "Option A Canonical", order: 1 } + ] + }, + { + id: "q_check", title: "Check Q", type: "checkbox", order: 10, + required: false, is_required: false, is_visible: true, + ui_config: {}, + options: [ + { id: "opt_c", value: "C", label: "Option C", order: 2 }, + { id: "opt_b", value: "B", label: "Option B", order: 1 } + ] + } + ] + }, + { + id: "card2", + title: "Card 2", + order: 1, // Card 2 should come first + questions: [ + { id: "q_first", title: "First Q", type: "text", order: 1, required: true, is_visible: true, ui_config: {}, options: [] }, + ] + } + ] + } + ], + progress: { sections_progress: {} } +}; + +describe("Schema Question Flow Integration", () => { + let capturedPayload: any = null; + + beforeEach(() => { + capturedPayload = null; + const updateMutateAsync = vi.fn(async (payload) => { + capturedPayload = payload; + return payload; + }); + (useUpdateMarriageSectionDataMutation as any).mockReturnValue({ + mutateAsync: updateMutateAsync, + isPending: false, + }); + (useMarriageProfileQuery as any).mockReturnValue({ + data: { can_edit_profile: true }, + }); + (useMarriageSectionDataQuery as any).mockReturnValue({ + data: { + slug: "sec1", + data: [ + { key: "q_radio", type: "radio", value: "A", option_id: "opt_a" }, + { key: "q_check", type: "checkbox", value: ["B", "C"], option_id: ["opt_b", "opt_c"] } + ] + }, + isLoading: false, + }); + (useFormSchemaQuery as any).mockReturnValue({ + data: mockSchema, + isLoading: false, + isFetching: false, + error: null, + refetch: vi.fn(), + }); + }); + + afterEach(() => { + cleanup(); + }); + + it("should enforce ordering, correctly hydrate canonical values via options, and hide invisible children", async () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + // Use convertSchemaToFrontendItems directly (no mock) + const frontendItems = convertSchemaToFrontendItems(mockSchema, "en"); + + // Sort logic validation + expect(frontendItems[0].questions[0].id).toBe("q_first"); + expect(frontendItems[0].questions[1].id).toBe("q_radio"); + expect(frontendItems[0].questions[2].id).toBe("q_check"); + expect(frontendItems[0].questions[3].id).toBe("q_child"); + + // Validate options sort + expect(frontendItems[0].questions[1].options[0].id).toBe("opt_a"); + + render( + + + + ); + + // Wait for hydration and rendering + await waitFor(() => { + // Radio hydration + const radioInput = screen.getByLabelText("Option A Canonical") as HTMLInputElement; + expect(radioInput.checked).toBe(true); + + // Checkbox hydration + const checkB = screen.getByLabelText("Option B") as HTMLInputElement; + const checkC = screen.getByLabelText("Option C") as HTMLInputElement; + expect(checkB.checked).toBe(true); + expect(checkC.checked).toBe(true); + + // Child should not be rendered + expect(screen.queryByText("Child Q")).toBeNull(); + + // q_child is required=false but is_required=true, if it was visible it should show *. + // Let's modify schema dynamically to test child visibility and requirement + }); + + // Interact to fire payload + const radioDummy = screen.getByLabelText("Option A Dummy") as HTMLInputElement; + fireEvent.click(radioDummy); + + // Uncheck Option B and Check Option C (Wait, they are both checked by default from hydration) + const checkB = screen.getByLabelText("Option B") as HTMLInputElement; + fireEvent.click(checkB); // Should now be unchecked + + const submitBtn = screen.getByText("Continue"); // Form submit button + fireEvent.click(submitBtn); + + await waitFor(() => { + expect(capturedPayload).not.toBeNull(); + const radioPayload = capturedPayload.fields.find((f: any) => f.key === "q_radio"); + expect(radioPayload.option_id).toBe("opt_a_dummy"); + + const checkPayload = capturedPayload.fields.find((f: any) => f.key === "q_check"); + expect(checkPayload.option_id).toEqual(["opt_c"]); // Only C is checked now + }); + }); + + it("should redirect and not render static questions on schema failure", () => { + replaceMock.mockClear(); + + (useFormSchemaQuery as any).mockReturnValue({ + data: undefined, + isLoading: false, + isFetching: false, + error: new Error("Network Error"), + refetch: vi.fn(), + }); + + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + render( + + + + ); + + // Should redirect to questions list + expect(replaceMock).toHaveBeenCalledWith("/list"); + // Static text should not exist + expect(screen.queryByText("First Q")).toBeNull(); + }); +}); diff --git a/src/components/Componentes/test-questions-flow.tsx b/src/components/Componentes/test-questions-flow.tsx index bcafe20..a00ccb0 100644 --- a/src/components/Componentes/test-questions-flow.tsx +++ b/src/components/Componentes/test-questions-flow.tsx @@ -13,6 +13,7 @@ import StickyHeader from "./sticky-header"; import TestLoadingScreen from "./test-loading-screen"; export type QuestionOption = { + id: string; label: string; value: string | number; }; diff --git a/src/components/Componentes/ui-config.test.tsx b/src/components/Componentes/ui-config.test.tsx new file mode 100644 index 0000000..3dbe8e2 --- /dev/null +++ b/src/components/Componentes/ui-config.test.tsx @@ -0,0 +1,114 @@ +import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react"; +import { describe, it, expect, vi, afterEach } from "vitest"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { QuestionAnswersProvider } from "./question-answer-storage"; +import { QuestionBirthplace } from "./question-birthplace"; +import QuestionNumber from "./question-number"; +import QuestionText from "./question-text"; + +vi.mock("@/translations/provider", () => ({ + useI18n: vi.fn(() => ({ locale: "fa", dictionary: {} })), +})); + +describe("UI Config based behavior", () => { + afterEach(() => { + cleanup(); + }); + + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + it("should trigger GeoIP only when ui_config.enable_geoip is true", () => { + // With totally random title but ui_config.enable_geoip = true + const qWithGeo = { id: "q1", title: "Random Title Here", type: "birthplace", order: 1, required: true, isVisible: true, description: "", tooltip: "", extras: {}, options: [], ui_config: { enable_geoip: true } } as any; + + const { rerender } = render( + + + + + + ); + + // Auto button is present when GeoIP is active + expect(screen.getByText("خودکار")).toBeDefined(); + + // With title "residence" but no ui_config + const qWithoutGeo = { id: "q2", title: "residence test", type: "birthplace", order: 1, required: true, isVisible: true, description: "", tooltip: "", extras: {}, options: [], ui_config: {} } as any; + + rerender( + + + + + + ); + + expect(screen.queryByText("خودکار")).toBeNull(); + }); + + it("should trigger currency behavior only when ui_config.currency_enabled is true", () => { + // Title is random, but currency_enabled is true + const qWithCurrency = { id: "q1", title: "Random Income", type: "number", order: 1, required: true, isVisible: true, description: "", tooltip: "", extras: { placeHolder: "", range: [0, 0] }, options: [], ui_config: { currency_enabled: true } } as any; + + const { rerender } = render( + + + + + + ); + + // Dropdown arrow should be rendered in currency mode (path is in SVG) + expect(screen.getByRole("img", { name: "Dropdown chevron" })).toBeDefined(); + + // Title is "Monthly Income", but currency_enabled is false + const qWithoutCurrency = { id: "q2", title: "Monthly Income", type: "number", order: 1, required: true, isVisible: true, description: "", tooltip: "", extras: { placeHolder: "", range: [0, 0] }, options: [], ui_config: {} } as any; + + rerender( + + + + + + ); + + expect(screen.queryByRole("img", { name: "Dropdown chevron" })).toBeNull(); + }); + + it("should render error message from validation.errorMessage or fallback to locale", async () => { + // Title is random, type is email, custom error message + const qCustomError = { id: "q1", title: "Random Email", type: "email", order: 1, required: true, isVisible: true, description: "", tooltip: "", extras: { placeHolder: "" }, options: [], validation: { errorMessage: "Custom Backend Error" } } as any; + + const { rerender } = render( + + + + + + ); + + const input = screen.getByRole("textbox"); + fireEvent.change(input, { target: { value: 'invalid_email' } }); + + await waitFor(() => { + expect(screen.getByText("Custom Backend Error")).toBeDefined(); + }); + + const qFallbackError = { id: "q2", title: "Random Email", type: "email", order: 1, required: true, isVisible: true, description: "", tooltip: "", extras: { placeHolder: "" }, options: [], validation: {} } as any; + + rerender( + + + + + + ); + + const input2 = screen.getByRole("textbox"); + fireEvent.change(input2, { target: { value: 'invalid_email_again' } }); + + await waitFor(() => { + expect(screen.getByText("یک آدرس ایمیل معتبر وارد کنید.")).toBeDefined(); + }); + }); +}); diff --git a/src/data/cattell-fallback.ts b/src/data/cattell-fallback.ts deleted file mode 100644 index ede0fff..0000000 --- a/src/data/cattell-fallback.ts +++ /dev/null @@ -1,1067 +0,0 @@ -export type CattellFallbackQuestion = { - question_number: number; - text: string; - options: string[]; -}; - -export const cattellFallbackQuestions: CattellFallbackQuestion[] = [ - { - question_number: 1, - text: "من حاضرم به هر سوال تا حد امکان صادقانه پاسخ دهم", - options: ["بله", "به اندازه کافی واضح نیست", "نه"], - }, - { - question_number: 2, - text: "ترجیح میدم خونه داشته باشم", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 3, - text: "من می توانم انرژی لازم را برای رویارویی با مشکلاتی که با آن مواجه می شوم پیدا کنم", - options: [ - "در محله ای قرار دارد که مردم به راحتی می توانند از یکدیگر دیدن کنند", - "بین این دو", - "جدا شده در اعماق جنگل", - ], - }, - { - question_number: 4, - text: "من می توانم انرژی لازم را برای رویارویی با مشکلاتی که با آن مواجه می شوم پیدا کنم", - options: ["همیشه", "اغلب", "به ندرت"], - }, - { - question_number: 5, - text: "من در مقابل حیوانات وحشی احساس ناراحتی می کنم حتی اگر آنها در قفس های امن محصور شوند", - options: ["بله", "مضطرب", "نه"], - }, - { - question_number: 6, - text: "من از انتقاد از مردم، عقاید و نظرات آنها خودداری می کنم", - options: ["درست است", "گاهی اوقات", "نادرست"], - }, - { - question_number: 7, - text: "من موسیقی کلاسیک را به موسیقی عامه پسند ترجیح می دهم", - options: ["معمولا", "گاهی اوقات", "هرگز"], - }, - { - question_number: 8, - text: "من موسیقی کلاسیک را به موسیقی عامه پسند ترجیح می دهم", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 9, - text: "اگر دیدم دو بچه همسایه در حال دعوا هستند", - options: [ - "من به آنها اجازه می دهم خودشان این موضوع را حل کنند", - "نمی دانم چه کار کنم", - "من آنها را آشتی خواهم داد", - ], - }, - { - question_number: 10, - text: "در جامعه", - options: [ - "من سعی می کنم خود را مورد توجه قرار دهم", - "بین این دو", - "ترجیح می دهم خودم را مورد توجه قرار ندهم", - ], - }, - { - question_number: 11, - text: "بودن جالب تر است", - options: ["مهندس ساختمان", "من نمی دانم", "بازیگر نمایشی"], - }, - { - question_number: 12, - text: "من معمولاً موفق می شوم با افراد خواستار کنار بیایم، حتی اگر آنها به خود ببالند یا خیلی از خود راضی به نظر برسند", - options: ["درست است", "من نمی دانم", "نادرست"], - }, - { - question_number: 13, - text: "من معمولاً موفق می شوم با افراد خواستار کنار بیایم، حتی اگر آنها به خود ببالند یا خیلی از خود راضی به نظر برسند", - options: ["درست است", "کم و بیش", "نادرست"], - }, - { - question_number: 14, - text: "من تقریباً همیشه می توانم از چهره افراد بفهمم که آیا آنها نادرست هستند یا خیر", - options: ["تقریبا همیشه", "گاهی اوقات", "هرگز"], - }, - { - question_number: 15, - text: "اگر تعطیلات (تعطیلات) طولانی تر باشد و همه موظف به گرفتن آن باشند، برای همه خوب است", - options: ["موافقم", "من مطمئن نیستم", "من مخالفم"], - }, - { - question_number: 16, - text: "ترجیح می‌دهم شغلی را بپذیرم که در آن به طور نامنظم اما زیاد درآمد داشته باشم، به جای اینکه کاری را بپذیرم که در آن به طور منظم اما کم درآمد داشته باشم.", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 17, - text: "من در مورد آنچه احساس می کنم صحبت می کنم", - options: [ - "فقط در صورت لزوم", - "بین این دو", - "به ابتکار خودم هر زمان که فرصتی داشته باشم", - ], - }, - { - question_number: 18, - text: "من گاهی اوقات به دلایلی که نمی توانم توضیح دهم احساس خطر مبهم می کنم یا می ترسم", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 19, - text: "اگر به ناحق به خاطر عملی که مرتکب نشده ام، اما به من نسبت داده شده، مورد انتقاد قرار بگیرم", - options: [ - "من اصلاً احساس گناه نمی کنم", - "بین این دو", - "هنوز هم کمی احساس گناه می کنم", - ], - }, - { - question_number: 20, - text: "با پول می توانید هر چیزی یا تقریباً هر چیزی را بدست آورید", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 21, - text: "تصمیمات من بیشتر دیکته می شود", - options: ["احساسات", "به همان اندازه با احساسات و عقل", "دلیل"], - }, - { - question_number: 22, - text: "بیشتر مردم اگر بیشتر در میان همسالان خود زندگی کنند و مانند آنها رفتار کنند، شادتر خواهند بود", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 23, - text: "وقتی در آینه نگاه می کنم، گاهی اوقات نمی دانم راست و چپم کجاست", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 24, - text: "وقتی صحبت می کنم، دوست دارم", - options: [ - "چیزها را همانطور که به ذهنشان می رسد ارائه دهید", - "بین این دو", - "از قبل ایده های سازمان یافته ای داشته باشید", - ], - }, - { - question_number: 25, - text: "وقتی چیزی من را عصبانی می کند، خیلی سریع آرام می شوم", - options: ["بله", "بین این دو", "نه"], - }, - { - question_number: 26, - text: "کار کردن به همان تعداد ساعت و با حقوق مساوی، بهتر است", - options: ["نجار یا آشپز", "من نمی دانم", "گارسون در یک رستوران"], - }, - { - question_number: 27, - text: "من تعیین شده ام برای انجام", - options: ["فقط چند عملکرد (تا 5)", "چندین عملکرد", "تعداد زیادی توابع"], - }, - { - question_number: 28, - text: 'بیل "حفاری کردن" است همانطور که "چاقو" است"', - options: ["لاغر شدن", "بریدن", "تیز کردن"], - }, - { - question_number: 29, - text: "این اتفاق می افتد که من نمی توانم بخوابم زیرا یک ایده مرا آزار می دهد", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 30, - text: "در زندگی شخصی به اهدافی که برای خودم در نظر گرفته ام می رسم", - options: ["در اکثر موارد", "گاهی اوقات", "هرگز"], - }, - { - question_number: 31, - text: "قانون منسوخ شده باید تغییر کند", - options: [ - "فقط پس از تجزیه و تحلیل کامل", - "بین این دو", - "بدون به تأخیر انداختن بیشتر تغییر از طریق بحث های طولانی", - ], - }, - { - question_number: 32, - text: "وقتی روی پروژه‌ای کار می‌کنم که نیاز به اقدام سریع با افراد دیگر دارد، راحت نیستم (احساس خوبی ندارم)", - options: ["بله", "بین این دو", "نه"], - }, - { - question_number: 33, - text: "به نظر من اکثر افرادی که می شناسم مرا یک شریک گفتگوی سرگرم کننده می دانند", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 34, - text: "اگر ناگهان در یک گروه اجتماعی در مرکز توجه عمومی قرار بگیرم کمی احساس خجالت می کنم", - options: [ - "من آنها را همانطور که هستند می پذیرم", - "بین این دو", - "آنها من را منزجر و ناراحت می کنند", - ], - }, - { - question_number: 35, - text: "اگر ناگهان در یک گروه اجتماعی در مرکز توجه عمومی قرار بگیرم کمی احساس خجالت می کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 36, - text: "من همیشه خوشحالم که مثلاً در یک گردهمایی بزرگ، پذیرایی، توپ یا اجتماع عمومی شرکت می کنم", - options: ["همیشه", "گاهی اوقات", "هرگز"], - }, - { - question_number: 37, - text: "در مدرسه ترجیح دادم یا ترجیح دادم", - options: ["موسیقی", "من نمی دانم", "برای آموزش عادات و اخلاق خوب"], - }, - { - question_number: 38, - text: "وقتی مسئولیتی در حوزه ای به من سپرده می شود، تقاضا دارم دستورالعمل هایم رعایت شود یا در غیر این صورت استعفا می دهم.", - options: ["بله", "گاهی اوقات", "نه"], - }, - { - question_number: 39, - text: "برای والدین مهمتر است", - options: [ - "برای کمک به رشد فرزندانشان", - "هر دو", - "تا به آنها یاد بدهیم خودشان را کنترل کنند", - ], - }, - { - question_number: 40, - text: "در کار گروهی، ترجیح می دهم", - options: ["خارها", "بین این دو", "برای اطمینان از رعایت مقررات"], - }, - { - question_number: 41, - text: "احساس می کنم نیاز به فعالیت بدنی دارم که نیاز به تلاش خاصی دارد", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 42, - text: "من ترجیح می دهم با افراد خوش اخلاق و مودب به جای افراد بی سواد برخورد کنم", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 43, - text: "وقتی مردم از من انتقاد می کنند احساس افسردگی زیادی می کنم", - options: ["بله", "بین این دو", "نه"], - }, - { - question_number: 44, - text: "وقتی رئیس با من تماس می گیرد", - options: [ - "هر دو", - "بین هر دو احتمال", - "می ترسم کار اشتباهی انجام داده باشم", - ], - }, - { - question_number: 45, - text: "در متن هایی که می خوانم، مقاصد پنهان را درک می کنم، نه به طور مستقیم بیان شده است", - options: [ - "افراد عملی با دستاوردهای مادی فوری", - "من نمی دانم", - "افرادی با ایده هایی برای بهبود جهان", - ], - }, - { - question_number: 46, - text: "در متن هایی که می خوانم، مقاصد پنهان را درک می کنم، نه به طور مستقیم بیان شده است", - options: ["بله", "بین این دو", "نه"], - }, - { - question_number: 47, - text: "وقتی نوجوان بودم در فعالیت های ورزشی مدرسه شرکت می کردم", - options: [ - "تا جایی که ممکن است، زمانی که مجبور شدم", - "با بی تفاوتی", - "با اشتیاق", - ], - }, - { - question_number: 48, - text: "اتاق من به خوبی چیده شده است و به طور کلی من دقیقاً می دانم که وسایلم کجا هستند", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 49, - text: "گاهی اوقات وقتی به اتفاقاتی که در طول روز افتاده فکر می کنم دچار تنش و عصبانیت می شوم", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 50, - text: "وقتی با مردم صحبت می‌کنم، فکر می‌کنم که آیا چیزی که می‌گویم واقعاً به آنها علاقه دارد؟", - options: ["تقریبا همیشه", "گاهی اوقات", "هرگز"], - }, - { - question_number: 51, - text: "اگر مجبور بودم انتخاب کنم، ترجیح می دادم باشم", - options: ["جنگلبان", "من نمی دانم", "معلم دبیرستان"], - }, - { - question_number: 52, - text: "در مناسبت های خاص جشن و سالگردهای مهم", - options: [ - "من دوست دارم هدیه شخصی بدهم", - "من نمی دانم", - "به نظر من پرداختن به خرید هدیه کسل کننده است", - ], - }, - { - question_number: 53, - text: 'خسته" یعنی "کار کردن" همانطور که "افتخار" است"', - options: ["لبخند بزن", "موفقیت", "خوشحال"], - }, - { - question_number: 54, - text: "کدام یک از 3 مورد زیر با دو مورد دیگر مطابقت ندارد", - options: ["شمع", "ماه", "لامپ برقی"], - }, - { - question_number: 55, - text: "دوستان من را رها کرده اند", - options: ["تقریبا هرگز", "گاهی اوقات", "اغلب"], - }, - { - question_number: 56, - text: "از طریق برخی ویژگی های شخصی، من نسبت به اکثر مردم احساس برتری می کنم", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 57, - text: "وقتی چیزی مرا آزار می دهد، سعی می کنم احساسم را از دیگران پنهان کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 58, - text: "من دوست دارم به نمایش بروم یا خوش بگذرانم", - options: [ - "چند بار در هفته", - "حدود یک بار در هفته", - "کمتر از یک بار در هفته", - ], - }, - { - question_number: 59, - text: "من معتقدم که درجه آزادی بسیار بالا از اخلاق خوب و احترام به قانون ارزشمندتر است", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 60, - text: "در حضور افراد مسئول (با تجربه بیشتر، مسن تر یا در پست های مهم) تمایل به صحبت ندارم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 61, - text: "برای من دشوار است که به گروه بزرگی از مردم خطاب کنم یا در مقابل آنها سخنرانی کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 62, - text: "وقتی در یک مکان ناآشنا هستم، حس جهت گیری خوبی دارم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 63, - text: "اگر کسی با من قهر کند", - options: [ - "سعی می کنم آنها را آرام کنم", - "من نمی دانم", - "من را اذیت می کند", - ], - }, - { - question_number: 64, - text: "وقتی مقاله ای را در مجله می خوانم که چیزها را به شکلی تحریف شده ارائه می کند، به جای اینکه بخواهم بگویم چگونه چیزها را می بینم، آن را فراموش می کنم.", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 65, - text: ".چیزهای بی اهمیتی مثل اسم خیابان ها و مغازه ها و ... یادم نیست", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 66, - text: "من با اشتها غذا می خورم و همیشه مثل بقیه مردم مرتب و تمیز می خورم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 67, - text: "من با اشتها غذا می خورم و همیشه مثل بقیه مردم مرتب و تمیز می خورم", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 68, - text: "روزهایی هست که حالم بد است و دوست ندارم کسی را ببینم", - options: ["خیلی به ندرت", "گاهی اوقات", "اغلب اوقات"], - }, - { - question_number: 69, - text: "گاهی به من گفته اند که در صدا و حرکاتم اشتیاق زیادی نشان می دهم", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 70, - text: "در نوجوانی اگر نظر دیگری با پدر و مادرم داشتم", - options: [ - "من نظر خودم را حفظ کردم", - "من کم و بیش نظرم را حفظ کردم", - "من به نظر پدر و مادرم تسلیم شدم", - ], - }, - { - question_number: 71, - text: "ترجیح می‌دهم یک دفتر فقط برای خودم داشته باشم بدون اینکه آن را با شخص دیگری به اشتراک بگذارم", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 72, - text: "ترجیح می دهم از زندگی بی سر و صدا به روش خودم لذت ببرم تا اینکه به خاطر دستاوردهایم تحسین شوم", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 73, - text: "من خودم را از بسیاری جهات بالغ می دانم", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 74, - text: "روشی که اکثر مردم انتقاد می کنند بیشتر از اینکه به من کمک کند، من را دلسرد می کند", - options: ["اغلب", "گاهی اوقات", "هرگز"], - }, - { - question_number: 75, - text: "من همیشه موفق به کنترل کامل بیان احساساتم می شوم", - options: ["همیشه", "اغلب", "هرگز"], - }, - { - question_number: 76, - text: "اگر ایده یک اختراع داشتم ترجیح می دادم", - options: [ - "تا در آزمایشگاه کامل شود", - "من نمی دانم", - "برای اجرا به دیگران منتقل شود", - ], - }, - { - question_number: 77, - text: 'غافلگیری "عجیب" است همانطور که "ترس" برای"', - options: ["شجاع", "گلبرگ ها", "وحشت زده"], - }, - { - question_number: 78, - text: "کدام یک از 3 کسر زیر به دسته 2 کسر دیگر تعلق ندارد", - options: ["3/7", "3/9", "3/11"], - }, - { - question_number: 79, - text: "به نظر می رسد برخی افراد مرا نادیده می گیرند یا از من دوری می کنند و من دلیل آن را نمی دانم", - options: ["درست است", "من نمی دانم", "نادرست"], - }, - { - question_number: 80, - text: "مردم به خاطر نیت خوبم کمتر از آنچه که شایسته آن هستم با من مهربانانه رفتار می کنند", - options: ["اغلب", "گاهی اوقات", "هرگز"], - }, - { - question_number: 81, - text: "استفاده از کلمات درشت، حتی زمانی که هیچ مرد و زن با هم در گروه وجود ندارد", - options: [ - "من را عمیقاً منزجر می کند", - "من را بی تفاوت می گذارد", - "من آن را دوست دارم", - ], - }, - { - question_number: 82, - text: "من از بسیاری از مردم دوستان کمتری دارم", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 83, - text: "من از بودن در جایی که افراد زیادی برای صحبت کردن با آنها وجود ندارند متنفرم", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 84, - text: "اگرچه مردم گاهی مرا یک جوکر می دانند، اما باز هم مرا دوست داشتنی می دانند", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 85, - text: "من ترس صحنه را در موقعیت های اجتماعی در مناسبت های مختلف تجربه کرده ام", - options: ["خیلی وقت ها", "به ندرت", "تقریبا هرگز"], - }, - { - question_number: 86, - text: "وقتی در گروه کوچکی از مردم هستم، راضی هستم که «در پس‌زمینه» بمانم و به دیگران اجازه بدهم مکالمه را ادامه دهند.", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 87, - text: "من ترجیح می دهم بخوانم", - options: [ - "گزارش خوبی از درگیری های نظامی یا مبارزات سیاسی", - "من نمی دانم", - "یک رمان خوب", - ], - }, - { - question_number: 88, - text: "وقتی افراد مقتدر سعی می کنند به من فشار بیاورند، مطابق میل آنها عمل می کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 89, - text: "مافوق و اعضای خانواده من معمولاً فقط زمانی برای من اظهار نظر می کنند که واقعاً موجه باشد", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 90, - text: "من از نگاه برخی افراد در خیابان یا مغازه ها به دیگران خوشم نمی آید", - options: ["درست است", "بی تفاوت", "نادرست"], - }, - { - question_number: 91, - text: "من آن را در طول یک سفر طولانی ترجیح می دهم", - options: [ - "برای خواندن چیزهای جالب و جالب", - "من نمی دانم", - "برای اینکه با یک دوست خوب در مورد این و آن صحبت کنید", - ], - }, - { - question_number: 92, - text: "در مواقعی که اگر درک نشویم می تواند عواقب جدی داشته باشد، حتی به بهای رعایت ادب و آرامش، از شوخی کردن و فهماندن خود دریغ نکنیم.", - options: ["بله", "بین این دو", "نه"], - }, - { - question_number: 93, - text: "اگر در دایره آشنای من افرادی هستند که از من خوششان نمی آید و به من بگویید که مورد پسند آنها نیستم.", - options: ["اصلا اذیتم نمیکنه", "بین این دو", "من را اذیت می کند"], - }, - { - question_number: 94, - text: "وقتی از من تعریف و تمجید می شود احساس خجالت می کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 95, - text: "ترجیح میدم حقوق بگیرم", - options: ["با حقوق ثابت و مطمئن", "بین این دو", "پر جنب و جوش"], - }, - { - question_number: 96, - text: "کار برای اینکه دائماً مطلع باشم، به روز با آنچه اتفاق می افتد، ترجیح می دهم", - options: [ - "برای بحث در مورد مسائل با مردم", - "بین این دو", - "به خود حقایق تکیه کنند", - ], - }, - { - question_number: 97, - text: "من دوست دارم فعالانه در فعالیت های اجتماعی شرکت کنم، در کمیته ها کار کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 98, - text: "برای تکمیل یک کار، من فقط زمانی راضی هستم که کوچکترین جزئیات از نزدیک بررسی شده باشد.", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 99, - text: "این اتفاق می افتد که چیزهای بسیار کوچک من را خیلی آزار می دهد", - options: ["بله", "مضطرب", "نه"], - }, - { - question_number: 100, - text: "من بدون آشفتگی و بدون صحبت در خواب راحت می خوابم", - options: ["همیشه", "اغلب", "خیلی به ندرت"], - }, - { - question_number: 101, - text: "اگر مجبور به انتخاب بودم، در فعالیت حرفه ای خود ترجیح می دادم", - options: [ - "برای تعامل با مردم", - "بین این دو", - "برای کار با اسناد بدون تعامل با مردم", - ], - }, - { - question_number: 102, - text: 'اندازه" به "عرض" است همانطور که "بی صداقتی" به"', - options: ["زندان", "افراد عملی با دستاوردهای مادی فوری", "سرقت"], - }, - { - question_number: 103, - text: 'AB" به "DC" است همانطور که "SR" به"', - options: ["PO", "OP", "TU"], - }, - { - question_number: 104, - text: "وقتی مردم کارهایی را انجام می دهند یا می گویند که غیر منطقی است", - options: ["من بی تفاوت هستم", "من نمی دانم", "من آنها را تحقیر می کنم"], - }, - { - question_number: 105, - text: "وقتی مردم در حال گوش دادن به موسیقی با صدای بلند صحبت می کنند", - options: [ - "اصلا", - "بین این دو", - "آزارم می دهد و میل به گوش دادن به موسیقی را از دست می دهم", - ], - }, - { - question_number: 106, - text: "فکر می کنم می توان من را به عنوان یک موجود در نظر گرفت", - options: ["بیشتر یک آدم آرام", "بین این دو", "بیشتر یک فرد خشن"], - }, - { - question_number: 107, - text: "من فقط زمانی در جلسات شرکت می کنم که مجبور باشم", - options: ["درست است", "من نمی دانم", "نادرست"], - }, - { - question_number: 108, - text: "بهتر است واقع بین باشید و انتظار زیادی نداشته باشید تا اینکه باور کنید همه چیز خود به خود حل می شود", - options: ["درست است", "من نمی دانم", "نادرست"], - }, - { - question_number: 109, - text: "وقتی به مشکلاتی که در فعالیتم پیش خواهد آمد فکر می کنم", - options: [ - "من سعی می کنم قبل از وقوع آنها پیش بینی کنم که چگونه پیش خواهم رفت", - "من نمی دانم", - "به خودم می گویم که وقتی زمانش برسد می توانم با آنها کنار بیایم", - ], - }, - { - question_number: 110, - text: "در مجالس به راحتی به مردم نزدیک می شوم", - options: ["درست است", "من مطمئن نیستم", "نادرست"], - }, - { - question_number: 111, - text: "وقتی کمی دیپلماسی یا کار متقاعدکننده ای لازم است تا مردم را به انجام کاری وادار کند، از من دعوت می شود", - options: ["اغلب", "گاهی اوقات", "هرگز"], - }, - { - question_number: 112, - text: "به نظر من بودن جالب تر است", - options: ["مشاور راهنمایی شغلی", "من نمی دانم", "رئیس اداره سازماندهی کار"], - }, - { - question_number: 113, - text: "اگر مطمئن باشم که شخصی بی انصاف یا خودخواه است، به او می گویم حتی اگر باعث ناراحتی من شود.", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 114, - text: "من به شوخی به خاطر بازی چیزهای پوچ می گویم تا مردم را متحیر کنم و ببینم چه واکنشی نشان می دهند.", - options: ["بله", "شاید", "هرگز"], - }, - { - question_number: 115, - text: "من می خواهم برای یک مجله در بخش سرگرمی (تئاتر، کنسرت، )اپرا بنویسم", - options: ["بله", "شاید", "نه"], - }, - { - question_number: 116, - text: "وقتی مجبور می شوم در طول جلسه بی حرکت بنشینم، هرگز نیازی به خط خطی کردن یا بی قراری احساس نمی کنم", - options: ["درست است", "شاید", "نادرست"], - }, - { - question_number: 117, - text: "اگر کسی چیزی را به من بگوید که من می دانم با واقعیت مطابقت ندارد، من فوراً این نظر را شکل می دهم", - options: ["آنها دروغگو هستند", "بین این دو", "آنها اطلاعات ضعیفی دارند"], - }, - { - question_number: 118, - text: "من احساس تهدید مبهم به مجازات دارم، حتی زمانی که هیچ اشتباهی مرتکب نشده ام", - options: ["اغلب", "به ندرت", "هرگز"], - }, - { - question_number: 119, - text: "این ایده مبالغه آمیز است که یک بیماری هم ماهیت روانی و هم جسمی دارد", - options: ["بله", "شاید", "نه"], - }, - { - question_number: 120, - text: "شکوه (عظمت) و شکوه مراسم اصلی رسمی از آداب و رسومی است که باید حفظ شود", - options: ["بله", "شاید", "نه"], - }, - { - question_number: 121, - text: "اگر مردم فکر می کردند که من خیلی رویایی یا خیلی بدیع هستم، ناراحت می شدم", - options: ["بسیار", "کمی", "اصلا"], - }, - { - question_number: 122, - text: "برای انجام کاری، ترجیح می دهم کار کنم", - options: ["همراه با دیگران", "من نمی دانم", "به تنهایی"], - }, - { - question_number: 123, - text: "من دوره هایی را پشت سر می گذارم که برایم سخت است که از دلسوزی برای خودم خودداری کنم", - options: ["اغلب", "گاهی اوقات", "هرگز"], - }, - { - question_number: 124, - text: "من خیلی زود با مردم عصبانی می شوم", - options: ["اغلب", "گاهی اوقات", "هرگز"], - }, - { - question_number: 125, - text: "من به راحتی می توانم برخی از عادت ها را بدون اینکه دوباره آنها را ترک کنم، ترک کنم", - options: ["همیشه", "گاهی اوقات", "هرگز"], - }, - { - question_number: 126, - text: "برای دستمزد برابر، ترجیح می دهم باشم", - options: ["وکیل", "من نمی دانم", "ناوبر و خلبان"], - }, - { - question_number: 127, - text: 'بهتر است برای "بدترین" همانطور که "آهسته تر" است"', - options: ["پر جنب و جوش", "بهترین", "سریع ترین"], - }, - { - question_number: 128, - text: "کدام یک از 3 پاسخ زیر باید بعد از گروه حروف زیر قرار گیرد: XOOOOXXOOOXXX", - options: ["OXXX", "OOXX", "XOOO"], - }, - { - question_number: 129, - text: "این اتفاق می افتد که وقتی زمان انجام کاری که به من پیشنهاد شده و قبلاً از آن خوشحال بودم فرا می رسد، دیگر حوصله کار کردن ندارم.", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 130, - text: "در بیشتر کارهایی که انجام می‌دهم، بدون ایجاد مزاحمت توسط افرادی که در اطرافم سروصدا می‌کنند، تمرکز می‌کنم.", - options: ["درست است", "شاید", "نادرست"], - }, - { - question_number: 131, - text: "گاهی اوقات می بینم که به غریبه ها چیزهایی می گویم که به نظرم مهم است، حتی اگر آنها چیزی از من نپرسیده باشند", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 132, - text: "من بسیاری از ساعات آزادم را صرف گفتگو با دوستان در مورد لحظات خوشی که در گذشته با هم داشته ایم می گذرانم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 133, - text: "من دوست دارم فقط برای سرگرمی در کارهای شجاعانه و حتی بی پروا افراط کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 134, - text: "یک اتاق به هم ریخته", - options: ["من اصلا ازش خوشم نمیاد", "کم و بیش", "من آن را دوست دارم"], - }, - { - question_number: 135, - text: "من خودم را فردی بسیار اجتماعی می دانم که افراد زیادی را ملاقات می کنم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 136, - text: "در گفتگوها و بحث ها", - options: [ - "من آزادانه احساساتم را بیان می کنم", - "کم و بیش", - "من احساساتم را برای خودم نگه می دارم", - ], - }, - { - question_number: 137, - text: "من موسیقی را ترجیح می دهم", - options: ["سبک و شاداب", "بین این دو", "عاطفی و احساسی"], - }, - { - question_number: 138, - text: "من زیبایی یک شعر را بیشتر از یک سلاح کامل قدر می دانم", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 139, - text: "اگر یک مشاهده خوب من مورد توجه قرار نگیرد", - options: [ - "من اصرار ندارم", - "من نمی دانم", - "من آن را تکرار می کنم تا به مردم فرصتی بدهم تا متوجه آن شوند", - ], - }, - { - question_number: 140, - text: "من مایلم در یک سرویس توانبخشی برای مجرمانی که در حال آزادی موقت هستند کار کنم", - options: ["بله", "مضطرب", "نه"], - }, - { - question_number: 141, - text: "هنگامی که با افراد غریبه مختلف رابطه دارید، باید محتاط باشید زیرا خطرات آلودگی و سایر خطرات ممکن است ایجاد شود.", - options: ["بله", "من نمی دانم", "نه"], - }, - { - question_number: 142, - text: "من ترجیح می دهم از طریق یک سازمان توریستی در یک برنامه سفر (مسیری) که توسط آن سازمان تعیین شده است، به خارج از کشور سفر کنم، نه اینکه خودم در مورد مکان هایی که بازدید خواهم کرد تصمیم بگیرم.", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 143, - text: "من را به درستی فردی سرسخت و سخت کوش می دانند، اما چندان موفق نیستم", - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 144, - text: "اگر پیش بیاید که مردم از محبت من سوء استفاده کنند، ناراحت نمی شوم و زود فراموش نمی کنم", - options: ["بله", "من مطمئن نیستم", "نه"], - }, - { - question_number: 145, - text: "اگر بخواهم شاهد جنجال داغی باشم که در طول یک بحث در یک گروه به وجود می آید", - options: [ - "من دوست دارم یک برنده وجود داشته باشد", - "بین این دو", - "من دوست دارم به یک عقل سلیم برسم", - ], - }, - { - question_number: 146, - text: "دوست دارم کارهایی را که باید انجام دهم به تنهایی سازماندهی کنم، بدون اینکه دیگران حرفم را قطع کنند یا به من توصیه کنند", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 147, - text: "در اعمالم تحت تاثیر احساس حسادت قرار می گیرم", - options: ["گاهی اوقات", "خیلی به ندرت", "اصلا"], - }, - { - question_number: 148, - text: 'من کاملا با جمله زیر موافقم: "حتی اگر او اشتباه می کند، رئیس ".هنوز رئیس است"', - options: ["بله", "بین این دو", "نه"], - }, - { - question_number: 149, - text: "وقتی به تمام کارهایی که باید انجام دهم فکر می کنم احساس اضطراب می کنم", - options: ["بله", "گاهی اوقات", "نه"], - }, - { - question_number: 150, - text: "وقتی درگیر یک بازی هستم، توصیه هایی که اطرافیانم به من می دهند، مرا آزار نمی دهد", - options: ["درست است", "من مطمئن نیستم", "نادرست"], - }, - { - question_number: 151, - text: "بودن جالب تر است", - options: ["هنرمند", "من نمی دانم", "دبیر یک انجمن"], - }, - { - question_number: 152, - text: "کدام یک از 3 کلمه زیر با 2 کلمه دیگر در یک دسته قرار نمی گیرد", - options: ['"مهم نیست"', '"کمی"', '"خیلی"'], - }, - { - question_number: 153, - text: 'شعله "گرما" است همانطور که "رز" است"', - options: ["خارها", "گلبرگ ها", "عطر"], - }, - { - question_number: 154, - text: "خواب های آشفته ای دارم که خوابم را مختل می کند", - options: ["اغلب", "گاهی اوقات", "تقریبا هرگز"], - }, - { - question_number: 155, - text: "حتی اگر شرایط واقعاً برای موفقیت چیزی نامطلوب باشد، من همچنان معتقدم که برای موفقیت باید همه چیز را امتحان کنم", - options: ["همیشه", "گاهی اوقات", "هرگز"], - }, - { - question_number: 156, - text: "من دوست دارم در شرایطی باشم که به خوبی می دانم که گروه باید چه کار کند، طبیعتاً رهبر می شوم", - options: ["درست است", "کم و بیش", "نادرست"], - }, - { - question_number: 157, - text: "ترجیح می‌دهم به جای اینکه با یک سبک شیک و اصیل برجسته باشم، ملایم و کلاسیک بپوشم", - options: ["درست است", "من مطمئن نیستم", "نادرست"], - }, - { - question_number: 158, - text: "یک شب آرام اختصاص داده شده به یک فعالیت مورد علاقه برای من لذت بخش تر از شرکت در یک جمع پر جنب و جوش است", - options: ["درست است", "من مطمئن نیستم", "نادرست"], - }, - { - question_number: 159, - text: "من در برابر پیشنهادهای خیرخواهانه دیگران مقاومت می کنم، حتی زمانی که متوجه می شوم اشتباه می کنم", - options: ["گاهی اوقات", "تقریبا هرگز", "هرگز"], - }, - { - question_number: 160, - text: "در تمام تصمیماتی که می‌گیرم، همیشه این وظیفه را می‌دانم که بر اساس قوانین اساسی درست و غلط هدایت شوم", - options: ["درست است", "کم و بیش", "نادرست"], - }, - { - question_number: 161, - text: "وقتی گروهی از مردم در حین کار من را تماشا می کنند، تا حدودی احساس ناراحتی می کنم", - options: ["درست است", "شاید", "نادرست"], - }, - { - question_number: 162, - text: 'نظر شما در مورد جمله زیر چیست: "با توجه به اینکه همیشه نمی توان عاقلانه کار کرد، گاهی اوقات لازم است بدون ".ناخوشایند" به اجبار متوسل شویم.', - options: ["موافقم", "من نمی دانم", "من مخالفم"], - }, - { - question_number: 163, - text: "در مدرسه ترجیح دادم", - options: ["زبان رومانیایی", "من نمی دانم", "ریاضیات"], - }, - { - question_number: 164, - text: "گاهی برای من پیش آمده که عصبانی می شوم زیرا پشت سرم چیزهای بدی درباره من می گویند که کاملا ساختگی بود.", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 165, - text: "گفتگو با مردم عادی که مطابق با قوانین و آداب و رسوم هستند", - options: [ - "اغلب بسیار جالب و آموزنده هستند", - "نسبت به من بی تفاوت هستند", - "حوصله ام سر می رود زیرا سطحی و غیر جالب هستند", - ], - }, - { - question_number: 166, - text: "بعضی چیزها آنقدر مرا ناراحت می کند که ترجیح می دهم دیگر در مورد آنها صحبت نکنم", - options: ["درست است", "بین این دو", "نادرست"], - }, - { - question_number: 167, - text: "در تربیت کودک اهمیت بیشتری دارد", - options: [ - "برای ارائه محبت، چیزی که آنها نیاز دارند", - "من نمی دانم", - "برای آموزش عادات و اخلاق خوب", - ], - }, - { - question_number: 168, - text: "مردم مرا فردی با صداقت (همه یکپارچه)، متعادل و بی‌تأثیر از موفقیت‌ها و شکست‌های زندگی می‌دانند.", - options: ["درست است", "شاید", "نادرست"], - }, - { - question_number: 169, - text: "من معتقدم جامعه باید این خرد را داشته باشد که عادات خود را تغییر دهد و حتی عادات قدیمی را کنار بگذارد", - options: ["درست است", "کم و بیش", "نادرست"], - }, - { - question_number: 170, - text: "من معتقدم که در دنیای مدرن حل کردن مهمتر است", - options: ["مشکل ارزش های اخلاقی", "من نمی دانم", "سریع ترین"], - }, - { - question_number: 171, - text: "بیشتر لذت می برم", - options: ["خواندن یک کتاب خوب", "من نمی دانم", "شرکت در بحث گروهی"], - }, - { - question_number: 172, - text: "من ترجیح می‌دهم کاری را که می‌خواهم انجام دهم تا اینکه با قوانین تعیین‌شده مطابقت کنم", - options: ["درست است", "من مطمئن نیستم", "نادرست"], - }, - { - question_number: 173, - text: "قبل از بحث در مورد چیزی، سعی می کنم از صحت آنچه می خواهم بگویم اطمینان حاصل کنم", - options: ["همیشه", "گاهی اوقات", "خیلی به ندرت"], - }, - { - question_number: 174, - text: "گاهی اوقات بعضی چیزهای کوچک به طرز غیرقابل تحملی اعصابم را به هم می ریزند، حتی اگر می دانم بی اهمیت هستند.", - options: ["گاهی اوقات", "خیلی به ندرت", "هرگز"], - }, - { - question_number: 175, - text: "به ندرت اتفاق می افتد که تحت تأثیر یک انگیزه لحظه ای چیزهایی بگویم که کاملاً تاسف انگیز است", - options: ["درست است", "بین این دو", "نادرست"], - }, - { - question_number: 176, - text: "اگر از من خواسته شود در یک مراسم خیریه شرکت کنم", - options: [ - "من قبول می کنم", - "من نمی دانم", - "من مودبانه پاسخ می دهم که خیلی سرم شلوغ است", - ], - }, - { - question_number: 177, - text: "کدام یک از 3 کلمه زیر به همان دسته دو کلمه دیگر تعلق ندارد", - options: ['"عریض"', '"سینوسی"', '"مستقیم"'], - }, - { - question_number: 178, - text: 'به زودی "هرگز" همان چیزی است که "تقریبا" است"', - options: ['"هیچ جا"', '"تقریبا"', '"در دوردست"'], - }, - { - question_number: 179, - text: "اگر اشتباهی در جامعه مرتکب شوم، به سرعت آن را فراموش می کنم", - options: ["درست است", "کم و بیش", "نادرست"], - }, - { - question_number: 180, - text: 'من یک فرد "خلاق" به حساب می آیم که تقریباً همیشه چیزی برای ارائه برای مشکلات پیش آمده دارد', - options: ["بله", "احتمالا", "نه"], - }, - { - question_number: 181, - text: "من باور دارم که هستم", - options: [ - "خونسردی در مواجهه با شرایط سخت", - "من نمی دانم", - "تحمل خواسته های دیگران", - ], - }, - { - question_number: 182, - text: "من را فردی می دانند که به راحتی مشتاق می شود", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 183, - text: "من کارهایی را دوست دارم که فرصت هایی برای تغییر، وظایف متنوع، سفر، حتی اگر این کار با خطراتی همراه باشد، ارائه می دهد", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 184, - text: "من آدم نسبتاً دقیقی هستم که دوست دارم کارها را به بهترین نحو ممکن انجام دهم", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 185, - text: "من کاری را دوست دارم که در آن باید با وجدان باشید و دقت لازم است", - options: ["بله", "کم و بیش", "نه"], - }, - { - question_number: 186, - text: "من از دسته افراد فعالی هستم که هرگز نمی توانند بدون انجام کاری یک جا بنشینند", - options: ["درست است", "بین این دو", "نادرست"], - }, - { - question_number: 187, - text: "مطمئنم هیچ سوالی را از این پرسشنامه حذف نکرده ام و هر بار به درستی پاسخ داده ام.", - options: ["بله", "بین این دو", "نه"], - }, -]; diff --git a/src/data/glasser-fallback.ts b/src/data/glasser-fallback.ts deleted file mode 100644 index 1bada8a..0000000 --- a/src/data/glasser-fallback.ts +++ /dev/null @@ -1,183 +0,0 @@ -export type GlasserFallbackQuestion = { - question_number: number; - factor_code: string; - text: string; -}; - -export const glasserFallbackQuestions: GlasserFallbackQuestion[] = [ - { - question_number: 1, - factor_code: "S", - text: "مسایلی مثل پس انداز، مخارج زندگی، مسکن، آینده شغلی و.. تا چه اندازه ذهن شما را به خود مشغول می دارد؟", - }, - { - question_number: 2, - factor_code: "S", - text: "تا چه اندازه به سلامت جسمانی، بهداشت و احتمال ابتلا به بیماری فکر میکنید؟", - }, - { - question_number: 3, - factor_code: "S", - text: "شدت میل جنسی خود را چگونه ارزیابی میکنید؟", - }, - { - question_number: 4, - factor_code: "S", - text: "در انجام کارها و اقدامات مخاطره انگیز تا چه اندازه محتاطانه عمل میکنید؟", - }, - { - question_number: 5, - factor_code: "S", - text: "از روبه رویی با تجارب جدید و شروع راه های ناشناخته تا چه اندازه اجتناب میکنید؟", - }, - { - question_number: 6, - factor_code: "S", - text: "از نظر دوستان و همکاران خود چقدر وقت شناس با نظم و ترتیب و دقیق هستید؟", - }, - { - question_number: 7, - factor_code: "S", - text: "تا چه اندازه امنیت شغلی و درآمد ثابت برایتان اهمیت دارد؟", - }, - { - question_number: 8, - factor_code: "L", - text: "احساس می کنید به چه میزان عشق، صمیمیت و مهرورزی نیاز دارید؟", - }, - { - question_number: 9, - factor_code: "L", - text: "تا چه اندازه رفاه و سعادت انسان های دیگر برایتان مهم است؟", - }, - { - question_number: 10, - factor_code: "L", - text: "تا چه اندازه تمایل دارید تجارب شخصی، احساسات عمیق و اسرار خود را با دیگران در میان بگذارید؟", - }, - { - question_number: 11, - factor_code: "L", - text: "در ایجاد صمیمیت و برقراری ارتباط عاطفی با دیگران چقدر پیش قدم می شوید؟", - }, - { - question_number: 12, - factor_code: "L", - text: "تا چه اندازه از دیدن، وقت گذراندن و گفتگو با دوستان و آشنایان خود لذت می برید؟", - }, - { - question_number: 13, - factor_code: "L", - text: "میزان بخشندگی، گذشت و فراموش کردن خطای دیگران را در خود چگونه ارزیابی می کنید؟", - }, - { - question_number: 14, - factor_code: "L", - text: "تا چه اندازه به ایجاد و حفظ روابط دوستانه، صمیمی و پایدار با دیگران علاقه مند هستید؟", - }, - { - question_number: 15, - factor_code: "P", - text: "تا چه اندازه تمایل دارید رهبری گروه‌ها، هدایت پروژه‌ها یا مدیریت امور را بر عهده بگیرید؟", - }, - { - question_number: 16, - factor_code: "P", - text: "رسیدن به موفقیت، موقعیت اجتماعی ممتاز و جایگاه برتر تا چه حد برایتان اهمیت دارد؟", - }, - { - question_number: 17, - factor_code: "P", - text: "در صورت بروز نقد، مخالفت یا اشتباه از سوی دیگران، تا چه حد رفتار تند، قاطع یا اصلاحی نشان می‌دهید؟", - }, - { - question_number: 18, - factor_code: "P", - text: "تا چه اندازه تمایل دارید کارها دقیقاً طبق نظرات، استانداردها و روش‌های شما انجام شوند؟", - }, - { - question_number: 19, - factor_code: "P", - text: "در بحث‌ها و گفتگوها تا چه حد اصرار دارید حرف و نظر خود را به عنوان نظر درست به اثبات برسانید؟", - }, - { - question_number: 20, - factor_code: "P", - text: "وقتی احساس می‌کنید حق با شماست، تا چه اندازه حاضر به رقابت یا پافشاری بر روی خواسته‌هایتان هستید؟", - }, - { - question_number: 21, - factor_code: "P", - text: "دیدگاه شما نسبت به یادگیری، کسب دانش و بالا بردن اطلاعات تخصصی چیست؟", - }, - { - question_number: 22, - factor_code: "Fr", - text: "تا چه اندازه تمایل دارید بدون دخالت، نظارت یا دستور دیگران اهداف شخصی خود را دنبال کنید؟", - }, - { - question_number: 23, - factor_code: "Fr", - text: "در برابر احساس محدودیت، اجبار یا رعایت قوانین دست و پا گیر تا چه حد واکنش نشان می‌دهید؟", - }, - { - question_number: 24, - factor_code: "Fr", - text: "میزان تمایل شما به داشتن حریم خصوصی، زمان تنهایی و استقلال در تصمیم‌گیری‌ها چقدر است؟", - }, - { - question_number: 25, - factor_code: "Fr", - text: "تا چه اندازه رهایی از قید و بندها و برنامه‌های فشرده را ترجیح می‌دهید؟", - }, - { - question_number: 26, - factor_code: "Fr", - text: "در مواجهه با فشار روانی یا استرس، تا چه حد نیاز دارید فضا و وقت مستقل داشته باشید؟", - }, - { - question_number: 27, - factor_code: "Fr", - text: "تا چه اندازه تمایل دارید مسیر زندگی، سبک پوشش یا رفتارهای خود را بر اساس معیارهای فردی انتخاب کنید؟", - }, - { - question_number: 28, - factor_code: "Fr", - text: "چقدر برای آزادی اندیشه و ابراز دیدگاه‌های غیرمعمول اهمیت قائل هستید؟", - }, - { - question_number: 29, - factor_code: "Fu", - text: "تا چه اندازه اهل شوخ‌طبعی، خنده، لطیفه‌گویی و ایجاد فضای شاد هستید؟", - }, - { - question_number: 30, - factor_code: "Fu", - text: "چقدر زمان و هزینه برای سرگرمی، تفریحات بی‌دغدغه و فعالیت‌های مفرح اختصاص می‌دهید؟", - }, - { - question_number: 31, - factor_code: "Fu", - text: "تا چه اندازه از کارهای خلاقانه، هنری، بازی و سرگرمی لذت می‌برید؟", - }, - { - question_number: 32, - factor_code: "Fu", - text: "چقدر قادر هستید لحظات سخت و جدیت زندگی را با نگاهی سبک، طنزآمیز و مثبت سپری کنید؟", - }, - { - question_number: 33, - factor_code: "Fu", - text: "تمایل شما به شرکت در دورهمی‌ها، برنامه‌های تفریحی و سفر با دوستان چقدر است؟", - }, - { - question_number: 34, - factor_code: "Fu", - text: "چقدر برای داشتن سرگرمی‌ها (هابی) و فعالیت‌های غیرکاری وقت می‌گذارید؟", - }, - { - question_number: 35, - factor_code: "Fu", - text: "از یادگیری چیزهای جدید همراه با بازی، هیجان و نشاط چقدر استقبال می‌کنید؟", - }, -]; diff --git a/src/data/question-data.ts b/src/data/question-data.ts deleted file mode 100644 index 48aea83..0000000 --- a/src/data/question-data.ts +++ /dev/null @@ -1,345 +0,0 @@ -import enQuestions from "@/data/questions/en.json"; -import faQuestions from "@/data/questions/fa.json"; -import type { MarriageGender } from "@/hooks/marriage/types"; -import { defaultLocale, type Locale } from "@/translations/config"; -import { getDictionary } from "@/translations/dictionaries"; - -export const bookingTerms = [ - "You will be contacted by your consultant.", - "The call may start 10-15 minutes earlier or later than scheduled.", - "Make sure you are available and in a quiet place at least 10 minutes before the session.", -] as const; - -export type QuestionCardIcon = - | "profile" - | "education" - | "details" - | "checklist" - | "contact" - | "family_marital"; - -type QuestionExtras = { - placeHolder: string; - range: [number, number]; - options: string[]; - noSearch?: boolean; -}; - -type QuestionAudienceRule = { - genders?: MarriageGender[]; - maxAge?: number; - minAge?: number; -}; - -export type QuestionLogic = { - dependsOn: { - title: string; - values: string[]; - }; -}; - -export type QuestionField = { - title: string; - type: string; - required: boolean; - private?: boolean; - description: string; - tooltip: string; - extras: QuestionExtras; - audience?: QuestionAudienceRule; - requiredWhen?: QuestionAudienceRule; - logic?: QuestionLogic; - showGuardianNotice?: boolean; - originalSlug?: string; - originalIndex?: number; - englishTitle?: string; -}; - -export type QuestionListItem = { - slug: string; - title: string; - estimate: string; - progress: number; - icon: QuestionCardIcon; - required?: boolean; - note?: string; - showInfoBadge?: boolean; - summary: string; - checkpoints: readonly string[]; - tooltip: string; - questions: readonly QuestionField[]; - audience?: QuestionAudienceRule; -}; - -type RawQuestionListItem = { - title: string; - icon: string; - slug: string; - required?: boolean; - estimateTime: string; - tooltip: string; - progress: number; - description: string; - questions: QuestionField[]; - audience?: QuestionAudienceRule; -}; - -const iconMap: Record = { - "user-circle": "profile", - school: "education", - "heart-handshake": "details", - "file-text": "contact", - "layout-grid": "checklist", -}; - -const questionsByLocale: Record = { - en: enQuestions as RawQuestionListItem[], - fa: faQuestions as RawQuestionListItem[], -}; - -function mapQuestionListItem(item: RawQuestionListItem): QuestionListItem { - return { - slug: item.slug, - title: item.title, - estimate: item.estimateTime, - progress: item.progress, - icon: iconMap[item.icon] ?? "details", - required: item.required, - showInfoBadge: Boolean(item.tooltip), - summary: item.description, - checkpoints: item.questions.map((question) => question.title), - tooltip: item.tooltip, - questions: item.questions, - }; -} - -export function getQuestionListItems(locale: Locale = defaultLocale) { - const rawItems = - questionsByLocale[locale] ?? - questionsByLocale[defaultLocale] ?? - questionsByLocale.en ?? - []; - - const enItems = questionsByLocale.en || []; - const mappedRawItems = rawItems.map((item) => { - const enItem = enItems.find((e) => e.slug === item.slug); - if (!enItem) return item; - return { - ...item, - questions: item.questions.map((q, idx) => { - const enQ = enItem.questions[idx]; - return { - ...q, - englishTitle: enQ ? enQ.title : q.title, - }; - }), - }; - }); - - const items = mappedRawItems.map(mapQuestionListItem); - - const fbIndex = items.findIndex((item) => item.slug === "family_background"); - const mhIndex = items.findIndex( - (item) => item.slug === "marital_history_children", - ); - - if (fbIndex !== -1 && mhIndex !== -1) { - const fbItem = items[fbIndex]; - const mhItem = items[mhIndex]; - - const dict = getDictionary(locale); - const mergedTitle = - (dict as any)["Family Background, Marital Status, and Children"] || - "Family Background, Marital Status, and Children"; - const mergedEstimate = (dict as any)["20 minutes"] || "20 minutes"; - - const fbQuestions = fbItem.questions.map((q, idx) => ({ - ...q, - originalSlug: "family_background", - originalIndex: idx, - })); - - const mhQuestions = mhItem.questions.map((q, idx) => ({ - ...q, - originalSlug: "marital_history_children", - originalIndex: idx, - })); - - const finalQuestions = [...fbQuestions, ...mhQuestions]; - - const mergedItem: QuestionListItem = { - slug: "family_marital_history", - title: mergedTitle, - estimate: mergedEstimate, - progress: 0, - icon: "family_marital", - required: fbItem.required || mhItem.required, - showInfoBadge: fbItem.showInfoBadge || mhItem.showInfoBadge, - summary: `${fbItem.summary}\n\n${mhItem.summary}`, - checkpoints: [...fbItem.checkpoints, ...mhItem.checkpoints], - tooltip: fbItem.tooltip || mhItem.tooltip, - questions: finalQuestions, - }; - - const newItems = [...items]; - newItems[fbIndex] = mergedItem; - 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; - } - - // 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; -} - -export function getQuestionListItemBySlug( - slug: string, - locale: Locale = defaultLocale, -) { - return getQuestionListItems(locale).find((item) => item.slug === slug); -} - -function matchesAudienceRule( - rule: QuestionAudienceRule | undefined, - profile: { - age?: number | null; - gender?: MarriageGender | null; - }, -) { - if (!rule) { - return true; - } - - if ( - rule.genders?.length && - profile.gender && - !rule.genders.includes(profile.gender) - ) { - return false; - } - - if ( - typeof rule.minAge === "number" && - profile.age != null && - profile.age < rule.minAge - ) { - return false; - } - - if ( - typeof rule.maxAge === "number" && - profile.age != null && - profile.age > rule.maxAge - ) { - return false; - } - - return true; -} - -export function isQuestionListItemVisibleForProfile( - item: QuestionListItem, - profile: { - age?: number | null; - gender?: MarriageGender | null; - }, -) { - return matchesAudienceRule(item.audience, profile); -} - -export function isQuestionVisibleForProfile( - question: QuestionField, - profile: { - age?: number | null; - gender?: MarriageGender | null; - }, -) { - return matchesAudienceRule(question.audience, profile); -} - -export function isQuestionRequiredForProfile( - question: QuestionField, - profile: { - age?: number | null; - gender?: MarriageGender | null; - }, -) { - return ( - question.required || - (Boolean(question.requiredWhen) && - matchesAudienceRule(question.requiredWhen, profile)) - ); -} - -export function getRequiredQuestionsCount( - slug: string, - profile: { - age?: number | null; - gender?: MarriageGender | null; - }, - locale: Locale = defaultLocale, -) { - const item = getQuestionListItemBySlug(slug, locale); - if (!item) return 0; - - return item.questions.filter((q) => isQuestionRequiredForProfile(q, profile)) - .length; -} diff --git a/src/data/questions/en.json b/src/data/questions/en.json deleted file mode 100644 index 083953d..0000000 --- a/src/data/questions/en.json +++ /dev/null @@ -1,1870 +0,0 @@ -[ - { - "title": "Personal and identity details", - "icon": "user-circle", - "slug": "personal_info", - "required": true, - "estimateTime": "2 minutes", - "progress": 0, - "description": "Collects personal details to start the marriage application flow.", - "questions": [ - { - "title": "Full Name", - "type": "text", - "required": true, - "extras": { - "placeHolder": "Sarah Smith", - "range": [0, 0], - "options": [] - }, - "private": false - }, - { - "title": "Date of Birth", - "type": "date", - "required": true, - "extras": { - "placeHolder": "YYYY-MM-DD", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Age", - "type": "number", - "required": false, - "extras": { - "placeHolder": "", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Birthplace", - "type": "birthplace", - "required": true, - "extras": { - "placeHolder": "City, region, or neighborhood", - "range": [0, 0], - "options": [ - "Iran", - "United States", - "United Kingdom", - "Canada", - "Germany", - "France", - "United Arab Emirates", - "Turkey", - "Iraq", - "Afghanistan", - "Pakistan", - "Saudi Arabia", - "Qatar", - "Sweden", - "Netherlands", - "Norway", - "Australia", - "Other" - ] - } - }, - { - "title": "Current Nationality / Citizenship", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select country", - "range": [0, 0], - "options": [ - "Iran", - "United States", - "United Kingdom", - "Canada", - "Germany", - "France", - "United Arab Emirates", - "Turkey", - "Iraq", - "Afghanistan", - "Pakistan", - "Saudi Arabia", - "Qatar", - "Sweden", - "Netherlands", - "Norway", - "Australia", - "Other" - ], - "noSearch": false - } - }, - { - "title": "Ethnicity / Family Origin / Race", - "type": "text", - "required": true, - "extras": { - "placeHolder": "British", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Mother Tongue", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Persian", - "English", - "Arabic", - "Turkish", - "Urdu", - "Kurdish", - "Balochi", - "French", - "German", - "Spanish", - "Other" - ], - "noSearch": false - } - }, - { - "title": "Other Languages Fluent In", - "type": "dropdown", - "required": false, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Persian", - "English", - "Arabic", - "Turkish", - "Urdu", - "Kurdish", - "Balochi", - "French", - "German", - "Spanish", - "Other" - ], - "noSearch": false - } - } - ] - }, - { - "title": "Contact, Residence, and Family Communication", - "icon": "file-text", - "slug": "contact_residence_family_communication", - "required": true, - "estimateTime": "2 minutes", - "progress": 0, - "description": "Contact details and residence.", - "questions": [ - { - "title": "Personal Contact Number", - "type": "phone", - "required": true, - "extras": { - "placeHolder": "+44 7911 123456", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Personal Email", - "type": "text", - "required": true, - "extras": { - "placeHolder": "user@example.com", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Current Residence", - "type": "birthplace", - "required": true, - "tooltip": "A precise address is not required. Just the general area of where you live is sufficient, such as the city, region, neighborhood, or nearest major city.", - "extras": { - "placeHolder": "City, region, or neighborhood", - "range": [0, 0], - "options": [ - "Iran", - "United States", - "United Kingdom", - "Canada", - "Germany", - "France", - "United Arab Emirates", - "Turkey", - "Iraq", - "Afghanistan", - "Pakistan", - "Saudi Arabia", - "Qatar", - "Sweden", - "Netherlands", - "Norway", - "Australia", - "Other" - ] - } - }, - { - "title": "Residence Status", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Citizen / National", - "Permanent Residence", - "Temporary Residence", - "Student Visa", - "Work Visa", - "Refugee / Humanitarian Protection", - "Processing / Pending Residence Status" - ], - "noSearch": true - } - }, - { - "title": "Willingness to Relocate", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Fully flexible; moving to another city or country is not a problem.", - "Willing to move to another city, but only within my current country.", - "Only willing to live in my current city; relocating is a red line.", - "Will decide based on my future spouse's job, family, residence, and life circumstances." - ] - }, - "private": true - }, - { - "title": "Representative's Full Name", - "type": "text", - "required": false, - "requiredWhen": { - "genders": ["female"], - "maxAge": 26 - }, - "showGuardianNotice": true, - "tooltip": "This field specifies the full name of the designated intermediary whose contact information is provided to the other party to facilitate communication.", - "extras": { - "placeHolder": "Sarah Smith", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Relationship to Representative", - "type": "dropdown", - "required": false, - "requiredWhen": { - "genders": ["female"], - "maxAge": 26 - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Father", - "Mother", - "Brother", - "Sister", - "Paternal / Maternal Uncle", - "Paternal / Maternal Aunt", - "Trusted Family Friend", - "Religious / Clerical Sponsor", - "Trusted Social Sponsor" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Representative's Contact Number", - "type": "phone", - "required": false, - "requiredWhen": { - "genders": ["female"], - "maxAge": 26 - }, - "extras": { - "placeHolder": "+44 7911 123456", - "range": [0, 0], - "options": [] - }, - "private": true - } - ] - }, - { - "title": "Physical Appearance, Health, and Physical Activity", - "icon": "heart-handshake", - "slug": "appearance_health_activity", - "required": true, - "estimateTime": "2 minutes", - "progress": 0, - "description": "Collects details about your physical appearance, health status, and mental well-being.", - "questions": [ - { - "title": "Height in Centimeters", - "type": "scale", - "required": true, - "extras": { - "placeHolder": "175", - "range": [100, 230], - "options": [] - }, - "private": true - }, - { - "title": "Weight in Kilograms", - "type": "scale", - "required": true, - "extras": { - "placeHolder": "70", - "range": [40, 180], - "options": [] - }, - "private": true - }, - { - "title": "Skin Color", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Fair / White", - "Light Tan / Wheatish", - "Dark Tan / Brown", - "Dark / Black" - ] - } - }, - { - "title": "Physical Health Status", - "type": "radio", - "required": true, - "private": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "I am in perfect health.", - "I have a specific or chronic illness.", - "I have a physical deformity, disability, or limitation." - ] - } - }, - { - "title": "Physical Health Description", - "type": "text", - "required": true, - "private": true, - "logic": { - "dependsOn": { - "title": "Physical Health Status", - "values": [ - "I have a specific or chronic illness.", - "I have a physical deformity, disability, or limitation." - ] - } - }, - "tooltip": "Provide more details if you have any health conditions or limitations.", - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Mental Health Status", - "type": "radio", - "required": true, - "private": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "I have no specific issues.", - "I have a history of counseling or am currently undergoing treatment.", - "I am undergoing pharmacotherapy." - ] - } - }, - { - "title": "Use of Permanent Medications", - "type": "text", - "required": true, - "logic": { - "dependsOn": { - "title": "Mental Health Status", - "values": ["I am undergoing pharmacotherapy."] - } - }, - "tooltip": "This field requires the user to declare all permanent medications currently being taken for any physical, psychological, medical, or non-medical condition.", - "extras": { - "placeHolder": "Insulin, etc.", - "range": [0, 0], - "options": [] - }, - "private": true - } - ] - }, - { - "title": "Education, Career, and Economic Status", - "icon": "school", - "slug": "education_career_economic_status", - "required": true, - "estimateTime": "3 minutes", - "progress": 0, - "description": "Collects information about your educational background, employment status, and financial situation.", - "questions": [ - { - "title": "Highest Level of Education", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Below High School", - "High School Diploma", - "Associate Degree", - "Professional Certificate", - "Technical or Vocational Training", - "Bachelor’s Degree", - "Master’s Degree", - "Doctorate and Above", - "Religious / Clerical Studies" - ], - "noSearch": true - } - }, - { - "title": "Field of Study", - "type": "text", - "required": false, - "extras": { - "placeHolder": "Computer Science", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Employment Status", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Full-time Employed", - "Part-time Employed", - "Self-employed / Freelancer", - "Entrepreneur / Business Owner", - "Student", - "Working Student", - "Student and Job Seeking", - "Job Seeking / Unemployed", - "Homemaker", - "Retired" - ], - "noSearch": true - } - }, - { - "title": "Job Title", - "type": "text", - "required": false, - "extras": { - "placeHolder": "Software Engineer", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Work Location", - "type": "text", - "required": false, - "extras": { - "placeHolder": "London, Remote", - "range": [0, 0], - "options": [] - } - }, - { - "title": "Monthly Income", - "type": "number", - "required": true, - "extras": { - "placeHolder": "3500 GBP, 4000 USD", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Overall Financial Status", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Stable and reliable income", - "Income is variable", - "At the start of career and financial path", - "Partially supported by family", - "No independent income" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Ability to Support Marriage Expenses", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Fully able to support expenses", - "Able to support the main portion of expenses", - "Need future partner's financial participation", - "Currently building suitable financial conditions", - "Depends on the country and future residence" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Current Housing Status", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Homeowner", - "Renting independently", - "Living with family / parents", - "Dormitory / Student housing", - "Organizational housing", - "Temporary conditions" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Post-Marriage Housing Plan", - "type": "radio", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Have a personal home for living together", - "Can buy a home", - "Will likely rent at the start", - "May temporarily live with family at the start" - ] - }, - "private": true - }, - { - "title": "Additional Comments on Economic and Housing Status", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "Enter your explanation here...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "If you have any special conditions regarding work, income, renting, home buying, migration, or future place of residence, please explain briefly." - } - ] - }, - { - "title": "Family Background", - "icon": "user-circle", - "slug": "family_background", - "required": true, - "estimateTime": "3 minutes", - "progress": 0, - "description": "Information about your siblings, parents, and family lifestyle.", - "questions": [ - { - "title": "Number of Siblings", - "type": "number", - "required": true, - "extras": { - "placeHolder": "2", - "range": [0, 20], - "options": [] - } - }, - { - "title": "Parents' Survival Status", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Both parents are alive", - "Father has passed away", - "Mother has passed away", - "Both parents have passed away" - ] - } - }, - { - "title": "Parents' Marital Status", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Living together", - "Separated / Divorced", - "I have special family circumstances and will provide the details in the description." - ] - }, - "private": true - }, - { - "title": "Do you currently have an ongoing financial, caregiving, or guardianship responsibility for a family member?", - "type": "checkbox", - "required": true, - "extras": { - "placeHolder": "Select option(s)", - "range": [0, 0], - "options": [ - "No, I do not have any ongoing responsibility.", - "I am responsible for caring for my parent(s) (father, mother, or both).", - "I am responsible for the care, custody, or guardianship of other family members (sibling, etc.).", - "I regularly provide financial support for a family member's living expenses." - ] - }, - "private": true - }, - { - "title": "Do the supported individual(s) live with you?", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Yes, they live with me permanently.", - "Yes, they live with me temporarily or periodically.", - "No, but they reside near my place of living.", - "No, they live in another city or country." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Do you currently have an ongoing financial, caregiving, or guardianship responsibility for a family member?", - "values": [ - "I am responsible for caring for my parent(s) (father, mother, or both).", - "I am responsible for the care, custody, or guardianship of other family members (sibling, etc.).", - "I regularly provide financial support for a family member's living expenses." - ] - } - } - }, - { - "title": "Additional details about family responsibility", - "type": "textarea", - "required": true, - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "familyResponsibilityTooltip", - "logic": { - "dependsOn": { - "title": "Do you currently have an ongoing financial, caregiving, or guardianship responsibility for a family member?", - "values": [ - "I am responsible for caring for my parent(s) (father, mother, or both).", - "I am responsible for the care, custody, or guardianship of other family members (sibling, etc.).", - "I regularly provide financial support for a family member's living expenses." - ] - } - } - }, - { - "title": "Family's Religious and Ideological Atmosphere", - "type": "radio", - "required": false, - "description": "Please select the option that best describes the general atmosphere and lifestyle of your family.", - "tooltip": "### Family Religious Atmosphere Options\n\n* **Religious and Strictly Observant:** This specifies a family highly dedicated to performing all **obligatory duties**, strictly maintaining **religious boundaries** (such as Mahram rules), and upholding **religious rituals and teachings** across all aspects of life.\n* **Religious (Observant of Obligations):** This indicates a family committed to core **religious duties** (such as prayer and fasting) and **Islamic ethics**, living within the standard frameworks of a religious society.\n* **Traditional (Respectful of Religious Values):** This describes a family that holds devotion to moral values and **respects religion**, but may not strictly execute every specific **religious law** or obligation.\n* **Non-religious / Secular:** This represents a family where **religious rituals and frameworks** do not significantly influence their daily **lifestyle, relationships, or decisions**, despite holding a general respect for religion.", - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Religious and strictly observant", - "Religious (observant of obligations)", - "Traditional (respectful of religious values)", - "Non-religious / Secular" - ] - } - }, - { - "title": "Family Economic Status", - "type": "radio", - "required": false, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": ["Weak", "Average", "Good", "Prosperous"] - } - }, - { - "title": "Short Family Description", - "type": "textarea", - "required": true, - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - }, - "tooltip": "Write any important point that was not covered in the options above here." - } - ] - }, - { - "title": "Marital Status, Marriage History, and Children", - "icon": "heart", - "slug": "marital_history_children", - "required": true, - "estimateTime": "3 minutes", - "progress": 0, - "description": "Information about your current marital status, previous marriages, and children.", - "questions": [ - { - "title": "Current Marital Status", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Single; never married", - "Failed engagement / Annulled marriage; without living together", - "Divorced; after living together", - "Widowed" - ] - }, - "private": true, - "tooltip": "currentMaritalStatusTooltip" - }, - { - "title": "Previous Marriage Duration", - "type": "text", - "required": false, - "logic": { - "dependsOn": { - "title": "Current Marital Status", - "values": [ - "Failed engagement / Annulled marriage; without living together", - "Divorced; after living together", - "Widowed" - ] - } - }, - "extras": { - "placeHolder": "3 years", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Reason for Separation", - "type": "text", - "required": false, - "logic": { - "dependsOn": { - "title": "Current Marital Status", - "values": [ - "Failed engagement / Annulled marriage; without living together", - "Divorced; after living together" - ] - } - }, - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Children and Guardianship Status", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "No children", - "Have children living with me", - "Have children not living with me", - "Someone else is under my guardianship" - ] - }, - "private": true - }, - { - "title": "Number of Children", - "type": "number", - "required": false, - "logic": { - "dependsOn": { - "title": "Current Marital Status", - "values": ["Divorced; after living together", "Widowed"] - } - }, - "extras": { - "placeHolder": "2", - "range": [0, 10], - "options": [] - }, - "private": true - }, - { - "title": "What is the custody status of your child(ren)?", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "I have full custody of the child(ren).", - "Joint custody or periodic visitation/relocation between parents.", - "Custody is with the other parent or another person.", - "Children have reached legal age (custody is not applicable).", - "Other circumstances (dispute, pending, or other)." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Children and Guardianship Status", - "values": [ - "Have children living with me", - "Have children not living with me" - ] - } - } - }, - { - "title": "How much time do the child(ren) usually live with you?", - "type": "radio", - "required": false, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Permanently or most days of the week with me.", - "Approximately half the time (joint custody/schedule).", - "Weekends, holidays, or specific days only.", - "They do not live with me, or there is no fixed schedule." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Children and Guardianship Status", - "values": [ - "Have children living with me", - "Have children not living with me" - ] - } - } - }, - { - "title": "Does the custody, visitation, or relocation schedule impact your residence or immigration?", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "No, it has no significant impact on residence or relocation.", - "Yes, relocation or immigration requires coordination, agreement, or a legal permit.", - "Yes, I am restricted and must reside in the same city or region.", - "Unclear (pending dispute/agreement or depends on conditions)." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Children and Guardianship Status", - "values": [ - "Have children living with me", - "Have children not living with me" - ] - } - } - }, - { - "title": "What is the payment or receipt status of child support?", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Ongoing financial commitment (paying child support or sharing expenses).", - "Receiving child support regularly.", - "Payments are case-by-case, agreed, or irregular.", - "No formal child support commitment (or child is independent / pending)." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Children and Guardianship Status", - "values": [ - "Have children living with me", - "Have children not living with me" - ] - } - } - }, - { - "title": "Acceptance of necessary communication between future spouse and the other parent", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "I accept necessary, respectful, and limited communication regarding child matters.", - "I only accept formal and highly limited communication regarding essential child matters.", - "I prefer communication to go through an intermediary, a family member, or a lawyer as much as possible.", - "Any ongoing communication beyond essential child matters with the other parent is a red line for me.", - "Acceptance depends on the type and extent of communication, custody conditions, and mutual trust." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Children and Guardianship Status", - "values": [ - "Have children living with me", - "Have children not living with me" - ] - } - } - }, - { - "title": "Short Children/Guardianship Explanation", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - }, - "private": true - } - ] - }, - { - "title": "Beliefs, Lifestyle, and Personal Boundaries", - "icon": "shield-check", - "slug": "beliefs_lifestyle_boundaries", - "required": true, - "estimateTime": "4 minutes", - "progress": 0, - "description": "Details about religious practice, public appearance, political outlook, habits, and lifestyle preferences.", - "questions": [ - { - "title": "Marja' al-Taqlid (Religious Authority)", - "type": "text", - "required": true, - "extras": { - "placeHolder": "Ayatollah Sistani", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Commitment to Obligatory Prayers", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Always committed, preferably at the earliest time", - "Always committed, but not necessarily at the earliest time", - "Sometimes", - "Do not pray" - ] - }, - "private": true - }, - { - "title": "Commitment to Ramadan Fasting", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Fully committed", - "Do not fast for religious or medical reasons", - "Occasionally do not fast without a specific reason", - "Not committed" - ] - }, - "private": true - }, - { - "title": "Type of Hijab and Public Appearance", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Full Islamic covering (Maximum Hijab) - Abaya, Jilbab, Chador, or Niqab with full observance.", - "Full Hijab with modest clothing - Modest styling with hair completely covered.", - "Customary covering - Modest everyday clothing with general hair covering.", - "No Hijab (Modest styling) - Dignified modest attire without headscarf.", - "No Hijab (Casual/Modern) - Modern styling and casual outfits." - ], - "noSearch": 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", - "type": "radio", - "required": false, - "audience": { - "genders": ["female"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Do not wear makeup at all", - "Only very light makeup", - "Full makeup" - ] - }, - "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", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Religion and politics are inseparable; my spouse must share this outlook.", - "Religion and politics are inseparable, but active engagement is not a requirement for my spouse.", - "Traditional and non-political view of Shiasm; cannot marry someone with a political view.", - "Non-political view of Shiasm, but it's not a red line if my spouse has political views.", - "These concepts and categories are not a major concern for me." - ], - "noSearch": 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", - "type": "dropdown", - "required": false, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Supporter of the current government; serious opposition from my spouse is a red line.", - "Supporter of the current government, but a difference in view is not a red line.", - "Opposed to the current government; serious support from my spouse is a red line.", - "Opposed to the current government, but a difference in view is not a red line.", - "No specific stance; political differences are not significant for my marriage." - ], - "noSearch": true - }, - "private": true, - "tooltip": "This section is designed to prevent serious ideological conflicts in married life." - }, - { - "title": "Boundaries with the Opposite Sex", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Very formal and limited (Only as necessary) - Avoid any unnecessary conversation or jokes.", - "Respectful and conventional (No intimacy) - Polite interactions with clear personal boundaries.", - "Social and comfortable (Within religious limits) - Active in social circles within moral limits.", - "No specific boundaries - Fully comfortable with modern social interactions." - ] - }, - "private": true, - "tooltip": "Please select the option that best describes your daily behavior when interacting with members of the opposite sex." - }, - { - "title": "Smoking", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Do not smoke at all", - "Occasional / Recreational", - "Regular smoker", - "Quitting" - ] - }, - "private": true - }, - { - "title": "Hookah", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Do not smoke hookah at all", - "Occasional / Recreational", - "Regular hookah smoker", - "Quitting" - ] - }, - "private": true - }, - { - "title": "Vape / E-cigarettes", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Do not use at all", - "Occasional / Recreational", - "Regular user", - "Quitting" - ] - }, - "private": true - }, - { - "title": "Alcoholic Beverages", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Do not consume at all", - "Alcohol is a serious red line for me", - "Occasional consumption", - "Regular consumption" - ] - }, - "private": true - }, - { - "title": "Narcotics or Illegal Substances", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Never used", - "Used in the past, but not anymore", - "Current user", - "In treatment or recovery" - ] - }, - "private": true - }, - { - "title": "Attitude towards Music", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Do not listen to any music", - "Only listen to Nasheeds, Acapella, religious, or instrument-free music", - "Listen to Halal and permissible music", - "No specific sensitivity towards music types" - ] - }, - "private": true - }, - { - "title": "Attitude towards Wedding Ceremony", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "No ceremony or very simple", - "Strictly religious and gender-segregated", - "Respectful mixed ceremony, no dancing/non-permissible music", - "Mixed ceremony with music and dancing", - "Undecided; depends on family agreement" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Your Personality Traits", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [1, 10], - "options": [ - "Calm and Introverted", - "Social and Extroverted", - "Emotional", - "Logical", - "Humorous", - "Serious", - "Communicative", - "Family-oriented", - "Independent", - "Responsible", - "Patient", - "Organized", - "Planner", - "Flexible", - "Sensitive and Precise", - "Dedicated to Personal Growth" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Your Hobbies and Main Interests", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [1, 15], - "options": [ - "Quran Recitation and Religious Studies", - "Mosque and Religious Gatherings", - "Religious and Cultural Activities", - "Pilgrimage Trips", - "Tourism and Travel", - "Nature and Outdoors", - "Sports / Exercise", - "Reading", - "Movies and Cinema", - "Cooking", - "Art", - "Music", - "Board Games / Puzzles", - "Social and Charity Work", - "Cafes and Restaurants", - "Language Learning", - "Technology and Computers" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Short explanation about your lifestyle", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "Write any important point that was not covered in the options above here." - } - ] - }, - { - "title": "Personality Test", - "icon": "layout-grid", - "slug": "personality_test", - "required": false, - "estimateTime": "8 minutes", - "progress": 0, - "description": "Personality Test", - "questions": [] - }, - { - "title": "Glasser 5 Needs Test", - "icon": "layout-grid", - "slug": "glasser_5_needs_test", - "required": false, - "estimateTime": "5 minutes", - "progress": 0, - "description": "Glasser 5 Needs Test", - "questions": [] - }, - { - "title": "Future Spouse Criteria and Red Lines", - "icon": "shield-check", - "slug": "future_spouse_criteria", - "required": true, - "estimateTime": "6 minutes", - "progress": 0, - "description": "Criteria and Red Lines.", - "questions": [ - { - "title": "Desired Height Range of Future Spouse", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Under 160", - "160 to 170", - "170 to 180", - "180 to 190", - "Above 190" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Body Type of Future Spouse", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Slim", - "Fit/Average", - "Athletic", - "Curvy/Full", - "Large frame" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Skin Color of Future Spouse", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Fair / White", - "Light Tan / Wheatish", - "Dark Tan / Brown", - "Dark / Black" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Ethnicity, Language, or Nationality of Future Spouse", - "type": "text", - "required": false, - "description": "", - "extras": { - "placeHolder": "English, French, etc.", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "Minimum Education Level of Future Spouse", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Minimum High School", - "Minimum Bachelor's", - "Minimum Master's", - "Doctorate or higher preferred", - "Must have religious studies", - "Maturity is more important than degree" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Employment Status of Future Spouse", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Employed", - "Student", - "Homemaker", - "Entrepreneur / Business Owner", - "In career growth path", - "Negotiable" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Acceptance of Future Spouse's Marriage History", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Never married only; history is a red line", - "Never married preferred, but open to special cases", - "No difference", - "Depends on reason, duration, and conditions" - ] - }, - "private": true - }, - { - "title": "Acceptance of Children from Previous Marriage", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Will not accept", - "Accept in special conditions", - "No difference", - "Only if children don't live with them" - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "Acceptance of Future Spouse's Marriage History", - "values": [ - "No difference", - "Depends on reason, duration, and conditions" - ] - } - } - }, - { - "title": "Desired Clothing style for Future Spouse", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Full Islamic covering mandatory", - "Sharia Hijab mandatory, type doesn't matter", - "Customary clothing with Hijab acceptable", - "Modest clothing important, details negotiable", - "No specific sensitivity" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Clothing style for Future Spouse", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["female"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Formal, dignified, and religious", - "Simple, neat, and modest", - "Regular and well-groomed", - "Modern style okay", - "No specific sensitivity" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Spouse's Tendency for Further Education", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Must intend to continue", - "Positive but not mandatory", - "Prefer not to continue after marriage", - "Negotiable" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Spouse's Tendency for Employment", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["female"] - }, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Must be employed", - "Must be a homemaker", - "Up to them", - "Negotiable", - "Compatible with religious values" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Desired Spouse's Family Status and Values", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [0, 0], - "options": [ - "Doesn't matter.", - "Parents not divorced", - "Father alive", - "Mother alive", - "Religious family atmosphere", - "Respectful family communication" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Level of Family Communication Post-Marriage", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Close and active", - "Respectful but independent", - "Limited and controlled", - "Based on conditions" - ] - }, - "private": true - }, - { - "title": "Desired Level of Religious Commitment", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Very religious and committed", - "Moderate religious", - "Customary but respectful", - "Not important" - ] - }, - "private": true - }, - { - "title": "Desired Political Outlook", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Must align with mine", - "Differences okay with mutual respect", - "Prefer non-political", - "Not important" - ] - }, - "private": true - }, - { - "title": "Future Spouse's Boundaries with the Opposite Sex", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Very formal and limited", - "Normal and respectful", - "Social within religious/moral limits" - ] - }, - "private": true - }, - { - "title": "Red Lines for Smoking, Alcohol, and Substances", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select options", - "range": [1, 10], - "options": [ - "Smoking is a red line", - "Hookah is a red line", - "Vape is a red line", - "Alcohol is a serious red line", - "Drugs are a definite red line", - "Occasional smoking okay in special cases", - "None are red lines" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Attitude towards Music", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Should not listen", - "Only religious/instrument-free okay", - "Halal/permissible okay", - "No sensitivity" - ] - }, - "private": true - }, - { - "title": "Attitude towards Wedding Ceremony", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Simple or no ceremony", - "Strictly religious and segregated", - "Respectful mixed without non-sharia elements", - "Mixed with music and dancing", - "Based on family agreement" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Acceptance of Chronic Illness or Disability", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Red line", - "Consider in special cases", - "Accept if not hindering healthy life", - "Case-by-case with consultation" - ] - }, - "private": true - }, - { - "title": "Acceptance of Psychological Counseling History", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "No problem", - "Depends on stability", - "Red line", - "Needs serious review" - ] - }, - "private": true - }, - { - "title": "Residence Preference after Marriage", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Country doesn't matter", - "Stay in my current country", - "Move to spouse's current country", - "Only my current city", - "Third country", - "Based on conditions" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "Preference for Living with Family", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "Select one option", - "range": [0, 0], - "options": [ - "Only independent life", - "Temporary with family okay", - "Living with either family okay", - "Based on conditions" - ] - }, - "private": true - }, - { - "title": "Additional Comments and Red Lines", - "type": "textarea", - "required": true, - "description": "", - "extras": { - "placeHolder": "Enter details here...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "Write any important point that was not covered in the options above here." - } - ] - }, - { - "title": "Identity Verification and Documents", - "icon": "shield-check", - "slug": "identity_verification", - "required": true, - "estimateTime": "2 minutes", - "progress": 0, - "description": "Upload identity documents.", - "questions": [ - { - "title": "Profile Picture", - "type": "photo", - "required": true, - "private": true, - "description": "This field requires the user to upload a recent, clear facial photograph that will remain private and accessible exclusively to advisors.", - "extras": { - "placeHolder": "Upload photo", - "range": [0, 0], - "options": [".jpg", ".jpeg", ".png"] - } - }, - { - "title": "Valid Identification Document", - "type": "file", - "required": true, - "private": true, - "description": "Passport, National ID, or Driver's License", - "extras": { - "placeHolder": "Upload document", - "range": [0, 0], - "options": [".pdf", ".jpg", ".jpeg", ".png"] - } - }, - { - "title": "Confirmation of Document and Information Accuracy", - "type": "checkbox", - "required": true, - "extras": { - "placeHolder": "I confirm", - "range": [0, 0], - "options": [ - "I confirm that my name, age, photo, and identity details match the uploaded documents." - ] - }, - "private": true - } - ] - } -] diff --git a/src/data/questions/fa.json b/src/data/questions/fa.json deleted file mode 100644 index fad0120..0000000 --- a/src/data/questions/fa.json +++ /dev/null @@ -1,1870 +0,0 @@ -[ - { - "title": "بخش ۱: مشخصات فردی و هویتی", - "icon": "user-circle", - "slug": "personal_info", - "required": true, - "estimateTime": "۲ دقیقه", - "progress": 0, - "description": "Collects personal details to start the marriage application flow.", - "questions": [ - { - "title": "نام و نام خانوادگی", - "type": "text", - "required": true, - "extras": { - "placeHolder": "سارا اسمیت", - "range": [0, 0], - "options": [] - }, - "private": false - }, - { - "title": "تاریخ تولد", - "type": "date", - "required": true, - "extras": { - "placeHolder": "YYYY-MM-DD", - "range": [0, 0], - "options": [] - } - }, - { - "title": "سن", - "type": "number", - "required": false, - "extras": { - "placeHolder": "", - "range": [0, 0], - "options": [] - } - }, - { - "title": "محل تولد", - "type": "birthplace", - "required": true, - "extras": { - "placeHolder": "شهر، منطقه یا محله", - "range": [0, 0], - "options": [ - "ایران", - "ایالات متحده", - "بریتانیا", - "کانادا", - "آلمان", - "فرانسه", - "امارات متحده عربی", - "ترکیه", - "عراق", - "افغانستان", - "پاکستان", - "عربستان سعودی", - "قطر", - "سوئد", - "هلند", - "نروژ", - "استرالیا", - "سایر" - ] - } - }, - { - "title": "ملیت / تابعیت فعلی", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کشور", - "range": [0, 0], - "options": [ - "ایران", - "ایالات متحده", - "بریتانیا", - "کانادا", - "آلمان", - "فرانسه", - "امارات متحده عربی", - "ترکیه", - "عراق", - "افغانستان", - "پاکستان", - "عربستان سعودی", - "قطر", - "سوئد", - "هلند", - "نروژ", - "استرالیا", - "سایر" - ], - "noSearch": false - } - }, - { - "title": "قومیت / اصلیت خانوادگی / نژاد", - "type": "text", - "required": true, - "extras": { - "placeHolder": "انگلستان", - "range": [0, 0], - "options": [] - } - }, - { - "title": "زبان مادری", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "فارسی", - "انگلیسی", - "عربی", - "ترکی", - "اردو", - "کردی", - "بلوچی", - "فرانسوی", - "آلمانی", - "اسپانیایی", - "سایر" - ], - "noSearch": false - } - }, - { - "title": "سایر زبان‌هایی که به آن‌ها مسلط هستم", - "type": "dropdown", - "required": false, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "فارسی", - "انگلیسی", - "عربی", - "ترکی", - "اردو", - "کردی", - "بلوچی", - "فرانسوی", - "آلمانی", - "اسپانیایی", - "سایر" - ], - "noSearch": false - } - } - ] - }, - { - "title": "بخش ۲: اطلاعات تماس، سکونت و ارتباط خانوادگی", - "icon": "file-text", - "slug": "contact_residence_family_communication", - "required": true, - "estimateTime": "۲ دقیقه", - "progress": 0, - "description": "اطلاعات تماس و سکونت.", - "questions": [ - { - "title": "شماره تماس شخصی", - "type": "phone", - "required": true, - "extras": { - "placeHolder": "+44 7911 123456", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "ایمیل", - "type": "text", - "required": true, - "extras": { - "placeHolder": "user@example.com", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "محل سکونت فعلی", - "type": "birthplace", - "required": true, - "tooltip": "نیازی به آدرس دقیق نیست. فقط محدوده کلی محل زندگی کافی است؛ مثلاً نام شهر، منطقه، ناحیه یا نزدیکترین شهر بزرگ.", - "extras": { - "placeHolder": "شهر، منطقه یا محله", - "range": [0, 0], - "options": [ - "ایران", - "ایالات متحده", - "بریتانیا", - "کانادا", - "آلمان", - "فرانسه", - "امارات متحده عربی", - "ترکیه", - "عراق", - "افغانستان", - "پاکستان", - "عربستان سعودی", - "قطر", - "سوئد", - "هلند", - "نروژ", - "استرالیا", - "سایر" - ] - } - }, - { - "title": "وضعیت اقامت در کشور فعلی", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "شهروند / دارای تابعیت", - "اقامت دائم", - "اقامت موقت", - "ویزای تحصیلی", - "ویزای کاری", - "پناهندگی / حمایت بشردوستانه", - "در حال پیگیری وضعیت اقامت" - ], - "noSearch": true - } - }, - { - "title": "تمایل به جابجایی و مهاجرت پس از ازدواج", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "کاملاً منعطف هستم؛ جابجایی به شهر یا کشور دیگر برایم مشکلی ندارد.", - "حاضرم به شهر دیگری نقل مکان کنم، اما فقط در کشور فعلی خودم حاضر به زندگی هستم.", - "فقط در شهر فعلی خودم حاضر به زندگی هستم و جابجایی برایم خط قرمز است.", - "بسته به شرایط شغلی، خانوادگی، اقامتی و زندگی همسر آینده‌ام تصمیم می‌گیرم." - ] - }, - "private": true - }, - { - "title": "نام و نام خانوادگی رابط", - "type": "text", - "required": false, - "requiredWhen": { - "genders": ["female"], - "maxAge": 26 - }, - "showGuardianNotice": true, - "tooltip": "این فیلد نام و نام خانوادگی رابط مشخص‌شده را تعیین می‌کند که اطلاعات تماس او جهت تسهیل ارتباط در اختیار طرف مقابل قرار می‌گیرد.", - "extras": { - "placeHolder": "سارا اسمیت", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "نسبت رابط با شما", - "type": "dropdown", - "required": false, - "requiredWhen": { - "genders": ["female"], - "maxAge": 26 - }, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "پدر", - "مادر", - "برادر", - "خواهر", - "عمو / دایی", - "خاله / عمه", - "دوست خانوادگی معتمد", - "معرف مذهبی / روحانی", - "معرف اجتماعی معتمد" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "شماره تماس رابط", - "type": "phone", - "required": false, - "requiredWhen": { - "genders": ["female"], - "maxAge": 26 - }, - "extras": { - "placeHolder": "+44 7911 123456", - "range": [0, 0], - "options": [] - }, - "private": true - } - ] - }, - { - "title": "بخش ۳: ویژگی‌های ظاهری، سلامت و فعالیت بدنی", - "icon": "heart-handshake", - "slug": "appearance_health_activity", - "required": true, - "estimateTime": "۲ دقیقه", - "progress": 0, - "description": "ثبت جزئیات مربوط به ظاهر فیزیکی، وضعیت سلامت جسمانی و روانی شما.", - "questions": [ - { - "title": "قد به سانتی‌متر", - "type": "scale", - "required": true, - "extras": { - "placeHolder": "۱۷۵", - "range": [100, 230], - "options": [] - }, - "private": true - }, - { - "title": "وزن به کیلوگرم", - "type": "scale", - "required": true, - "extras": { - "placeHolder": "۷۰", - "range": [40, 180], - "options": [] - }, - "private": true - }, - { - "title": "رنگ پوست", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "سفید / روشن", - "گندم‌گون / سبزه روشن", - "سبزه تیره / قهوه‌ای", - "تیره / سیاه‌پوست" - ] - } - }, - { - "title": "وضعیت سلامت جسمانی", - "type": "radio", - "required": true, - "private": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "در سلامت کامل هستم.", - "بیماری خاص یا مزمن دارم.", - "نقص عضو، معلولیت یا محدودیت جسمی دارم." - ] - } - }, - { - "title": "توضیحات سلامت جسمانی", - "type": "text", - "required": true, - "private": true, - "logic": { - "dependsOn": { - "title": "وضعیت سلامت جسمانی", - "values": [ - "بیماری خاص یا مزمن دارم.", - "نقص عضو، معلولیت یا محدودیت جسمی دارم." - ] - } - }, - "tooltip": "وضعیت بیماریهای مزمن، محدودیتهای جسمانی یا داروهای مصرفی خود را به دقت شرح داده و در صورت سلامت، عبارت «دارای سلامت کامل جسمانی» را در این کادر ثبت کنید.", - "extras": { - "placeHolder": "توضیحات را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - } - }, - { - "title": "وضعیت سلامت روان", - "type": "radio", - "required": true, - "private": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مشکل خاصی ندارم.", - "سابقه مشاوره داشته یا در حال حاضر تحت درمان هستم.", - "تحت دارو درمانی هستم." - ] - } - }, - { - "title": "استفاده از داروهای دائمی", - "type": "text", - "required": true, - "logic": { - "dependsOn": { - "title": "وضعیت سلامت روان", - "values": ["تحت دارو درمانی هستم."] - } - }, - "tooltip": "این فیلد برای ثبت تمامی داروهای دائمی است که در حال حاضر به دلایل جسمی، روانی، پزشکی یا غیرپزشکی مصرف می‌کنید.", - "extras": { - "placeHolder": "انسولین و غیره", - "range": [0, 0], - "options": [] - }, - "private": true - } - ] - }, - { - "title": "بخش ۴: تحصیلات، شغل و وضعیت اقتصادی", - "icon": "school", - "slug": "education_career_economic_status", - "required": true, - "estimateTime": "۳ دقیقه", - "progress": 0, - "description": "ثبت اطلاعات مربوط به پیشینه تحصیلی، وضعیت اشتغال و شرایط مالی شما.", - "questions": [ - { - "title": "بالاترین سطح تحصیلات", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "زیر دیپلم", - "دیپلم / High School", - "کاردانی / Associate", - "گواهینامه مهارت حرفه‌ای / Certificate", - "آموزش فنی یا مهارتی", - "کارشناسی / Bachelor’s Degree", - "کارشناسی ارشد / Master’s Degree", - "دکتری و بالاتر", - "تحصیلات حوزوی / علوم دینی" - ], - "noSearch": true - } - }, - { - "title": "رشته تحصیلی", - "type": "text", - "required": false, - "extras": { - "placeHolder": "مهندسی کامپیوتر", - "range": [0, 0], - "options": [] - } - }, - { - "title": "وضعیت اشتغال", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "شاغل تمام‌وقت", - "شاغل پاره‌وقت", - "خویش‌فرما / فریلنسر", - "کارآفرین / صاحب کسب‌وکار", - "دانشجو", - "دانشجو و شاغل", - "دانشجو و جویای کار", - "جویای کار / بیکار", - "خانه‌دار", - "بازنشسته" - ], - "noSearch": true - } - }, - { - "title": "عنوان شغلی", - "type": "text", - "required": false, - "extras": { - "placeHolder": "مهندس نرم‌افزار", - "range": [0, 0], - "options": [] - } - }, - { - "title": "محل فعالیت", - "type": "text", - "required": false, - "extras": { - "placeHolder": "لندن، دورکاری", - "range": [0, 0], - "options": [] - } - }, - { - "title": "میزان درآمد ماهانه", - "type": "number", - "required": true, - "extras": { - "placeHolder": "۳۵۰۰ پوند، ۴۰۰۰ دلار", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "وضعیت مالی کلی", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "درآمد پایدار و قابل اتکا دارم.", - "درآمد دارم، اما متغیر است.", - "در ابتدای مسیر شغلی و مالی هستم.", - "فعلاً بخشی از هزینه‌هایم توسط خانواده تأمین می‌شود.", - "فعلاً درآمد مستقل ندارم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "توانایی تأمین هزینه‌های زندگی مشترک", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "توانایی تأمین کامل هزینه‌های زندگی مشترک را دارم.", - "توانایی تأمین بخش اصلی هزینه‌ها را دارم.", - "نیاز به مشارکت مالی همسر آینده دارم.", - "فعلاً در حال ساختن شرایط مالی مناسب هستم.", - "این موضوع بستگی به کشور و محل زندگی آینده دارد." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "وضعیت مسکن فعلی", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مالک خانه شخصی هستم.", - "مستأجر هستم و مستقل زندگی می‌کنم.", - "همراه خانواده / والدین زندگی می‌کنم.", - "خوابگاه / مسکن دانشجویی", - "مسکن سازمانی", - "فعلاً شرایط موقت دارم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "برنامه یا توانایی تأمین مسکن بعد از ازدواج", - "type": "radio", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "خانه شخصی دارم و امکان زندگی مشترک در آن وجود دارد.", - "امکان خرید خانه دارم.", - "در ابتدای ازدواج احتمالاً مستأجر خواهیم بود.", - "در ابتدای ازدواج ممکن است موقتاً با خانواده زندگی کنیم." - ] - }, - "private": true - }, - { - "title": "توضیح تکمیلی درباره وضعیت اقتصادی و مسکن", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "توضیحات خود را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "اگر شرایط خاصی درباره کار، درآمد، اجاره، خرید خانه، مهاجرت یا محل زندگی آینده دارید، کوتاه توضیح دهید." - } - ] - }, - { - "title": "بخش ۵: پیشینه خانوادگی", - "icon": "user-circle", - "slug": "family_background", - "required": true, - "estimateTime": "۳ دقیقه", - "progress": 0, - "description": "اطلاعاتی درباره خواهر و برادرها، والدین و سبک زندگی خانواده شما.", - "questions": [ - { - "title": "تعداد خواهر و برادر", - "type": "number", - "required": true, - "extras": { - "placeHolder": "۲", - "range": [0, 20], - "options": [] - } - }, - { - "title": "وضعیت حیات والدین", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "هر دو در قید حیات هستند.", - "پدر فوت شده است.", - "مادر فوت شده است.", - "هر دو فوت شده‌اند." - ] - } - }, - { - "title": "وضعیت تأهل والدین", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "با هم زندگی می‌کنند.", - "از هم جدا شده‌اند / طلاق گرفته‌اند.", - "شرایط خانوادگی خاص دارم و در توضیحات می‌نویسم." - ] - }, - "private": true - }, - { - "title": "آیا در حال حاضر مسئولیت مستمر مالی، مراقبتی یا سرپرستی یکی از اعضای خانواده را بر عهده دارید؟", - "type": "checkbox", - "required": true, - "extras": { - "placeHolder": "انتخاب گزینه‌ها", - "range": [0, 0], - "options": [ - "خیر، مسئولیت مستمری ندارم.", - "مسئولیت مراقبت از والدین (پدر، مادر یا هر دو) را بر عهده دارم.", - "مسئولیت مراقبت، سرپرستی یا قیمومیت سایر اعضای خانواده (خواهر، برادر و...) را بر عهده دارم.", - "بهصورت منظم بخشی از هزینههای زندگی یا حمایت مالی یکی از اعضای خانواده را تأمین میکنم." - ] - }, - "private": true - }, - { - "title": "آیا فرد یا افراد تحت حمایت با شما زندگی میکنند؟", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "بله، بهصورت دائم با من زندگی میکنند.", - "بله، بهصورت موقت یا دورهای با من زندگی میکنند.", - "خیر، اما در نزدیکی محل زندگی من سکونت دارند.", - "خیر، در شهر یا کشور دیگری زندگی میکنند." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "آیا در حال حاضر مسئولیت مستمر مالی، مراقبتی یا سرپرستی یکی از اعضای خانواده را بر عهده دارید؟", - "values": [ - "مسئولیت مراقبت از والدین (پدر، مادر یا هر دو) را بر عهده دارم.", - "مسئولیت مراقبت، سرپرستی یا قیمومیت سایر اعضای خانواده (خواهر، برادر و...) را بر عهده دارم.", - "بهصورت منظم بخشی از هزینههای زندگی یا حمایت مالی یکی از اعضای خانواده را تأمین میکنم." - ] - } - } - }, - { - "title": "توضیحات تکمیلی درباره مسئولیت خانوادگی", - "type": "textarea", - "required": true, - "extras": { - "placeHolder": "توضیحات خود را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "familyResponsibilityTooltip", - "logic": { - "dependsOn": { - "title": "آیا در حال حاضر مسئولیت مستمر مالی، مراقبتی یا سرپرستی یکی از اعضای خانواده را بر عهده دارید؟", - "values": [ - "مسئولیت مراقبت از والدین (پدر، مادر یا هر دو) را بر عهده دارم.", - "مسئولیت مراقبت، سرپرستی یا قیمومیت سایر اعضای خانواده (خواهر، برادر و...) را بر عهده دارم.", - "بهصورت منظم بخشی از هزینههای زندگی یا حمایت مالی یکی از اعضای خانواده را تأمین میکنم." - ] - } - } - }, - { - "title": "فضای مذهبی و اعتقادی خانواده", - "type": "radio", - "required": false, - "description": "Please select the option that best describes the general atmosphere and lifestyle of your family.", - "tooltip": "### راهنمای گزینه‌های فضای مذهبی خانواده\n\n* **مذهبی و کاملاً مقید:** خانواده‌ای که تقید بسیار بالایی به انجام تمام واجبات دارد، حدود شرعی (مانند محرم و نامحرم) را به شدت رعایت می‌کند و آداب و مناسک مذهبی در تمام شئون زندگی آن‌ها جریان دارد.\n* **مذهبی (مقید به واجبات):** خانواده‌ای که متعهد به واجبات اصلی مذهبی (مانند نماز و روزه) و اخلاق اسلامی است و در چارچوب‌های متعارف یک جامعه متدین زندگی می‌کند.\n* **سنتی (محترم به ارزش‌های دینی):** خانواده‌ای که به ارزش‌های اخلاقی پایبند است و به دین احترام می‌گذارد، اما ممکن است تمام احکام و واجبات مذهبی را به طور دقیق و کامل اجرا نکند.\n* **غیرمذهبی / عرفی:** خانواده‌ای که مناسک و چارچوب‌های مذهبی تاثیر تعیین‌کننده‌ای بر سبک زندگی، ارتباطات یا تصمیم‌گیری‌های روزمره‌شان ندارد، هرچند ممکن است احترامی کلی برای مذهب قائل باشند.", - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مذهبی و کاملاً مقید", - "مذهبی (مقید به واجبات)", - "سنتی (محترم به ارزش‌های دینی)", - "غیرمذهبی / عرفی" - ] - } - }, - { - "title": "وضعیت اقتصادی خانواده", - "type": "radio", - "required": false, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": ["ضعیف", "متوسط", "خوب", "مرفه"] - } - }, - { - "title": "توضیح کوتاه درباره خانواده", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "توضیحات خود را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "tooltip": "هر نکته مهمی که در گزینه‌های بالا نبود، اینجا بنویسید." - } - ] - }, - { - "title": "بخش ۶: وضعیت تأهل، سابقه ازدواج و فرزندان", - "icon": "heart", - "slug": "marital_history_children", - "required": true, - "estimateTime": "۳ دقیقه", - "progress": 0, - "description": "ثبت اطلاعات مربوط به وضعیت تأهل فعلی، ازدواج‌های قبلی و فرزندان شما.", - "questions": [ - { - "title": "وضعیت تأهل فعلی", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مجرد؛ بدون هیچ‌گونه سابقه عقد یا ازدواج", - "عقد ناموفق / فسخ نامزدی؛ بدون شروع زندگی مشترک", - "جدا شده؛ طلاق پس از زندگی مشترک", - "همسر فوت شده" - ] - }, - "private": true, - "tooltip": "currentMaritalStatusTooltip" - }, - { - "title": "مدت ازدواج یا عقد قبلی", - "type": "text", - "required": false, - "logic": { - "dependsOn": { - "title": "وضعیت تأهل فعلی", - "values": [ - "عقد ناموفق / فسخ نامزدی؛ بدون شروع زندگی مشترک", - "جدا شده؛ طلاق پس از زندگی مشترک", - "همسر فوت شده" - ] - } - }, - "extras": { - "placeHolder": "۳ سال", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "علت جدایی، در صورت وجود", - "type": "text", - "required": false, - "logic": { - "dependsOn": { - "title": "وضعیت تأهل فعلی", - "values": [ - "عقد ناموفق / فسخ نامزدی؛ بدون شروع زندگی مشترک", - "جدا شده؛ طلاق پس از زندگی مشترک" - ] - } - }, - "extras": { - "placeHolder": "توضیحات را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "وضعیت فرزند و تکفل", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "فرزندی ندارم.", - "فرزند دارم و با من زندگی می‌کند.", - "فرزند دارم اما با من زندگی نمی‌کند.", - "شخص دیگری غیر از فرزند تحت تکفل من است." - ] - }, - "private": true - }, - { - "title": "تعداد فرزندان", - "type": "number", - "required": false, - "logic": { - "dependsOn": { - "title": "وضعیت تأهل فعلی", - "values": ["جدا شده؛ طلاق پس از زندگی مشترک", "همسر فوت شده"] - } - }, - "extras": { - "placeHolder": "۲", - "range": [0, 10], - "options": [] - }, - "private": true - }, - { - "title": "وضعیت حضانت فرزند یا فرزندان شما چگونه است؟", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "حضانت کامل فرزند یا فرزندان با من است.", - "حضانت به صورت مشترک یا رفت‌وآمد دوره‌ای انجام می‌شود.", - "حضانت با والد دیگر یا شخص دیگری است.", - "فرزندان به سن قانونی رسیده‌اند و حضانت مطرح نیست.", - "شرایط دیگر (در حال بررسی، اختلاف یا موارد دیگر)." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "وضعیت فرزند و تکفل", - "values": [ - "فرزند دارم و با من زندگی می‌کند.", - "فرزند دارم اما با من زندگی نمی‌کند." - ] - } - } - }, - { - "title": "فرزند یا فرزندان معمولاً چه میزان با شما زندگی میکنند؟", - "type": "radio", - "required": false, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "به‌صورت تمام‌وقت یا بیشتر روزهای هفته با من زندگی می‌کنند.", - "تقریباً نیمی از زمان (به‌صورت مشترک) با من زندگی می‌کنند.", - "فقط آخر هفته‌ها، تعطیلات یا روزهای خاص با من زندگی می‌کنند.", - "با من زندگی نمی‌کنند یا برنامه ثابتی وجود ندارد." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "وضعیت فرزند و تکفل", - "values": [ - "فرزند دارم و با من زندگی می‌کند.", - "فرزند دارم اما با من زندگی نمی‌کند." - ] - } - } - }, - { - "title": "آیا برنامه حضانت، ملاقات یا جابهجایی فرزند بر محل زندگی یا امکان مهاجرت شما تأثیر میگذارد؟", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "خیر، تأثیر قابل‌توجهی بر محل زندگی یا مهاجرت ندارد.", - "بله، جابه‌جایی یا مهاجرت نیازمند هماهنگی، رضایت والد دیگر یا مجوز قانونی است.", - "بله، محدودیت جدی دارد و باید در همین شهر/منطقه بمانم.", - "نامشخص (در حال بررسی یا بستگی به شرایط آینده دارد)." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "وضعیت فرزند و تکفل", - "values": [ - "فرزند دارم و با من زندگی می‌کند.", - "فرزند دارم اما با من زندگی نمی‌کند." - ] - } - } - }, - { - "title": "وضعیت پرداخت یا دریافت نفقه و حمایت مالی فرزند چگونه است؟", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "تعهد مالی مستمر دارم (پرداخت منظم نفقه یا تقسیم هزینه‌ها).", - "حمایت مالی مستمر دریافت می‌کنم (دریافت منظم نفقه).", - "پرداخت‌ها به‌صورت موردی، توافقی یا غیرمنظم انجام می‌شود.", - "تعهد مالی یا نفقه رسمی وجود ندارد (یا فرزند مستقل است/پرونده در جریان است)." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "وضعیت فرزند و تکفل", - "values": [ - "فرزند دارم و با من زندگی می‌کند.", - "فرزند دارم اما با من زندگی نمی‌کند." - ] - } - } - }, - { - "title": "پذیرش ارتباط ضروری همسر آینده با والد دیگرِ فرزند", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "ارتباط ضروری، محترمانه و محدود به امور فرزند را میپذیرم.", - "فقط ارتباط رسمی و محدود درباره امور ضروری فرزند را میپذیرم.", - "ترجیح میدهم ارتباط تا حد امکان از طریق واسطه، یکی از اعضای خانواده یا وکیل انجام شود.", - "وجود ارتباط مستمر و فراتر از امور ضروری فرزند برایم خط قرمز است.", - "پذیرش این موضوع به نوع و میزان ارتباط، شرایط حضانت و اعتماد متقابل بستگی دارد." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "وضعیت فرزند و تکفل", - "values": [ - "فرزند دارم و با من زندگی می‌کند.", - "فرزند دارم اما با من زندگی نمی‌کند." - ] - } - } - }, - { - "title": "توضیح کوتاه درباره شرایط فرزند یا تکفل", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "توضیحات را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "private": true - } - ] - }, - { - "title": "بخش ۷: اعتقادات، سبک زندگی، شخصیت و خطوط قرمز شخصی", - "icon": "shield-check", - "slug": "beliefs_lifestyle_boundaries", - "required": true, - "estimateTime": "۴ دقیقه", - "progress": 0, - "description": "جزئیات مربوط به فرایض مذهبی، پوشش در اجتماع، دیدگاه سیاسی، عادات و ترجیحات سبک زندگی.", - "questions": [ - { - "title": "مرجع تقلید", - "type": "text", - "required": true, - "extras": { - "placeHolder": "آیت‌الله سیستانی", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "میزان تقید به نمازهای واجب", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "همیشه مقید هستم، ترجیحاً اول وقت", - "همیشه مقید هستم، اما نه لزوماً اول وقت", - "گاهی اوقات می‌خوانم", - "نمی‌خوانم" - ] - }, - "private": true - }, - { - "title": "میزان تقید به روزه ماه رمضان", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "کاملاً مقید هستم", - "به دلیل عذر شرعی یا پزشکی روزه نمی‌گیرم", - "گاهی بدون عذر شرعی روزه نمی‌گیرم", - "مقید نیستم" - ] - }, - "private": true - }, - { - "title": "نوع پوشش و ظاهر در اجتماع", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "پوشش کامل اسلامی (حداکثری) - چادر، عبا یا پوشش‌های مشابه با رعایت کامل حدود شرعی.", - "حجاب کامل با لباس‌های پوشیده و آزاد - مانتوهای بلند و گشاد با پوشش کامل موی سر.", - "پوشش عرفی و امروزی با رعایت حجاب - استایل‌های مدرن با استفاده از شال یا توربان برای پوشش مو.", - "پوشش آراسته و سنگین (بدون پوشش مو) - لباس‌های رسمی و موقر بدون استفاده از روسری یا شال.", - "پوشش مدرن و آزاد (بدون رعایت حجاب) - دنبال کردن استایل‌های روز بدون پایبندی به قواعد حجاب اسلامی." - ], - "noSearch": true - }, - "private": true, - "tooltip": "لطفاً گزینه‌ای را انتخاب کنید که بیشترین تطابق را با پوشش روزمره شما در اجتماع دارد.\n\n### توضیحات تکمیلی سوال\n\n* **برای کاربران خانم:** این سوال از شما می‌خواهد که **وضعیت فعلی**، ویژگی‌های شخصی و ترجیحات سبک زندگی فردی خود را مشخص کنید.\n* **برای کاربران آقا:** این سوال از شما می‌خواهد که **انتظارات**، معیارهای مورد نظر و ترجیحات خود را در رابطه با همسر آینده‌تان مشخص کنید." - }, - { - "title": "استفاده از آرایش در اجتماع", - "type": "radio", - "required": false, - "audience": { - "genders": ["female"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "اصلاً آرایش نمی‌کنم", - "فقط آرایش بسیار ملایم", - "آرایش کامل" - ] - }, - "private": true, - "tooltip": "لطفاً گزینه‌ای را انتخاب کنید که بیشترین تطابق را با استفاده روزمره شما از آرایش در اجتماع دارد.\n\nاین سوال از شما می‌خواهد که **وضعیت فعلی**، ویژگی‌های شخصی و ترجیحات سبک زندگی فردی خود را مشخص کنید." - }, - { - "title": "نگرش به رابطه دین و سیاست", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "دین و سیاست از هم جدایی‌ناپذیرند و حتماً همسرم باید همین دیدگاه را داشته باشد.", - "دین و سیاست از هم جدایی‌ناپذیرند، اما فعالیت سیاسی همسرم الزامی نیست.", - "نگرش سنتی و غیرسیاسی به تشیع؛ با فردی که دیدگاه سیاسی داشته باشد نمی‌توانم ازدواج کنم.", - "نگرش غیرسیاسی به تشیع، اما اگر همسرم دیدگاه سیاسی داشته باشد خط قرمز من نیست.", - "این مفاهیم و دسته‌بندی‌ها دغدغه اصلی من نیست." - ], - "noSearch": true - }, - "private": true, - "tooltip": "لطفاً گزینه‌ای را انتخاب کنید که نگاه شما به مذهب و انتظار شما از همسر آینده‌تان را بهتر توصیف می‌کند." - }, - { - "title": "موضع نسبت به نظام و حاکمیت فعلی", - "type": "dropdown", - "required": false, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "موافق و حامی نظام فعلی؛ مخالفت جدی همسرم خط قرمز است.", - "موافق و حامی نظام فعلی، اما تفاوت دیدگاه همسرم خط قرمز نیست.", - "مخالف نظام فعلی؛ حمایت جدی همسرم از حاکمیت خط قرمز است.", - "مخالف نظام فعلی، اما تفاوت دیدگاه همسرم خط قرمز نیست.", - "موضع خاصی ندارم؛ تفاوت‌های سیاسی برایم در ازدواج تعیین‌کننده نیست." - ], - "noSearch": true - }, - "private": true, - "tooltip": "این بخش برای جلوگیری از تنش‌های جدیِ اعتقادی در زندگی مشترک طراحی شده است." - }, - { - "title": "حدود روابط با جنس مخالف", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "بسیار رسمی و محدود (فقط در حد ضرورت) - اجتناب از هرگونه گفتگوی غیرضروری یا شوخی.", - "محترمانه و متعارف (بدون صمیمیت) - تعاملات مؤدبانه با حفظ مرزهای مشخص شخصی.", - "اجتماعی و راحت (در چارچوب شرعی) - حضور فعال در جمع‌های اجتماعی با رعایت حدود اخلاقی.", - "مرز خاصی ندارم - با تعاملات اجتماعی مدرن کاملاً راحت هستم." - ] - }, - "private": true, - "tooltip": "لطفاً گزینه‌ای را انتخاب کنید که رفتار روزمره شما را در مواجهه با نامحرم بهتر توصیف می‌کند." - }, - { - "title": "سیگار", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "اصلاً مصرف نمی‌کنم", - "گاهی / تفریحی مصرف می‌کنم", - "مرتب مصرف می‌کنم", - "در حال ترک هستم" - ] - }, - "private": true - }, - { - "title": "قلیان", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "اصلاً مصرف نمی‌کنم", - "گاهی / تفریحی مصرف می‌کنم", - "مرتب مصرف می‌کنم", - "در حال ترک هستم" - ] - }, - "private": true - }, - { - "title": "ویپ / سیگار الکترونیک", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "اصلاً مصرف نمی‌کنم", - "گاهی / تفریحی مصرف می‌کنم", - "مرتب مصرف می‌کنم", - "در حال ترک هستم" - ] - }, - "private": true - }, - { - "title": "مشروبات الکلی", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "اصلاً مصرف نمی‌کنم", - "مصرف الکل برای من خط قرمز جدی است", - "گاهی مصرف می‌کنم", - "مصرف می‌کنم" - ] - }, - "private": true - }, - { - "title": "مواد مخدر یا مواد غیرقانونی", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "هرگز مصرف نکرده‌ام", - "در گذشته مصرف داشته‌ام اما اکنون ترک کرده‌ام", - "در حال حاضر مصرف می‌کنم", - "در حال درمان یا بازپروری هستم" - ] - }, - "private": true - }, - { - "title": "نگرش به موسیقی", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "اصلاً به هیچ موسیقی گوش نمی‌دهم", - "فقط به تواشیح، موسیقی‌های مذهبی یا بدون ساز گوش می‌دهم", - "به موسیقی‌های مجاز و حلال گوش می‌دهم", - "حساسیت خاصی روی نوع موسیقی ندارم" - ] - }, - "private": true - }, - { - "title": "نگرش به مراسم عروسی", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "بدون مراسم یا بسیار ساده", - "کاملاً مذهبی و تفکیک‌شده (زنانه و مردانه جدا)", - "مراسم مختلط سنگین بدون رقص و موسیقی غیرمجاز", - "مراسم مختلط همراه با موسیقی و رقص", - "هنوز تصمیم نگرفته‌ام؛ بستگی به توافق خانواده‌ها دارد" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "ویژگی‌های شخصیتی خودتان", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [1, 10], - "options": [ - "آرام و درونگرا", - "اجتماعی و برونگرا", - "احساسی", - "منطقی", - "شوخ‌طبع", - "جدی", - "اهل گفتگو", - "خانواده‌دوست", - "مستقل", - "مسئولیت‌پذیر", - "صبور", - "منظم", - "اهل برنامه‌ریزی", - "انعطاف‌پذیر", - "حساس و دقیق", - "اهل رشد فردی" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "سرگرمی‌ها و علایق اصلی", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [1, 15], - "options": [ - "تلاوت قرآن و مطالعه دینی", - "حضور در مسجد و هیئت", - "فعالیت مذهبی و فرهنگی", - "سفر زیارتی", - "سفر سیاحتی", - "طبیعت‌گردی", - "ورزش", - "مطالعه", - "فیلم و سینما", - "آشپزی", - "هنر", - "موسیقی", - "بازی‌های فکری", - "فعالیت اجتماعی و خیریه", - "کافه و رستوران", - "یادگیری زبان", - "تکنولوژی و کامپیوتر" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "توضیح کوتاه درباره سبک زندگی", - "type": "textarea", - "required": false, - "extras": { - "placeHolder": "توضیحات را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "هر نکته مهمی که در گزینه‌های بالا نبود، اینجا بنویسید." - } - ] - }, - { - "title": "تست شخصیت شناسی", - "icon": "layout-grid", - "slug": "personality_test", - "required": false, - "estimateTime": "۸ دقیقه", - "progress": 0, - "description": "تست شخصیت شناسی", - "questions": [] - }, - { - "title": "تست ۵ نیاز گلاسر", - "icon": "layout-grid", - "slug": "glasser_5_needs_test", - "required": false, - "estimateTime": "۵ دقیقه", - "progress": 0, - "description": "تست ۵ نیاز گلاسر", - "questions": [] - }, - { - "title": "بخش ۸: معیارها و خطوط قرمز همسر آینده", - "icon": "shield-check", - "slug": "future_spouse_criteria", - "required": true, - "estimateTime": "۶ دقیقه", - "progress": 0, - "description": "معیارها و خطوط قرمز.", - "questions": [ - { - "title": "بازه قدی مطلوب همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "کمتر از ۱۶۰", - "۱۶۰ تا ۱۷۰", - "۱۷۰ تا ۱۸۰", - "۱۸۰ تا ۱۹۰", - "بالای ۱۹۰" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "تیپ بدنی مطلوب همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "لاغراندام", - "متناسب", - "ورزیده", - "توپر", - "درشت‌اندام" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "رنگ پوست مطلوب همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "سفید / روشن", - "گندمگون / سبزه روشن", - "سبزه تیره / قهوه‌ای", - "تیره / سیاهپوست" - ], - "noSearch": true - }, - "private": true - }, - { - "title": "قومیت، زبان یا ملیت مطلوب همسر آینده", - "type": "text", - "required": false, - "description": "", - "extras": { - "placeHolder": "انگلیسی، فرانسوی و...", - "range": [0, 0], - "options": [] - }, - "private": true - }, - { - "title": "حداقل سطح تحصیلات همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "حداقل دیپلم", - "حداقل کارشناسی", - "حداقل کارشناسی ارشد", - "دکتری یا بالاتر ترجیح دارد.", - "حتماً تحصیلات حوزوی / علوم دینی داشته باشد.", - "تحصیلات دانشگاهی مهم نیست، اما بلوغ فکری مهم است." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "وضعیت اشتغال مطلوب همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "شاغل باشد.", - "دانشجو باشد.", - "خانه‌دار باشد.", - "کارآفرین / صاحب کسب‌وکار باشد.", - "در مسیر رشد شغلی باشد.", - "بسته به شرایط قابل گفتگو است." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "پذیرش سابقه عقد یا ازدواج همسر آینده", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "فقط مجرد؛ سابقه عقد یا ازدواج قبلی برایم خط قرمز است.", - "مجرد ترجیح دارد، اما شرایط خاص را بررسی می‌کنم.", - "تفاوتی ندارد.", - "بستگی به علت جدایی، مدت ازدواج قبلی و شرایط خانوادگی دارد." - ] - }, - "private": true - }, - { - "title": "پذیرش داشتن فرزند از ازدواج قبلی", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "به هیچ وجه نمی‌پذیرم.", - "در شرایط خاص می‌پذیرم.", - "تفاوتی ندارد.", - "فقط اگر فرزند با او زندگی نکند، بررسی می‌کنم." - ] - }, - "private": true, - "logic": { - "dependsOn": { - "title": "پذیرش سابقه عقد یا ازدواج همسر آینده", - "values": [ - "تفاوتی ندارد.", - "بستگی به علت جدایی، مدت ازدواج قبلی و شرایط خانوادگی دارد." - ] - } - } - }, - { - "title": "پوشش و استایل مطلوب همسر آینده", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["male"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "پوشش کامل اسلامی مانند چادر، عبایا یا جلباب الزامی است.", - "حجاب شرعی الزامی است، اما نوع آن مهم نیست.", - "پوشش عرفی همراه با حجاب قابل قبول است.", - "پوشش محجوب و سنگین مهم است، اما جزئیات قابل گفتگو است.", - "حساسیت خاصی ندارم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "پوشش و استایل مطلوب همسر آینده", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["female"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "پوشش رسمی، سنگین و مذهبی داشته باشد.", - "ساده، مرتب و محجوب باشد.", - "پوشش معمولی و آراسته کافی است.", - "پوشش مدرن برایم مشکلی ندارد.", - "حساسیت خاصی ندارم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "تمایل به ادامه تحصیل همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "حتماً قصد ادامه تحصیل داشته باشد.", - "ادامه تحصیل مثبت است، اما الزامی نیست.", - "ترجیح می‌دهم بعد از ازدواج ادامه تحصیل ندهد.", - "بسته به شرایط زندگی مشترک تصمیم می‌گیریم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "تمایل به اشتغال همسر آینده", - "type": "dropdown", - "required": true, - "audience": { - "genders": ["female"] - }, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مهم نیست.", - "حتماً شاغل باشد.", - "حتماً خانه‌دار باشد.", - "اختیار با خودش باشد.", - "بسته به شرایط زندگی، فرزندآوری و توافق مشترک تصمیم می‌گیریم.", - "فقط با شغلی که با ارزش‌های دینی و خانوادگی من سازگار باشد موافقم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "وضعیت خانوادگی همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [0, 0], - "options": [ - "این معیارها برایم مهم نیست.", - "والدین همسرم طلاق نگرفته باشند.", - "پدر همسر آینده‌ام در قید حیات باشد.", - "مادر همسر آینده‌ام در قید حیات باشد.", - "فضای مذهبی خانواده همسر برایم مهم است.", - "خانواده همسر باید اهل ارتباط محترمانه و سالم باشند." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "سطح ارتباط با خانواده‌ها بعد از ازدواج", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "ارتباط نزدیک و پررنگ با خانواده‌ها را دوست دارم.", - "ارتباط محترمانه اما با حفظ استقلال زندگی مشترک را ترجیح می‌دهم.", - "ارتباط محدود و کنترل‌شده را ترجیح می‌دهم.", - "بسته به شرایط خانواده‌ها تصمیم می‌گیرم." - ] - }, - "private": true - }, - { - "title": "میزان پایبندی مذهبی مطلوب همسر آینده", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "بسیار مذهبی و مقید", - "مذهبی معتدل", - "عرفی اما محترم به دین", - "این معیار برایم اهمیت زیادی ندارد." - ] - }, - "private": true - }, - { - "title": "نگرش سیاسی مطلوب همسر آینده", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "حتماً همسو با دیدگاه من باشد.", - "تفاوت دیدگاه سیاسی مهم نیست، به شرط احترام متقابل.", - "ترجیح می‌دهم سیاسی نباشد.", - "سیاست برایم اهمیتی در ازدواج ندارد." - ] - }, - "private": true - }, - { - "title": "حدود روابط همسر آینده با جنس مخالف", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "بسیار رسمی و محدود باشد.", - "معمولی و محترمانه باشد.", - "اجتماعی‌تر باشد، اما در چارچوب شرع و اخلاق." - ] - }, - "private": true - }, - { - "title": "خط قرمزهای مربوط به دخانیات، الکل و مواد در همسر آینده", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "انتخاب کنید", - "range": [1, 10], - "options": [ - "سیگار برایم خط قرمز است.", - "قلیان برایم خط قرمز است.", - "ویپ برایم خط قرمز است.", - "الکل برایم خط قرمز جدی است.", - "مواد مخدر برایم خط قرمز قطعی است.", - "مصرف تفریحی دخانیات را در شرایط خاص می‌پذیرم.", - "هیچ‌کدام برایم خط قرمز نیست." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "نگرش به موسیقی", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "نباید به موسیقی گوش بدهد.", - "فقط موسیقی مذهبی / بدون ساز / نشید قابل قبول است.", - "موسیقی حلال و مجاز قابل قبول است.", - "حساسیت خاصی ندارم." - ] - }, - "private": true - }, - { - "title": "نگرش به مراسم عروسی", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "مراسم ساده یا بدون مراسم را ترجیح دهد.", - "فقط مراسم کاملاً شرعی و تفکیک‌شده قابل قبول است.", - "مراسم مختلط محترمانه و بدون رقص و موسیقی غیرشرعی قابل قبول است.", - "مراسم مختلط با موسیقی و رقص قابل قبول است.", - "بسته به توافق خانواده‌ها قابل تصمیم‌گیری است." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "پذیرش بیماری خاص، معلولیت یا شرایط درمانی در همسر آینده", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "برایم خط قرمز است.", - "در شرایط خاص و با توضیح کامل بررسی می‌کنم.", - "اگر مانع زندگی مشترک سالم نباشد، می‌پذیرم.", - "موردی و با مشورت بررسی می‌کنم." - ] - }, - "private": true - }, - { - "title": "پذیرش سابقه مشاوره یا درمان روان‌شناختی همسر آینده", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "برایم مشکلی ندارد.", - "بستگی به شرایط فعلی و میزان ثبات دارد.", - "برایم خط قرمز است.", - "نیاز به بررسی جدی‌تر دارد." - ] - }, - "private": true - }, - { - "title": "ترجیح درباره محل زندگی بعد از ازدواج", - "type": "dropdown", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "کشور محل زندگی طرف مقابل برایم مهم نیست.", - "ترجیح می‌دهم در کشور فعلی خودم بمانم.", - "ترجیح می‌دهم در کشور فعلی همسر آینده‌ام زندگی کنیم.", - "فقط در شهر فعلی خودم حاضر به زندگی هستم.", - "آماده مهاجرت به کشور ثالث هستم.", - "بسته به کار، اقامت، خانواده و شرایط مالی تصمیم می‌گیریم." - ], - "noSearch": true - }, - "private": true - }, - { - "title": "زندگی با خانواده بعد از ازدواج", - "type": "radio", - "required": true, - "extras": { - "placeHolder": "یک گزینه را انتخاب کنید", - "range": [0, 0], - "options": [ - "فقط زندگی مستقل را می‌پذیرم.", - "زندگی موقت با خانواده در ابتدای ازدواج قابل قبول است.", - "زندگی با خانواده همسر یا خانواده خودم برایم مشکلی ندارد.", - "بستگی به شرایط دارد." - ] - }, - "private": true - }, - { - "title": "توضیحات تکمیلی و سایر خطوط قرمز", - "type": "textarea", - "required": true, - "description": "", - "extras": { - "placeHolder": "توضیحات تکمیلی را اینجا وارد کنید...", - "range": [0, 0], - "options": [] - }, - "private": true, - "tooltip": "هر نکته مهمی که در گزینه‌های بالا نبود، اینجا بنویسید." - } - ] - }, - { - "title": "بخش ۹: بارگذاری مدارک، احراز هویت و تصاویر", - "icon": "shield-check", - "slug": "identity_verification", - "required": true, - "estimateTime": "۲ دقیقه", - "progress": 0, - "description": "بارگذاری مدارک شناسایی.", - "questions": [ - { - "title": "تصویر چهره؛ عکس جدید و واضح", - "type": "photo", - "required": true, - "private": true, - "description": "این بخش از کاربر می‌خواهد یک عکس جدید و واضح از چهره خود بارگذاری کند که کاملاً خصوصی باقی مانده و منحصراً برای معرف‌ها قابل دسترسی خواهد بود.", - "extras": { - "placeHolder": "بارگذاری تصویر", - "range": [0, 0], - "options": [".jpg", ".jpeg", ".png"] - } - }, - { - "title": "تصویر کارت شناسایی معتبر", - "type": "file", - "required": true, - "private": true, - "description": "پاسپورت، کارت ملی یا گواهینامه", - "extras": { - "placeHolder": "بارگذاری مدرک", - "range": [0, 0], - "options": [".pdf", ".jpg", ".jpeg", ".png"] - } - }, - { - "title": "تأیید تطابق مدارک و اطلاعات", - "type": "checkbox", - "required": true, - "extras": { - "placeHolder": "تأیید می‌کنم", - "range": [0, 0], - "options": [ - "تأیید می‌کنم که نام، سن، تصویر و اطلاعات هویتی من با مدارک بارگذاری‌شده مطابقت دارد." - ] - }, - "private": true - } - ] - } -] diff --git a/src/data/section-slug-map.ts b/src/data/section-slug-map.ts deleted file mode 100644 index 02b33bd..0000000 --- a/src/data/section-slug-map.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Maps backend section slugs to their corresponding frontend question list slugs. - * Some backend sections aggregate multiple frontend question categories under a - * single slug, so these two naming systems don't always match 1-to-1. - */ -export const BACKEND_TO_FRONTEND_SLUG_MAP: Record = { - personal_identity: "personal_info", - contact_residence: "contact_residence_family_communication", - appearance_health: "appearance_health_activity", - education_career: "education_career_economic_status", - family_background: "family_marital_history", - marital_history: "family_marital_history", - beliefs_lifestyle: "beliefs_lifestyle_boundaries", - spouse_criteria: "future_spouse_criteria", - documents_verification: "identity_verification", -}; - -/** - * Reverse map: frontend slug → backend slug. - * Built automatically from BACKEND_TO_FRONTEND_SLUG_MAP. - */ -export const FRONTEND_TO_BACKEND_SLUG_MAP: Record = - Object.fromEntries( - Object.entries(BACKEND_TO_FRONTEND_SLUG_MAP).map(([backend, frontend]) => [ - frontend, - backend, - ]), - ); - -/** - * Resolve a frontend question-list slug to the backend section slug - * the API expects. Falls back to the original slug when no mapping exists. - */ -export function toBackendSlug(frontendSlug: string): string { - if (frontendSlug === "marital_history_children") { - return "marital_history"; - } - return FRONTEND_TO_BACKEND_SLUG_MAP[frontendSlug] ?? frontendSlug; -} - -/** - * Resolve a backend section slug to the frontend question-list slug - * used in the JSON data. Falls back to the original slug. - */ -export function toFrontendSlug(backendSlug: string): string { - return BACKEND_TO_FRONTEND_SLUG_MAP[backendSlug] ?? backendSlug; -} diff --git a/src/hooks/marriage/types.ts b/src/hooks/marriage/types.ts index b3e15b7..8fdd8d5 100644 --- a/src/hooks/marriage/types.ts +++ b/src/hooks/marriage/types.ts @@ -44,6 +44,7 @@ export type MarriageField = { label: string; type: string; value: MarriageFieldValue; + option_id?: string | string[]; private?: boolean; }; @@ -202,7 +203,7 @@ export type CattellQuestion = { question_number: number; text: string; item_type?: string; - options?: string[]; + options: { id: string; label: string; value: string | number }[]; }; export type CattellQuestionsResponse = { @@ -237,6 +238,7 @@ export type GlasserQuestion = { text: string; factor?: string; factor_code: string; + options: { id: string; label: string; value: string | number }[]; }; export type GlasserQuestionsResponse = { diff --git a/src/hooks/marriage/use-form-schema.ts b/src/hooks/marriage/use-form-schema.ts index 96409dd..53338f2 100644 --- a/src/hooks/marriage/use-form-schema.ts +++ b/src/hooks/marriage/use-form-schema.ts @@ -9,6 +9,7 @@ export interface FormOption { id: string; value: string; label: string; + order: number; } export interface FormQuestion { @@ -19,17 +20,20 @@ export interface FormQuestion { tooltip: string; placeholder: string; required: boolean; + is_required?: boolean; show_guardian_notice: boolean; validation: Record; ui_config: Record; logic: Record | null; is_visible: boolean; + order: number; options: FormOption[]; } export interface FormCard { id: string; title: string; + order: number; questions: FormQuestion[]; } @@ -38,6 +42,7 @@ export interface FormSection { title: string; icon: string; is_required: boolean; + order: number; estimated_minutes: number; cards: FormCard[]; } @@ -52,7 +57,7 @@ export interface FormSchemaResponse { form_id: string; version: number; sections: FormSection[]; - answers: Record; + answers: Record; progress: { current_step: number; total_steps: number; @@ -89,7 +94,7 @@ export interface SaveAnswersPayload { answers: Array<{ question_id: string; value: any; - option_id?: string; + option_id?: string | string[]; }>; } diff --git a/src/hooks/marriage/use-section-data.ts b/src/hooks/marriage/use-section-data.ts index 4a11981..19e5f67 100644 --- a/src/hooks/marriage/use-section-data.ts +++ b/src/hooks/marriage/use-section-data.ts @@ -1,8 +1,6 @@ "use client"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { getQuestionListItemBySlug } from "@/data/question-data"; -import { toBackendSlug } from "@/data/section-slug-map"; import { http } from "@/lib/http"; import type { MutationOptions, QueryOptions } from "./options"; import { pathParam } from "./path-param"; @@ -14,42 +12,7 @@ import type { import type { FormSchemaResponse } from "./use-form-schema"; -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 slugifyQuestionTitle(title: string) { - const slug = title - .normalize("NFKD") - .replace(/[\u0300-\u036f]/g, "") - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - return slug || `field_${hashString(title)}`; -} - -function getQuestionFieldKey( - title: string, - index: number, - englishTitle?: string, -) { - const finalTitle = englishTitle || title; - return `q${index + 1}_${slugifyQuestionTitle(finalTitle)}`; -} -function hasQuestionAnswerValue(value: unknown) { - if (value === null || value === undefined) { - return false; - } - if (typeof value === "string") { - return value.trim().length > 0; - } - return true; -} export async function getMarriageSectionData( slug: string, @@ -63,32 +26,11 @@ export async function getMarriageSectionData( }; const lang = getClientCookie("HABIB_LANGUAGE") || getClientCookie("habib_language") || "en"; - if (slug === "family_marital_history") { - const [fbData, mhData] = await Promise.all([ - getMarriageSectionData("family_background"), - getMarriageSectionData("marital_history_children"), - ]); - return { - slug: "family_marital_history", - data: [...(fbData.data || []), ...(mhData.data || [])], - current_step: fbData.current_step + mhData.current_step, - total_steps: fbData.total_steps + mhData.total_steps, - completion_percent: - fbData.total_steps + mhData.total_steps > 0 - ? ((fbData.current_step + mhData.current_step) / - (fbData.total_steps + mhData.total_steps)) * - 100 - : 0, - updated_at: fbData.updated_at || mhData.updated_at, - }; - } - - const backendSlug = toBackendSlug(slug); const { data } = await http.get( `/api/marriage/forms/profile/?lang=${lang}` ); - const sec = data.sections.find((s: any) => s.id === backendSlug); + const sec = data.sections.find((s: any) => s.id === slug); if (!sec) { return { slug, @@ -114,7 +56,7 @@ export async function getMarriageSectionData( }); }); - const prog = data.progress.sections_progress[backendSlug] || { + const prog = data.progress.sections_progress[slug] || { current_step: 0, total_steps: 0, completion_percent: 0.0, @@ -134,46 +76,6 @@ export async function updateMarriageSectionData( slug: string, payload: UpdateMarriageSectionDataPayload, ) { - if (slug === "family_marital_history") { - const answersPayload = payload.fields.map((f) => ({ - question_id: f.key, - value: f.value, - option_id: (f as any).option_id || undefined, - })); - - const { data } = await http.put( - `/api/marriage/forms/profile/answers/`, - { - answers: answersPayload, - } - ); - - const fbProg = data.progress.sections_progress["family_background"] || { - current_step: 0, - total_steps: 0, - completion_percent: 0.0, - }; - const mhProg = data.progress.sections_progress["marital_history"] || { - current_step: 0, - total_steps: 0, - completion_percent: 0.0, - }; - - return { - slug: "family_marital_history", - data: payload.fields, - current_step: fbProg.current_step + mhProg.current_step, - total_steps: fbProg.total_steps + mhProg.total_steps, - completion_percent: - fbProg.total_steps + mhProg.total_steps > 0 - ? ((fbProg.current_step + mhProg.current_step) / - (fbProg.total_steps + mhProg.total_steps)) * - 100 - : 0, - updated_at: new Date().toISOString(), - }; - } - const answersPayload = payload.fields.map((f) => ({ question_id: f.key, value: f.value, @@ -187,8 +89,7 @@ export async function updateMarriageSectionData( } ); - const backendSlug = toBackendSlug(slug); - const prog = data.progress.sections_progress[backendSlug] || { + const prog = data.progress.sections_progress[slug] || { current_step: 0, total_steps: 0, completion_percent: 0.0, diff --git a/src/lib/schema-adapter.ts b/src/lib/schema-adapter.ts index c07a5ce..5808873 100644 --- a/src/lib/schema-adapter.ts +++ b/src/lib/schema-adapter.ts @@ -3,13 +3,66 @@ import type { FormSection, FormQuestion, } from "@/hooks/marriage/use-form-schema"; -import type { - QuestionListItem, - QuestionField, - QuestionCardIcon, -} from "@/data/question-data"; import { defaultLocale, type Locale } from "@/translations/config"; +export type QuestionCardIcon = + | "profile" + | "education" + | "details" + | "checklist" + | "contact" + | "family_marital"; + +export type QuestionExtras = { + placeHolder: string; + range: [number, number]; + options: string[]; + noSearch?: boolean; +}; + +export type QuestionAudienceRule = { + genders?: string[]; + maxAge?: number; + minAge?: number; +}; + + + +export type QuestionField = { + id: string; + title: string; + type: string; + order: number; + required: boolean; + baseRequired: boolean; + isVisible: boolean; + private?: boolean; + description: string; + tooltip: string; + validation?: any; + ui_config?: any; + extras: QuestionExtras; + audience?: QuestionAudienceRule; + requiredWhen?: QuestionAudienceRule; + showGuardianNotice?: boolean; + options: { id: string; value: string | number; label: string; order: number }[]; +}; + +export type QuestionListItem = { + slug: string; + title: string; + estimate: string; + progress: number; + icon: QuestionCardIcon; + required: boolean; + showInfoBadge?: boolean; + summary: string; + checkpoints: string[]; + tooltip?: string; + note?: string; + questions: QuestionField[]; +}; + const iconMap: Record = { "user-circle": "profile", school: "education", @@ -18,13 +71,18 @@ const iconMap: Record = { "layout-grid": "checklist", }; -export function mapBackendQuestionToFrontend(bq: FormQuestion, index: number, originalSlug?: string): QuestionField { +export function mapBackendQuestionToFrontend(bq: FormQuestion, index: number): QuestionField { return { + id: bq.id, title: bq.title || "Untitled", - englishTitle: bq.title, type: bq.type, - required: bq.required, + order: bq.order !== undefined ? bq.order : index, + required: bq.is_required !== undefined ? bq.is_required : bq.required, + baseRequired: bq.required, + isVisible: bq.is_visible, private: bq.ui_config?.private, + validation: bq.validation, + ui_config: bq.ui_config, description: bq.description || "", tooltip: bq.tooltip || "", extras: { @@ -33,10 +91,8 @@ export function mapBackendQuestionToFrontend(bq: FormQuestion, index: number, or range: bq.ui_config?.range || [0, 0], noSearch: bq.ui_config?.noSearch, }, - logic: bq.logic ? { dependsOn: bq.logic.dependsOn } : undefined, showGuardianNotice: bq.show_guardian_notice, - originalSlug: originalSlug, - originalIndex: index, + options: [...(bq.options || [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), }; } @@ -47,14 +103,11 @@ export function mapBackendSectionToFrontend( const allQuestions: QuestionField[] = []; let index = 0; - section.cards.forEach((card) => { - card.questions.forEach((q) => { - const fq = mapBackendQuestionToFrontend(q, index, section.id); - fq.required = q.required; - (fq as any).backendId = q.id; - (fq as any).isVisible = q.is_visible; - (fq as any).backendOptions = q.options; - + const cards = [...(section.cards || [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + cards.forEach((card) => { + const questions = [...(card.questions || [])].sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); + questions.forEach((q) => { + const fq = mapBackendQuestionToFrontend(q, index); allQuestions.push(fq); index++; }); @@ -87,44 +140,5 @@ export function convertSchemaToFrontendItems( return mapBackendSectionToFrontend(sec, progress); }); - const fbIndex = rawItems.findIndex((item) => item.slug === "family_background"); - const mhIndex = rawItems.findIndex((item) => item.slug === "marital_history_children" || item.slug === "marital_history"); - - if (fbIndex !== -1 && mhIndex !== -1) { - const fbItem = rawItems[fbIndex]; - const mhItem = rawItems[mhIndex]; - - const fbProgInfo = schema.progress?.sections_progress?.[fbItem.slug]; - const mhProgInfo = schema.progress?.sections_progress?.[mhItem.slug]; - - const fbTotal = fbProgInfo?.total_steps ?? 0; - const mhTotal = mhProgInfo?.total_steps ?? 0; - const fbCurrent = fbProgInfo?.current_step ?? 0; - const mhCurrent = mhProgInfo?.current_step ?? 0; - - let combinedProgress = 0; - if (fbTotal + mhTotal > 0) { - combinedProgress = Math.round(((fbCurrent + mhCurrent) / (fbTotal + mhTotal)) * 100); - } - - const mergedItem: QuestionListItem = { - slug: "family_marital_history", - title: locale === "fa" ? "سوابق ازدواج و خانواده" : "Family & Marital History", - estimate: "5 min", - progress: Math.max(0, Math.min(100, combinedProgress)), - icon: "family_marital" as QuestionCardIcon, - required: fbItem.required || mhItem.required, - summary: "", - tooltip: "", - checkpoints: [...fbItem.checkpoints, ...mhItem.checkpoints], - questions: [...fbItem.questions, ...mhItem.questions], - }; - - const newItems = [...rawItems]; - newItems.splice(Math.max(fbIndex, mhIndex), 1); - newItems.splice(Math.min(fbIndex, mhIndex), 1, mergedItem); - return newItems; - } - - return rawItems; + return rawItems.sort((a, b) => (schema.sections.find(s => s.id === a.slug)?.order ?? 0) - (schema.sections.find(s => s.id === b.slug)?.order ?? 0)); }