diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx index 7465e02..419e558 100644 --- a/src/app/[lang]/page.tsx +++ b/src/app/[lang]/page.tsx @@ -39,15 +39,25 @@ export default async function LocaleEntryPage({ ); if (cachedEntryPath) { + if (cachedEntryPath === "/intro") { + return ; + } redirect(localizePath(cachedEntryPath, lang)); } // 3. Deep SSR Profile Resolution: resolve profile & redirect directly on server const profile = await fetchProfileSSR(token); if (profile) { - const targetPath = hasCompletedMarriageProfileBasics(profile) - ? getSubmitPath(profile) - : "/intro"; + const isBasicsCompleted = hasCompletedMarriageProfileBasics(profile); + if (!isBasicsCompleted) { + return ; + } + + const targetPath = getSubmitPath(profile); + if (targetPath === "/intro") { + return ; + } + redirect(localizePath(targetPath, lang)); } diff --git a/src/app/[lang]/test-api/page.tsx b/src/app/[lang]/test-api/page.tsx new file mode 100644 index 0000000..e86a30d --- /dev/null +++ b/src/app/[lang]/test-api/page.tsx @@ -0,0 +1,273 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { http } from "@/lib/http"; +import { getUserGeoRegion } from "@/lib/geo-region"; +import { resolveCountryName } from "@/data/countries"; +import { useI18n } from "@/translations/provider"; + +type TestResult = { + name: string; + url: string; + status: "pending" | "success" | "error"; + statusCode?: number; + data?: any; + error?: string; + timeMs?: number; +}; + +export default function TestApiPage() { + const { locale } = useI18n(); + const [results, setResults] = useState>({}); + const [isRunning, setIsRunning] = useState(false); + const [clientInfo, setClientInfo] = useState<{ + userAgent: string; + cookie: string; + localStorageGeo: string | null; + }>({ userAgent: "", cookie: "", localStorageGeo: null }); + + const logToConsoleAndFlutter = (tag: string, payload: any) => { + const message = `[TEST_API] ${tag}: ${typeof payload === "string" ? payload : JSON.stringify(payload, null, 2)}`; + console.log(message); + if (typeof window !== "undefined" && (window as any).flutter_inappwebview) { + try { + (window as any).flutter_inappwebview.callHandler("log", message); + } catch {} + } + }; + + const runSingleTest = async ( + id: string, + name: string, + url: string, + fetcher: () => Promise, + ) => { + setResults((prev) => ({ + ...prev, + [id]: { name, url, status: "pending" }, + })); + + const start = Date.now(); + try { + logToConsoleAndFlutter(`START ${name}`, { url }); + const data = await fetcher(); + const timeMs = Date.now() - start; + logToConsoleAndFlutter(`SUCCESS ${name}`, { timeMs, data }); + setResults((prev) => ({ + ...prev, + [id]: { + name, + url, + status: "success", + statusCode: 200, + data, + timeMs, + }, + })); + } catch (err: any) { + const timeMs = Date.now() - start; + const status = err.response?.status || 500; + const errorMsg = + err.response?.data?.detail || + err.response?.data?.message || + err.message || + "Unknown error"; + logToConsoleAndFlutter(`ERROR ${name}`, { + timeMs, + status, + error: errorMsg, + data: err.response?.data, + }); + setResults((prev) => ({ + ...prev, + [id]: { + name, + url, + status: "error", + statusCode: status, + error: errorMsg, + data: err.response?.data, + timeMs, + }, + })); + } + }; + + const runAllTests = async () => { + setIsRunning(true); + + if (typeof window !== "undefined") { + setClientInfo({ + userAgent: navigator.userAgent, + cookie: document.cookie, + localStorageGeo: localStorage.getItem("user_geo_region"), + }); + } + + // Test 1: User Region Backend API + await runSingleTest( + "region", + "1. User Region API (/account/auth/user/region/)", + "/account/auth/user/region/", + async () => { + const res = await http.get("/account/auth/user/region/"); + const raw = res.data; + const localizedCountry = resolveCountryName( + raw.country || raw.country_code, + locale || "fa", + ); + return { + rawResponse: raw, + resolvedCountryInPersian: resolveCountryName( + raw.country || raw.country_code, + "fa", + ), + resolvedCountryInEnglish: resolveCountryName( + raw.country || raw.country_code, + "en", + ), + currentLocaleResolved: localizedCountry, + }; + }, + ); + + // Test 2: getUserGeoRegion helper with force refresh + await runSingleTest( + "geo_helper", + "2. getUserGeoRegion(force=true)", + "lib/geo-region.ts", + async () => { + return await getUserGeoRegion(true); + }, + ); + + // Test 3: Marriage Forms Profile Overview + await runSingleTest( + "overview", + "3. Forms Profile Overview (/api/marriage/forms/profile/overview/)", + "/api/marriage/forms/profile/overview/?lang=fa", + async () => { + const res = await http.get( + "/api/marriage/forms/profile/overview/?lang=fa", + ); + return { + completion_percent: res.data?.progress?.completion_percent, + sections_progress: res.data?.progress?.sections_progress, + sections_count: res.data?.sections?.length, + }; + }, + ); + + // Test 4: Marriage Personal Identity Section + await runSingleTest( + "personal_identity", + "4. Personal Identity Section (/sections/personal_identity/)", + "/api/marriage/forms/profile/sections/personal_identity/?lang=fa", + async () => { + const res = await http.get( + "/api/marriage/forms/profile/sections/personal_identity/?lang=fa", + ); + return { + section_progress: res.data?.section_progress, + current_step: res.data?.section_progress?.current_step, + total_steps: res.data?.section_progress?.total_steps, + answers_count: Object.keys(res.data?.answers || {}).length, + }; + }, + ); + + setIsRunning(false); + }; + + useEffect(() => { + void runAllTests(); + }, []); + + return ( +
+
+ {/* Header */} +
+
+

+ 🧪 Marriage Backend API & GeoIP Diagnostics +

+

+ Live Phone Webview Test Runner & Logger +

+
+ +
+ + {/* Client Diagnostics */} +
+
📱 Device Info:
+
+ UA: {clientInfo.userAgent} +
+
+ Stored LocalStorage Geo:{" "} + {clientInfo.localStorageGeo || "None"} +
+
+ + {/* Results List */} +
+ {Object.entries(results).map(([id, res]) => ( +
+
+ + {res.name} + + + {res.status === "success" + ? `✅ 200 OK (${res.timeMs}ms)` + : res.status === "error" + ? `❌ ${res.statusCode || "Error"} (${res.timeMs}ms)` + : "⏳ Testing..."} + +
+ + {res.url && ( +
+ URL: {res.url} +
+ )} + + {res.error && ( +
+ Error: {res.error} +
+ )} + + {res.data && ( +
+
+                    {JSON.stringify(res.data, null, 2)}
+                  
+
+ )} +
+ ))} +
+
+
+ ); +} diff --git a/src/app/api/proxy/route.ts b/src/app/api/proxy/route.ts index 63ec0dd..7515068 100644 --- a/src/app/api/proxy/route.ts +++ b/src/app/api/proxy/route.ts @@ -13,6 +13,8 @@ const REQUEST_HEADERS_TO_FORWARD = [ "cookie", "token", "content-type", + "security-key", + "x-security-key", "x-csrf-token", "x-csrftoken", "x-requested-with", @@ -147,6 +149,15 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) { headers.set("authorization", `Token ${authKey}`); } + // Set security-key if available in environment and not already in headers + const securityKey = + process.env.NEXT_PUBLIC_SECURITY_KEY || + process.env.SECURITY_KEY || + "t5yugymks5458fd4ghfg6h6"; + if (securityKey && !headers.has("security-key")) { + headers.set("security-key", securityKey); + } + // Dynamically set language headers const requestedLanguages = [ request.headers.get("x-user-language"), @@ -165,9 +176,20 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) { headers.set("x-user-language", lang); headers.set("http_x_user_language", lang); headers.set("origin", targetUrl.origin); - headers.set("platform", "android"); - headers.set("referer", `${targetUrl.origin}/`); - headers.set("user-agent", "dart:io"); + const clientUserAgent = request.headers.get("user-agent"); + headers.set("user-agent", clientUserAgent || "dart:io"); + + // Forward client IP headers so backend can determine actual user location + const clientIp = + request.headers.get("cf-connecting-ip") || + request.headers.get("x-real-ip") || + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim(); + + if (clientIp) { + headers.set("x-forwarded-for", clientIp); + headers.set("x-real-ip", clientIp); + headers.set("x-client-ip", clientIp); + } return headers; } diff --git a/src/app/globals.css b/src/app/globals.css index 7ec4c38..cdc3128 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -231,10 +231,6 @@ html:lang(ar) body, -webkit-overflow-scrolling: touch; } -html[data-web-bootstrap="pending"] .app-shell { - visibility: hidden; -} - @media (min-width: 640px) { .app-shell { width: 375px; @@ -351,7 +347,7 @@ body.section-overlay-open .app-shell { calc(var(--question-lift-y, 0px) * -1), 0 ); - transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1); + transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1); will-change: transform; } diff --git a/src/app/intro/intro-client.tsx b/src/app/intro/intro-client.tsx index bcf6d05..b3d2a7c 100644 --- a/src/app/intro/intro-client.tsx +++ b/src/app/intro/intro-client.tsx @@ -2,15 +2,24 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import Button from "@/components/Componentes/button"; import NetworkImage from "@/components/Componentes/network-image"; import PageHeader from "@/components/Componentes/page-header"; import ReportActionsSheet from "@/components/Componentes/report-actions-sheet"; +import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; +import SliderPage from "@/components/Componentes/slider-page"; import VideoPlayer from "@/components/Componentes/video-player"; +import { + IntroMatchesIcon, + IntroMarriagesIcon, + IntroPlayButtonIcon, + IntroUsersIcon, +} from "@/components/Componentes/intro-hero-illustration"; import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useHabibWebReady } from "@/hooks/use-habib-web-ready"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { authBridge } from "@/lib/auth-bridge"; import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; @@ -26,6 +35,7 @@ export default function IntroClient() { const [isReportSheetOpen, setIsReportSheetOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [isPlayerOpen, setIsPlayerOpen] = useState(false); + const [isStepsOpen, setIsStepsOpen] = useState(false); const { data: config } = useMarriageConfigQuery(); @@ -33,6 +43,47 @@ export default function IntroClient() { // image so we don't need to wait for it — announce immediately on mount. useHabibWebReady(true); + useEffect(() => { + const readStepsFromUrl = () => { + if (typeof window === "undefined") return; + const params = new URLSearchParams(window.location.search); + setIsStepsOpen(params.get("steps") === "open"); + }; + + readStepsFromUrl(); + window.addEventListener("popstate", readStepsFromUrl); + return () => window.removeEventListener("popstate", readStepsFromUrl); + }, []); + + const handleOpenSteps = useCallback(() => { + setIsStepsOpen(true); + if (typeof window !== "undefined") { + const url = new URL(window.location.href); + url.searchParams.set("steps", "open"); + window.history.pushState({ steps: "open" }, "", url.toString()); + } + }, []); + + const handleCloseSteps = useCallback(() => { + if (typeof window !== "undefined") { + const params = new URLSearchParams(window.location.search); + if (params.get("steps") === "open") { + setIsStepsOpen(false); + window.history.back(); + return; + } + } + setIsStepsOpen(false); + }, []); + + useHardwareBackHandler(() => { + if (isStepsOpen) { + handleCloseSteps(); + return true; // handled: keep WebView open, just close the overlay + } + return false; // Root: tell Flutter to close WebView screen + }, isStepsOpen); + const handleSubmit = async () => { if (isSubmitting) { return; @@ -61,137 +112,127 @@ export default function IntroClient() { } const submitPath = getSubmitPath(profileResponse); - const nextPath = localizePath( - submitPath === "/intro" ? "/terms" : submitPath, - locale, - ); - router.push(nextPath); + if (submitPath === "/intro" || submitPath === "/terms") { + handleOpenSteps(); + } else { + router.push(localizePath(submitPath, locale)); + } } catch (error) { console.error("Submission/redirect failed", error); - router.push(localizePath("/terms", locale)); + handleOpenSteps(); } finally { setIsSubmitting(false); } }; return ( -
- {isReportSheetOpen && ( - setIsReportSheetOpen(false)} /> - )} - setIsReportSheetOpen(true), - }} - /> -
-
- {t["heavenly -

- {t["A Path to Heavenly Marriage"]} -

-

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

-
-
-
- {t["user -
-

120

-

- {t["user profiles"]} -

-
-
-
+ <> +
+ {isReportSheetOpen && ( + setIsReportSheetOpen(false)} /> + )} + setIsReportSheetOpen(true), + }} + /> +
+
{t["matches"]} -
-

14

-

- {t["matches"]} -

+

+ {t["A Path to Heavenly Marriage"]} +

+

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

+
+
+
+ +
+

120

+

+ {t["user profiles"]} +

+
+
+
+ +
+

14

+

+ {t["matches"]} +

+
+
+
+ +
+

14

+

+ {t["marriages"]} +

+
-
- {t["marriages"]} setIsPlayerOpen(true)} + > + -
-

14

-

- {t["marriages"]} -

-
+
+
-
-
setIsPlayerOpen(true)} - > - -
- {t["play"]} -
- setIsPlayerOpen(false)} - videoUrl={config?.intro_video_url} - /> -
-
- + +
-
-
-
+
+
+ + + {isStepsOpen && ( + + )} + + ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 39863b6..7f9c933 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -248,9 +248,6 @@ export default async function RootLayout({ }); var isWebView = !!(window.HabibApp && window.HabibApp.postMessage); - if (isWebView && !configApplied) { - root.dataset.webBootstrap = 'pending'; - } // 4. Queued web_ready Delivery Protocol // @@ -273,17 +270,8 @@ export default async function RootLayout({ if (!window.HabibApp || !window.HabibApp.postMessage) return false; window.__habibWebReadySent = true; - if (!configApplied) { - root.dataset.webBootstrap = 'pending'; - } + root.dataset.webBootstrap = 'ready'; window.HabibApp.postMessage(JSON.stringify({ action: 'web_ready' })); - - // Safety: release bootstrap-pending if initial_config never arrives - setTimeout(function() { - if (root.dataset.webBootstrap === 'pending') { - root.dataset.webBootstrap = 'ready'; - } - }, 2500); return true; } diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index c5e2058..624c821 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -53,28 +53,49 @@ const fieldCandidateMatchers = { "first_name", "last_name", "display_name", + "q1_full_name", ], - occupation: [ + age: ["age", "date_of_birth", "birth_date", "dob", "birth_year"], + residence: [ + "current_residence", + "residence", + "current_country_city", + "location", + "current_city", + "residence_city", + "city", + "country", + "birth_city", + "birthplace", + ], + educationLevel: [ + "highest_level_of_education", + "highest_education_level", + "education_level", + "education", + ], + fieldOfStudy: [ + "field_of_study", + "study_field", + "study", + ], + job: [ "job_title", - "occupation", + "job_title_and_description", "job", + "occupation", "profession", "career", - "work", - "highest_level_of_education", - "field_of_study", "employment_status", + "work", ], - age: ["age", "date_of_birth", "birth_date", "dob"], - city: [ - "current_residence", - "city", - "current_city", - "residence_city", - "location", - "residence", - "birthplace", - "birth_city", + hobbies: [ + "your_hobbies_and_main_interests", + "hobbies_and_interests", + "hobbies", + "interests", + "your_personality_traits", + "personality_traits", ], maritalStatus: [ "current_marital_status", @@ -243,15 +264,7 @@ function useMatchSummaryDisplay( displayName = `Profile #${matchSummary.id}`; } - // 2. Occupation - const occupation = pickField( - fields, - fieldCandidateMatchers.occupation, - usedIndexes, - t, - ); - - // 3. Age (extract and calculate from date_of_birth if available) + // 2. Age (extract and calculate from date_of_birth if available) const dobIdx = fields.findIndex( (f) => f.key === "personal_identity.date_of_birth" || @@ -275,10 +288,47 @@ function useMatchSummaryDisplay( age = pickField(fields, fieldCandidateMatchers.age, usedIndexes, t); } - // 4. City / Current Residence - const city = pickField(fields, fieldCandidateMatchers.city, usedIndexes, t); + // 3. Country / City / Residence + const residence = pickField( + fields, + fieldCandidateMatchers.residence, + usedIndexes, + t, + ); + + // 4. Highest level of education + const educationLevel = pickField( + fields, + fieldCandidateMatchers.educationLevel, + usedIndexes, + t, + ); + + // 5. Field of study + const fieldOfStudy = pickField( + fields, + fieldCandidateMatchers.fieldOfStudy, + usedIndexes, + t, + ); + + // 6. Job / Occupation + const job = pickField( + fields, + fieldCandidateMatchers.job, + usedIndexes, + t, + ); + + // 7. Hobbies & Interests + const hobbies = pickField( + fields, + fieldCandidateMatchers.hobbies, + usedIndexes, + t, + ); - // 5. Marital Status + // 8. Marital Status const maritalStatus = pickField( fields, fieldCandidateMatchers.maritalStatus, @@ -286,7 +336,7 @@ function useMatchSummaryDisplay( t, ); - // 6. City Preference / Relocation + // 9. City Preference / Relocation const cityPreference = pickField( fields, fieldCandidateMatchers.cityPreference, @@ -302,36 +352,41 @@ function useMatchSummaryDisplay( if (typeof window !== "undefined") { console.log( - "🔍 [useMatchSummaryDisplay] input matchSummary:", - matchSummary, + "🔍 [useMatchSummaryDisplay] computed 8 fields output:", + { + displayName, + age, + residence, + educationLevel, + fieldOfStudy, + job, + hobbies, + maritalStatus, + cityPreference, + extraFieldsCount: extraFields.length, + }, ); - console.log("🔍 [useMatchSummaryDisplay] computed output:", { - displayName, - occupation, - age, - city, - maritalStatus, - cityPreference, - extraFieldsCount: extraFields.length, - }); } return { age, - city, - cityPreference, - extraFields, + residence, + educationLevel, + fieldOfStudy, + job, + hobbies, maritalStatus, + cityPreference, name: displayName, - occupation, + extraFields, }; }, [matchSummary, t]); } function FieldLine({ field }: { field: DisplayField }) { return ( -

- {field.label}: +

+ {field.label}: {field.value}

); @@ -467,69 +522,75 @@ export default function NewMatchClient() {
- +
+ +
-
- {/* Header Section Skeleton */} -
+
+ {/* 1. Header Section Skeleton */} +
- - - + + +
-
- {/* Match Card Skeleton */} -
- {/* Name line */} - - - {/* Subtitle / Details lines */} -
- - - -
+ {/* 2. Match Card Skeleton */} +
+ {/* Name line */} + - {/* Button skeleton */} - -
+ {/* Subtitle / Details lines */} +
+ + +
- {/* Advisor Card Skeleton */} -
-
- -
- - -
+ {/* Button skeleton */} + +
-
-
- - - - -
+ {/* 3. Advisor Card Skeleton */} +
+
+ +
+ + +
- +
+
+ + + +
+ +
-
-
+
+
); } - const pairedFields = [matchDisplay.age, matchDisplay.city].filter( - (field): field is DisplayField => Boolean(field), - ); + + const pairedPersonalFields = [ + matchDisplay.age, + matchDisplay.residence, + ].filter((field): field is DisplayField => Boolean(field)); + + const pairedEduFields = [ + matchDisplay.educationLevel, + matchDisplay.fieldOfStudy, + ].filter((field): field is DisplayField => Boolean(field)); const isFemaleProfile = profile?.gender === "female"; const matchHeadingTitle = isFemaleProfile @@ -556,15 +617,18 @@ export default function NewMatchClient() {
- +
+ +
-
-
+
+ {/* 1. Header Section */} +
-

+

{matchHeadingTitle}

-

+

{matchHeadingDescription}

-
+ + {/* 2. Match Summary Card Section */} +
{isLoading ? ( -
+
{/* Name line */} {/* Subtitle / Details lines */} -
+
-
{/* Button skeleton */} - +
) : ( -
+
{isError ? (

Unable to load match summary.

) : matchSummary ? ( <> -

+

{matchDisplay.name}

-
- {matchDisplay.occupation ? ( - +
+ {pairedPersonalFields.length ? ( +

+ {pairedPersonalFields.map((field, index) => ( + + {index > 0 ? ( + | + ) : null} + + {field.label}:{" "} + + {field.value} + + ))} +

) : null} - {pairedFields.length ? ( -

- {pairedFields.map((field, index) => ( + {pairedEduFields.length ? ( +

+ {pairedEduFields.map((field, index) => ( - {index > 0 ? | : null} - - {field.label}: {field.value} + {index > 0 ? ( + | + ) : null} + + {field.label}:{" "} + {field.value} ))}

) : null} + {matchDisplay.job ? ( + + ) : null} + + {matchDisplay.hobbies ? ( + + ) : null} + {matchDisplay.maritalStatus ? ( ) : null} @@ -648,7 +736,7 @@ export default function NewMatchClient() { openProfile(); } }} - className="mt-[15px] inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white h-[40px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors cursor-pointer" + className="mt-3 inline-flex w-full items-center justify-center rounded-[10px] border-none bg-white h-[40px] text-[16px] font-semibold text-[#F0445B] no-underline shadow-none hover:bg-white/95 transition-colors cursor-pointer" > {t["View more details"]} @@ -660,21 +748,22 @@ export default function NewMatchClient() { )}
)} -
- -
+
+ + {/* 3. Advisor Section */} +
+
diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx index b838db4..4aa2ba8 100644 --- a/src/app/questions-list/questions-list-client.tsx +++ b/src/app/questions-list/questions-list-client.tsx @@ -52,6 +52,7 @@ import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; import { prefetchSectionsWithBoundedConcurrency } from "./section-prefetch"; import DevTapInstrumentation from "@/components/Componentes/dev-tap-instrumentation"; +import { fetchGeoCountryCode } from "@/components/Componentes/question-phone"; import SectionsRequest from "./sections-request"; import SectionOverlayHost from "@/components/Componentes/section-overlay-host"; import QuestionDetailClient from "@/app/questions-list/[slug]/question-detail-client"; @@ -101,6 +102,11 @@ export default function QuestionsListClient() { } }, [isProfileRedirecting, locale, profileTargetPath, router]); + // Background prefetch user's geo country code so phone question is pre-warmed + useEffect(() => { + void fetchGeoCountryCode(); + }, []); + // Signal Flutter to lift its loading cover once the profile is available // (either from SSR hydration or client-side fetch). useHabibWebReady(!!profile && !isProfileLoading); diff --git a/src/app/test-api/page.tsx b/src/app/test-api/page.tsx new file mode 100644 index 0000000..05d8de4 --- /dev/null +++ b/src/app/test-api/page.tsx @@ -0,0 +1,5 @@ +import TestApiPage from "../[lang]/test-api/page"; + +export default function RootTestApiPage() { + return ; +} diff --git a/src/components/Componentes/intro-hero-illustration.tsx b/src/components/Componentes/intro-hero-illustration.tsx new file mode 100644 index 0000000..2cd0734 --- /dev/null +++ b/src/components/Componentes/intro-hero-illustration.tsx @@ -0,0 +1,213 @@ +"use client"; + +import type { SVGProps } from "react"; +import { useId } from "react"; + +/** + * Inline SVG components for Intro page stats and video controls. + * Rendering these icons as inline SVG eliminates external HTTP requests, + * lazy-loading delay, and layout shifts — rendering instantly on frame 0. + */ + +export function IntroUsersIcon(props: SVGProps) { + const uid = useId().replace(/[^a-zA-Z0-9_-]/g, ""); + const gradId = `paint0_linear_users_${uid}`; + + return ( + + ); +} + +export function IntroMatchesIcon(props: SVGProps) { + const uid = useId().replace(/[^a-zA-Z0-9_-]/g, ""); + const maskId = `mask0_matches_${uid}`; + const gradId = `paint0_linear_matches_${uid}`; + + return ( + + ); +} + +export function IntroMarriagesIcon(props: SVGProps) { + const uid = useId().replace(/[^a-zA-Z0-9_-]/g, ""); + const gradId = `paint0_linear_marriages_${uid}`; + + return ( + + ); +} + +export function IntroPlayButtonIcon(props: SVGProps) { + const uid = useId().replace(/[^a-zA-Z0-9_-]/g, ""); + const filterId = `filter0_d_${uid}`; + + return ( + + ); +} diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index d759bf3..cdd55a2 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -132,11 +132,27 @@ function createQuestionField( ): MarriageField { let option_id: string | string[] | undefined; - if (question.options && Array.isArray(question.options) && question.options.length > 0) { + const isChoiceType = + question.type === "dropdown" || + question.type === "radio" || + question.type === "checkbox" || + question.type === "scale"; + + if (isChoiceType && question.options && Array.isArray(question.options) && question.options.length > 0) { if (Array.isArray(value)) { option_id = value; } else if (typeof value === "string" && value) { - const selectedOpt = question.options.find((opt) => opt.id === value || opt.value === value); + const strVal = value.trim().toLowerCase(); + const selectedOpt = question.options.find( + (opt) => + opt.id === value || + opt.value === value || + opt.label === value || + opt.id.toLowerCase() === strVal || + (typeof opt.value === "string" && opt.value.toLowerCase() === strVal) || + (typeof opt.label === "string" && opt.label.toLowerCase() === strVal) || + opt.id.toLowerCase().endsWith(`.${strVal}`) + ); option_id = selectedOpt ? selectedOpt.id : value; } } @@ -149,7 +165,7 @@ function createQuestionField( type: question.type, value, private: question.private, - option_id: option_id, + option_id: isChoiceType ? option_id : undefined, } as MarriageField; } diff --git a/src/components/Componentes/question-answer.test.tsx b/src/components/Componentes/question-answer.test.tsx index 0f99de7..d5ac6c3 100644 --- a/src/components/Componentes/question-answer.test.tsx +++ b/src/components/Componentes/question-answer.test.tsx @@ -90,6 +90,33 @@ function TestComponent({ slug }: { slug: string }) { Set Checkbox + + @@ -214,6 +241,51 @@ describe("Question Answer & Schema Integration", () => { }); }); + it("should send array of option_ids for multi-select dropdown", async () => { + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + render( + + + + + , + ); + + fireEvent.click(screen.getByTestId("set-multi-dropdown")); + fireEvent.click(screen.getByTestId("save")); + + await waitFor(() => { + expect(capturedPayload).not.toBeNull(); + const field = capturedPayload.fields.find( + (f: any) => f.key === "q_multi_drop", + ); + expect(field.option_id).toEqual(["opt_d1", "opt_d2"]); + }); + }); + it("batches all dirty answers into one mutation", async () => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, diff --git a/src/components/Componentes/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx index 7265702..25a706e 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 { createPortal } from "react-dom"; -import { getCountryList, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries"; +import { getCountryList, resolveCountryName, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries"; import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; @@ -10,6 +10,7 @@ import QuestionTitle from "./question-title"; import { LoadingThreeDot } from "./loading-three-dot"; import { useSheetScrollLock } from "./use-sheet-scroll-lock"; import { Input } from "@/components/ui/input"; +import { getUserGeoRegion, getStoredUserGeoRegion } from "@/lib/geo-region"; const EXIT_ANIMATION_MS = 220; @@ -88,9 +89,26 @@ export function QuestionBirthplace({ const { getAnswerValue, setAnswerValue, isLoading } = useQuestionAnswers(); const rawValue = getAnswerValue(question); + const isResidence = question.ui_config?.enable_geoip === true; + const storedRegion = isResidence ? getStoredUserGeoRegion() : null; + const initial = parseValue(rawValue); - const [selectedCountry, setSelectedCountry] = useState(initial.country); - const [cityInput, setCityInput] = useState(initial.city); + const localizedInitialCountry = resolveCountryName( + initial.country || storedRegion?.country, + locale, + ); + const initialCity = initial.city || storedRegion?.city || ""; + const initialLoc = + localizedInitialCountry || initialCity + ? [initialCity, localizedInitialCountry].filter(Boolean).join(", ") + : ""; + + const [selectedCountry, setSelectedCountry] = useState( + () => localizedInitialCountry || "", + ); + const [cityInput, setCityInput] = useState( + () => initialCity, + ); const [isOpen, setIsOpen] = useState(false); const [isClosing, setIsClosing] = useState(false); @@ -99,11 +117,9 @@ export function QuestionBirthplace({ const listRef = useRef(null); const isMountedRef = useRef(true); - const isResidence = question.ui_config?.enable_geoip === true; - const [mode, setMode] = useState<"auto" | "manual">("auto"); const [isDetecting, setIsDetecting] = useState(false); - const [detectedLocation, setDetectedLocation] = useState(""); + const [detectedLocation, setDetectedLocation] = useState(initialLoc); useEffect(() => { isMountedRef.current = true; @@ -152,89 +168,59 @@ export function QuestionBirthplace({ setAnswerValue(question, formatted); }; - // GeoIP detection logic - const detectLocation = (force = false) => { - const alreadyChecked = - typeof window !== "undefined" - ? localStorage.getItem("hasCheckedGeoIPResidence") - : "true"; - if (alreadyChecked && !force) { - if (rawValue) { - const parsed = parseValue(rawValue); - if (parsed.country && parsed.city) { - setDetectedLocation(`${parsed.city}, ${parsed.country}`); - setMode("auto"); - } else { - setMode("manual"); - } - } else { + // GeoIP detection logic using unified getUserGeoRegion + const detectLocation = useCallback(async (force = false) => { + // If there is already a saved answer and we are not forcing, display it + if (rawValue && !force) { + const parsed = parseValue(rawValue); + const cName = resolveCountryName(parsed.country, locale); + if (cName || parsed.city) { + const loc = [parsed.city, cName].filter(Boolean).join(", "); + setSelectedCountry(cName); + setCityInput(parsed.city); + setDetectedLocation(loc); setMode("auto"); + return; } - return; } - // If there is already a saved value and we are not forcing, do not fetch again - if (rawValue && !force) { - const parsed = parseValue(rawValue); - if (parsed.country && parsed.city) { - setDetectedLocation(`${parsed.city}, ${parsed.country}`); + setIsDetecting(true); + + try { + const region = await getUserGeoRegion(force); + if (!isMountedRef.current) return; + + const city = region.city || ""; + const rawCountry = region.country || region.countryCode || ""; + const country = resolveCountryName(rawCountry, locale) || rawCountry; + + if (city || country) { + const loc = [city, country].filter(Boolean).join(", "); + setSelectedCountry(country); + setCityInput(city); + setDetectedLocation(loc); + updateAnswers(country, city); setMode("auto"); } else { setMode("manual"); } - return; - } - - setIsDetecting(true); - if (typeof window !== "undefined") { - localStorage.setItem("hasCheckedGeoIPResidence", "true"); - } - fetch("https://ipapi.co/json/") - .then((res) => res.json()) - .then((data) => { - if (data && data.city && data.country_name) { - const city = data.city; - const country = data.country_name; - setSelectedCountry(country); - setCityInput(city); - setDetectedLocation(`${city}, ${country}`); - updateAnswers(country, city); - setMode("auto"); - } else { - throw new Error("Missing data"); - } - }) - .catch(() => { - fetch("https://ipwho.is/") - .then((res) => res.json()) - .then((data) => { - if (data && data.city && data.country) { - const city = data.city; - const country = data.country; - setSelectedCountry(country); - setCityInput(city); - setDetectedLocation(`${city}, ${country}`); - updateAnswers(country, city); - setMode("auto"); - } else { - setMode("manual"); - } - }) - .catch(() => { - setMode("manual"); - }); - }) - .finally(() => { + } catch { + if (isMountedRef.current) { + setMode("manual"); + } + } finally { + if (isMountedRef.current) { setIsDetecting(false); - }); - }; + } + } + }, [rawValue, locale]); useEffect(() => { if (isLoading) return; if (isResidence) { - detectLocation(); + void detectLocation(); } - }, [isResidence, isLoading]); + }, [isResidence, isLoading, detectLocation]); const handleAutoClick = () => { setMode("auto"); @@ -247,7 +233,8 @@ export function QuestionBirthplace({ } setMode("manual"); const parsed = parseValue(rawValue); - const country = selectedCountry || parsed.country; + const resolvedC = resolveCountryName(selectedCountry || parsed.country, locale); + const country = resolvedC || selectedCountry || parsed.country; const city = cityInput || parsed.city; setSelectedCountry(country); setCityInput(city); @@ -257,16 +244,17 @@ export function QuestionBirthplace({ // Synchronize state if rawValue changes externally useEffect(() => { const updated = parseValue(rawValue); - if (updated.country !== selectedCountry) { - setSelectedCountry(updated.country); + const resolvedC = resolveCountryName(updated.country, locale); + if (resolvedC && resolvedC !== selectedCountry) { + setSelectedCountry(resolvedC); } if (updated.city.trim() !== cityInput.trim()) { setCityInput(updated.city); } - if (updated.country && updated.city) { - setDetectedLocation(`${updated.city}, ${updated.country}`); + if (resolvedC || updated.city) { + setDetectedLocation([updated.city, resolvedC].filter(Boolean).join(", ")); } - }, [rawValue]); + }, [rawValue, locale]); const options = getCountryList(locale); const filteredOptions = options.filter((option) => @@ -576,7 +564,7 @@ export function QuestionBirthplace({ >
e.stopPropagation()} diff --git a/src/components/Componentes/question-card.tsx b/src/components/Componentes/question-card.tsx index 3ca0deb..fd78e5d 100644 --- a/src/components/Componentes/question-card.tsx +++ b/src/components/Componentes/question-card.tsx @@ -9,6 +9,7 @@ import { } from "@/lib/schema-adapter"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; +import { SectionIcon } from "./section-icon"; import { UiIcon, type UiIconName } from "./ui-icon"; type QuestionCardProps = { @@ -122,22 +123,11 @@ export function QuestionCard({ >
- {iconImageSrc ? ( - - ) : ( -
diff --git a/src/components/Componentes/question-checkbox.tsx b/src/components/Componentes/question-checkbox.tsx index 04c8445..9d1e199 100644 --- a/src/components/Componentes/question-checkbox.tsx +++ b/src/components/Componentes/question-checkbox.tsx @@ -42,7 +42,7 @@ export function QuestionCheckbox({ const isShortOptions = options.length <= 4 && - options.every((opt) => (opt.label || opt.value || "").length <= 15); + options.every((opt) => String(opt.label || opt.value || "").length <= 15); return (
{ answerMap[q.id] = val; }); +const httpMocks = vi.hoisted(() => ({ + get: vi.fn(), +})); + +vi.mock("@/lib/http", () => ({ + http: { get: httpMocks.get }, +})); + vi.mock("@/translations/provider", () => ({ useI18n: () => ({ locale: "en", @@ -22,6 +30,8 @@ vi.mock("@/translations/provider", () => ({ "Select country": "Select country", Close: "Close", Confirm: "Confirm", + "Enter a valid phone number with country code.": + "Enter a valid phone number with country code.", }, }), })); @@ -62,8 +72,6 @@ const phoneQuestion2: QuestionField = { options: [], }; - - describe("QuestionPhone IP country detection and shimmer", () => { beforeEach(() => { answerMap = {}; @@ -71,13 +79,47 @@ describe("QuestionPhone IP country detection and shimmer", () => { localStorage.clear(); resetGeoPhoneStateForTesting(); vi.restoreAllMocks(); + httpMocks.get.mockReset(); }); afterEach(() => { cleanup(); }); - it("renders shimmer on country button while IP request is pending, then shows resolved country code", async () => { + it("renders single unified shimmer on country button while IP request is pending, then shows resolved country code from Habib user region API", async () => { + let resolveRegion!: (value: unknown) => void; + const regionPromise = new Promise((resolve) => { + resolveRegion = resolve; + }); + + httpMocks.get.mockReturnValue(regionPromise); + + const { container } = render(); + + const shimmerElements = container.querySelectorAll(".shimmer-bg"); + expect(shimmerElements.length).toBe(1); + const input = screen.getByRole("textbox"); + expect(input.classList.contains("shimmer-bg")).toBe(false); + + await act(async () => { + resolveRegion({ + data: { + country: "Iran", + country_code: "IR", + }, + }); + }); + + await waitFor(() => { + expect(container.querySelectorAll(".shimmer-bg").length).toBe(0); + expect(screen.getByText("+98")).toBeDefined(); + expect(screen.getByText("🇮🇷")).toBeDefined(); + }); + }); + + it("falls back to secondary fetch when Habib region API fails and shows resolved code", async () => { + httpMocks.get.mockRejectedValue(new Error("Network failure")); + let resolveIpFetch!: (value: unknown) => void; const ipPromise = new Promise((resolve) => { resolveIpFetch = resolve; @@ -95,11 +137,6 @@ describe("QuestionPhone IP country detection and shimmer", () => { const { container } = render(); - const shimmerElements = container.querySelectorAll(".shimmer-bg"); - expect(shimmerElements.length).toBeGreaterThan(0); - const input = screen.getByRole("textbox"); - expect(input.classList.contains("shimmer-bg")).toBe(false); - await act(async () => { resolveIpFetch({ country_calling_code: "+98" }); }); @@ -111,7 +148,8 @@ describe("QuestionPhone IP country detection and shimmer", () => { }); }); - it("shows default country code when IP request fails", async () => { + it("shows default country code when all IP requests fail", async () => { + httpMocks.get.mockRejectedValue(new Error("Network failure")); vi.spyOn(globalThis, "fetch").mockRejectedValue( new Error("Network failure"), ); @@ -128,20 +166,12 @@ describe("QuestionPhone IP country detection and shimmer", () => { }); it("fetches IP country code only once when multiple fields are rendered and updates both", async () => { - let resolveIpFetch!: (value: unknown) => void; - const ipPromise = new Promise((resolve) => { - resolveIpFetch = resolve; + let resolveRegion!: (value: unknown) => void; + const regionPromise = new Promise((resolve) => { + resolveRegion = resolve; }); - const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(() => - ipPromise.then( - (data) => - ({ - ok: true, - json: async () => data, - }) as unknown as Response, - ), - ); + httpMocks.get.mockReturnValue(regionPromise); render( <> @@ -150,10 +180,15 @@ describe("QuestionPhone IP country detection and shimmer", () => { , ); - expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(httpMocks.get).toHaveBeenCalledTimes(1); await act(async () => { - resolveIpFetch({ country_calling_code: "+98" }); + resolveRegion({ + data: { + country: "Iran", + country_code: "IR", + }, + }); }); await waitFor(() => { @@ -170,41 +205,127 @@ describe("QuestionPhone IP country detection and shimmer", () => { phoneNumber: "2025550143", }; - const fetchSpy = vi.spyOn(globalThis, "fetch"); - const { container } = render(); expect(container.querySelectorAll(".shimmer-bg").length).toBe(0); expect(screen.getByText("+1")).toBeDefined(); - expect(screen.getByDisplayValue("2025550143")).toBeDefined(); - expect(fetchSpy).not.toHaveBeenCalled(); + expect(screen.getByDisplayValue("202-555-0143")).toBeDefined(); + expect(httpMocks.get).not.toHaveBeenCalled(); }); it("does not overwrite manual selection when user manually interacts", async () => { - let resolveIpFetch!: (value: unknown) => void; - const ipPromise = new Promise((resolve) => { - resolveIpFetch = resolve; + let resolveRegion!: (value: unknown) => void; + const regionPromise = new Promise((resolve) => { + resolveRegion = resolve; }); - vi.spyOn(globalThis, "fetch").mockImplementation(() => - ipPromise.then( - (data) => - ({ - ok: true, - json: async () => data, - }) as unknown as Response, - ), - ); + httpMocks.get.mockReturnValue(regionPromise); - render(); + render(); const input = screen.getByRole("textbox"); - fireEvent.change(input, { target: { value: "123456" } }); + fireEvent.change(input, { target: { value: "7400123456" } }); await act(async () => { - resolveIpFetch({ country_calling_code: "+98" }); + resolveRegion({ + data: { + country: "Iran", + country_code: "IR", + }, + }); + }); + + expect(screen.getByDisplayValue("7400 123456")).toBeDefined(); + }); + + it("formats Iranian phone numbers as 3-3-4 Telegram style as digits are typed", async () => { + httpMocks.get.mockResolvedValue({ + data: { country: "Iran", country_code: "IR" }, }); - expect(screen.getByDisplayValue("123456")).toBeDefined(); + render(); + + const input = screen.getByRole("textbox") as HTMLInputElement; + + // Type 3 digits + fireEvent.change(input, { target: { value: "912" } }); + expect(input.value).toBe("912"); + + // Type 6 digits + fireEvent.change(input, { target: { value: "912345" } }); + expect(input.value).toBe("912 345"); + + // Type 10 digits + fireEvent.change(input, { target: { value: "9123456789" } }); + expect(input.value).toBe("912 345 6789"); + }); + + it("shows dynamic placeholder based on resolved country code", async () => { + let resolveRegion!: (value: unknown) => void; + const regionPromise = new Promise((resolve) => { + resolveRegion = resolve; + }); + + httpMocks.get.mockReturnValue(regionPromise); + + render(); + + const input = screen.getByRole("textbox") as HTMLInputElement; + expect(input.placeholder).toBe("7400 123456"); + + await act(async () => { + resolveRegion({ + data: { + country: "Iran", + country_code: "IR", + }, + }); + }); + + await waitFor(() => { + expect(input.placeholder).toBe("912 345 6789"); + }); + }); + + it("does not show validation error on active typing, only shows error on blur if incomplete", async () => { + httpMocks.get.mockResolvedValue({ + data: { country: "Iran", country_code: "IR" }, + }); + + const { container } = render( + , + ); + + const input = screen.getByRole("textbox") as HTMLInputElement; + + // Focus and start typing incomplete number + fireEvent.focus(input); + fireEvent.change(input, { target: { value: "912" } }); + + // No error while focused and typing + expect( + screen.queryByText("Enter a valid phone number with country code."), + ).toBeNull(); + expect(container.querySelector(".border-\\[\\#F2465F\\]")).toBeNull(); + + // Blur input without completing + fireEvent.blur(input); + + // Error appears after blur + expect( + screen.getByText("Enter a valid phone number with country code."), + ).toBeDefined(); + expect(container.querySelector(".border-\\[\\#F2465F\\]")).not.toBeNull(); + + // Focus back and complete valid number + fireEvent.focus(input); + fireEvent.change(input, { target: { value: "9123456789" } }); + fireEvent.blur(input); + + // Error is gone + expect( + screen.queryByText("Enter a valid phone number with country code."), + ).toBeNull(); + expect(container.querySelector(".border-\\[\\#F2465F\\]")).toBeNull(); }); }); diff --git a/src/components/Componentes/question-phone.tsx b/src/components/Componentes/question-phone.tsx index f490360..0617f7b 100644 --- a/src/components/Componentes/question-phone.tsx +++ b/src/components/Componentes/question-phone.tsx @@ -1,9 +1,14 @@ "use client"; -import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; +import { + AsYouTypeFormatter, + PhoneNumberFormat, + PhoneNumberUtil, +} from "google-libphonenumber"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types"; +import { http } from "@/lib/http"; import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; @@ -23,134 +28,35 @@ type PhoneValueParts = { phoneValue: string; }; -const phoneUtil = PhoneNumberUtil.getInstance(); +import { + getUserGeoRegion, + getStoredUserGeoRegion, + setStoredUserGeoRegion, + subscribeToUserGeoRegion, + resetUserGeoRegionForTesting, +} from "@/lib/geo-region"; -// Module-level singleton state for IP phone country resolution -let cachedGeoCountryCode: string | null = null; -let geoIpPromise: Promise | null = null; -const geoListeners = new Set<(code: string) => void>(); +const phoneUtil = PhoneNumberUtil.getInstance(); export function resetGeoPhoneStateForTesting() { - cachedGeoCountryCode = null; - geoIpPromise = null; - geoListeners.clear(); + resetUserGeoRegionForTesting(); } function getStoredGeoCode(): string | null { - if (cachedGeoCountryCode) return cachedGeoCountryCode; - if (typeof window !== "undefined") { - try { - const stored = localStorage.getItem("geoIPPhoneCode"); - if (stored) { - cachedGeoCountryCode = stored; - return stored; - } - } catch {} - } - return null; + const region = getStoredUserGeoRegion(); + return region?.phoneCode || null; } export function setManuallySelectedGeoCode(code: string) { - cachedGeoCountryCode = code; - if (typeof window !== "undefined") { - try { - localStorage.setItem("geoIPPhoneCode", code); - localStorage.setItem("hasCheckedGeoIPPhone", "true"); - } catch {} - } + const current = getStoredUserGeoRegion() || {}; + setStoredUserGeoRegion({ ...current, phoneCode: code }); } -export function fetchGeoCountryCode(defaultCode = "+44"): Promise { - const existing = getStoredGeoCode(); - if (existing) { - return Promise.resolve(existing); - } - - if (geoIpPromise) { - return geoIpPromise.then((res) => res || defaultCode); - } - - geoIpPromise = (async () => { - try { - // 1. Primary: ipapi.co with 2s timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 2000); - try { - const res = await fetch("https://ipapi.co/json/", { - signal: controller.signal, - }); - clearTimeout(timeoutId); - if (res?.ok) { - const data = await res.json(); - if (data?.country_calling_code) { - const rawCode = String(data.country_calling_code).trim(); - const formatted = rawCode.startsWith("+") ? rawCode : `+${rawCode}`; - setManuallySelectedGeoCode(formatted); - geoListeners.forEach((fn) => { - fn(formatted); - }); - return formatted; - } - } - } catch { - clearTimeout(timeoutId); - } - - // 2. Secondary fallback: ipwho.is with 2s timeout - const secondaryController = new AbortController(); - const secondaryTimeoutId = setTimeout( - () => secondaryController.abort(), - 2000, - ); - try { - const res = await fetch("https://ipwho.is/", { - signal: secondaryController.signal, - }); - clearTimeout(secondaryTimeoutId); - if (res?.ok) { - const data = await res.json(); - if (data?.calling_code) { - const rawCode = String(data.calling_code).trim(); - const formatted = rawCode.startsWith("+") ? rawCode : `+${rawCode}`; - setManuallySelectedGeoCode(formatted); - geoListeners.forEach((fn) => { - fn(formatted); - }); - return formatted; - } - } - } catch { - clearTimeout(secondaryTimeoutId); - } - - // 3. Fallback to default - if (typeof window !== "undefined") { - try { - localStorage.setItem("geoIPPhoneCode", defaultCode); - localStorage.setItem("hasCheckedGeoIPPhone", "true"); - } catch {} - } - cachedGeoCountryCode = defaultCode; - geoListeners.forEach((fn) => { - fn(defaultCode); - }); - return defaultCode; - } catch { - if (typeof window !== "undefined") { - try { - localStorage.setItem("geoIPPhoneCode", defaultCode); - localStorage.setItem("hasCheckedGeoIPPhone", "true"); - } catch {} - } - cachedGeoCountryCode = defaultCode; - geoListeners.forEach((fn) => { - fn(defaultCode); - }); - return defaultCode; - } - })(); - - return geoIpPromise.then((res) => res || defaultCode); +export async function fetchGeoCountryCode( + defaultCode = "+44", +): Promise { + const region = await getUserGeoRegion(); + return region.phoneCode || defaultCode; } function isMarriagePhoneFieldValue( @@ -235,10 +141,11 @@ function readPhoneValue(value: unknown, fallbackCode: string): PhoneValueParts { function getMaxLengthForCountry(codeValue: string): number { try { const cleanCode = codeValue.replace(/[^\d]/g, ""); - if (!cleanCode) return 15; + if (!cleanCode) return 18; + if (cleanCode === "98") return 12; // 3 + 1 + 3 + 1 + 4 = 12 chars const countryCode = Number(cleanCode); const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode); - if (!regionCode || regionCode === "ZZ") return 15; + if (!regionCode || regionCode === "ZZ") return 18; const exampleMobile = phoneUtil.getExampleNumberForType( regionCode, @@ -254,9 +161,93 @@ function getMaxLengthForCountry(codeValue: string): number { : 0; const baseLen = Math.max(lenMobile, lenGeneral, 8); - return baseLen + 1; + return baseLen + 6; + } catch { + return 18; + } +} + +function getCountryPlaceholder( + codeValue: string, + defaultPlaceholder?: string, +): string { + try { + const cleanCode = codeValue.replace(/[^\d]/g, ""); + if (!cleanCode) { + return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789"; + } + + if (cleanCode === "98") { + return "912 345 6789"; + } + + const countryCode = Number(cleanCode); + const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode); + if (!regionCode || regionCode === "ZZ") { + return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789"; + } + + const example = + phoneUtil.getExampleNumberForType( + regionCode, + 1, // MOBILE + ) || phoneUtil.getExampleNumber(regionCode); + + if (!example) { + return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789"; + } + + const intl = phoneUtil.format(example, PhoneNumberFormat.INTERNATIONAL); + const prefix = `+${cleanCode} `; + if (intl.startsWith(prefix)) { + return intl.slice(prefix.length); + } + return String(example.getNationalNumber()); + } catch { + return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789"; + } +} + +function formatPhoneNumberAsYouType( + rawInput: string, + codeValue: string, +): string { + const cleanCode = codeValue.replace(/[^\d]/g, ""); + const rawDigits = rawInput.replace(/\D/g, ""); + if (!rawDigits) return ""; + + // Special telegram-style handling for Iran (+98): 3 - 3 - 4 (e.g. 912 345 6789) + if (cleanCode === "98") { + const digits = rawDigits.startsWith("0") ? rawDigits.slice(1) : rawDigits; + const max10 = digits.slice(0, 10); + if (max10.length <= 3) return max10; + if (max10.length <= 6) return `${max10.slice(0, 3)} ${max10.slice(3)}`; + return `${max10.slice(0, 3)} ${max10.slice(3, 6)} ${max10.slice(6)}`; + } + + const countryCode = Number(cleanCode); + const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode); + if (!regionCode || regionCode === "ZZ") { + return rawDigits; + } + + try { + const formatter = new AsYouTypeFormatter(regionCode); + let formatted = ""; + const fullNumber = `+${cleanCode}${rawDigits}`; + for (const char of fullNumber) { + formatted = formatter.inputDigit(char); + } + const codePrefix = `+${cleanCode} `; + if (formatted.startsWith(codePrefix)) { + return formatted.slice(codePrefix.length); + } + if (formatted.startsWith(`+${cleanCode}`)) { + return formatted.slice(`+${cleanCode}`.length).trim(); + } + return rawDigits; } catch { - return 15; + return rawDigits; } } @@ -404,13 +395,23 @@ export function QuestionPhone({ }, [value, defaultCodeValue]); const initialPhone = useMemo(() => { - return readPhoneValue(value, defaultCodeValue).phoneValue; - }, [value, defaultCodeValue]); + const rawPhone = readPhoneValue(value, defaultCodeValue).phoneValue; + return formatPhoneNumberAsYouType(rawPhone, initialCode); + }, [value, defaultCodeValue, initialCode]); const [codeValue, setCodeValue] = useState(initialCode); const [phoneValue, setPhoneValue] = useState(initialPhone); + const [isFocused, setIsFocused] = useState(false); + const [isTouched, setIsTouched] = useState(false); const lastCommittedValueRef = useRef(value); + const dynamicPlaceholder = useMemo(() => { + return getCountryPlaceholder( + codeValue || defaultCodeValue, + question.extras?.placeHolder, + ); + }, [codeValue, defaultCodeValue, question.extras?.placeHolder]); + const [isOpen, setIsOpen] = useState(false); const [isClosing, setIsClosing] = useState(false); const [searchQuery, setSearchQuery] = useState(""); @@ -439,19 +440,30 @@ export function QuestionPhone({ let isMounted = true; - const onGeoCodeResolved = (resolvedCode: string) => { + const unsubscribe = subscribeToUserGeoRegion((region) => { if (!isMounted || userInteractedRef.current) return; - setCodeValue(resolvedCode); - setIsResolvingCountry(false); - }; - - geoListeners.add(onGeoCodeResolved); + if (region.phoneCode) { + setCodeValue(region.phoneCode); + if (phoneValue) { + setPhoneValue((prev) => + formatPhoneNumberAsYouType(prev, region.phoneCode!), + ); + } + setIsResolvingCountry(false); + } + }); fetchGeoCountryCode(defaultCodeValue) .then((resolvedCode) => { if (!isMounted) return; if (!userInteractedRef.current) { - setCodeValue(resolvedCode || defaultCodeValue); + const finalCode = resolvedCode || defaultCodeValue; + setCodeValue(finalCode); + if (phoneValue) { + setPhoneValue((prev) => + formatPhoneNumberAsYouType(prev, finalCode), + ); + } } setIsResolvingCountry(false); }) @@ -465,12 +477,14 @@ export function QuestionPhone({ return () => { isMounted = false; - geoListeners.delete(onGeoCodeResolved); + unsubscribe(); }; - }, [hasExplicitValue, isResolvingCountry, defaultCodeValue]); + }, [hasExplicitValue, isResolvingCountry, defaultCodeValue, phoneValue]); const normalizedPhoneState = getNormalizedPhoneValue(codeValue, phoneValue); const showInvalidState = + isTouched && + !isFocused && codeValue.trim().length > 0 && phoneValue.trim().length > 0 && !normalizedPhoneState.isValid; @@ -602,13 +616,19 @@ export function QuestionPhone({ if (explicit) { setIsResolvingCountry(false); setCodeValue(nextValue.codeValue); - const maxLen = getMaxLengthForCountry(nextValue.codeValue); - setPhoneValue(nextValue.phoneValue.slice(0, maxLen)); + const formatted = formatPhoneNumberAsYouType( + nextValue.phoneValue, + nextValue.codeValue, + ); + setPhoneValue(formatted); } else if (value !== null && value !== undefined) { const resolvedCode = cachedCode || defaultCodeValue; setCodeValue(resolvedCode); - const maxLen = getMaxLengthForCountry(resolvedCode); - setPhoneValue(nextValue.phoneValue.slice(0, maxLen)); + const formatted = formatPhoneNumberAsYouType( + nextValue.phoneValue, + resolvedCode, + ); + setPhoneValue(formatted); } lastCommittedValueRef.current = value; @@ -635,12 +655,11 @@ export function QuestionPhone({ userInteractedRef.current = true; setIsResolvingCountry(false); setManuallySelectedGeoCode(selectedCode); - const maxLen = getMaxLengthForCountry(selectedCode); - const truncatedPhone = phoneValue.slice(0, maxLen); + const reformatted = formatPhoneNumberAsYouType(phoneValue, selectedCode); setCodeValue(selectedCode); - setPhoneValue(truncatedPhone); - updateStoredValue(selectedCode, truncatedPhone); + setPhoneValue(reformatted); + updateStoredValue(selectedCode, reformatted); closeSheet(); }; @@ -691,12 +710,10 @@ export function QuestionPhone({ > {isResolvingCountry ? ( ) : ( <> @@ -741,19 +758,27 @@ export function QuestionPhone({ data-bwignore="true" data-form-type="other" disabled={disabled} - placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")} + placeholder={dynamicPlaceholder} value={phoneValue} maxLength={getMaxLengthForCountry(codeValue)} + onFocus={() => { + setIsFocused(true); + }} + onBlur={() => { + setIsFocused(false); + setIsTouched(true); + }} onChange={(event) => { userInteractedRef.current = true; setIsResolvingCountry(false); setManuallySelectedGeoCode(codeValue); - const nextPhoneValue = sanitizePhoneNumber(event.target.value); - const maxLen = getMaxLengthForCountry(codeValue); - const truncatedPhone = nextPhoneValue.slice(0, maxLen); + const formatted = formatPhoneNumberAsYouType( + event.target.value, + codeValue, + ); - setPhoneValue(truncatedPhone); - updateStoredValue(codeValue, truncatedPhone); + setPhoneValue(formatted); + updateStoredValue(codeValue, formatted); }} dir="ltr" className="h-full w-full border-0 bg-transparent p-0 text-left text-[15px] font-medium leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#98A2B3]" @@ -762,7 +787,8 @@ export function QuestionPhone({
{showInvalidState ? ( - Enter a valid phone number with country code. + {t["Enter a valid phone number with country code."] || + "Enter a valid phone number with country code."} ) : null} @@ -782,7 +808,7 @@ export function QuestionPhone({ >
diff --git a/src/components/Componentes/question-radio.tsx b/src/components/Componentes/question-radio.tsx index 34ad9b0..826e785 100644 --- a/src/components/Componentes/question-radio.tsx +++ b/src/components/Componentes/question-radio.tsx @@ -25,7 +25,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.label || opt.value || "").length <= 15); + options.every((opt) => String(opt.label || opt.value || "").length <= 15); return (
({ useI18n: vi.fn(() => ({ locale: "fa", dictionary: { Confirm: "تایید" } })), })); +vi.mock("@/hooks/marriage/use-profile-main", () => ({ + useMarriageProfileQuery: vi.fn(() => ({ + data: { id: 1, can_edit_profile: true }, + isLoading: false, + })), +})); + vi.mock("@/hooks/marriage/use-section-data", () => ({ applyProfilePatchResultToCache: vi.fn(), useMarriageSectionDataQuery: vi.fn(() => ({ @@ -91,16 +98,19 @@ describe("QuestionSheet component", () => { fireEvent.click(iranBtn); // Trigger should now show Iran - await waitFor(() => { - expect(screen.getByText("ایران")).toBeDefined(); - expect(document.body.classList.contains("dropdown-open")).toBe(false); - expect(document.body.classList.contains("question-sheet-open")).toBe( - false, - ); - expect( - (document.querySelector(".app-shell") as HTMLElement).style.overflowY, - ).toBe(""); - }); + await waitFor( + () => { + expect(screen.getByText("ایران")).toBeDefined(); + expect(document.body.classList.contains("dropdown-open")).toBe(false); + expect(document.body.classList.contains("question-sheet-open")).toBe( + false, + ); + expect( + (document.querySelector(".app-shell") as HTMLElement).style.overflowY, + ).toBe(""); + }, + { timeout: 1000 }, + ); }); it("supports multi-selection with confirm button", async () => { diff --git a/src/components/Componentes/question-sheet.tsx b/src/components/Componentes/question-sheet.tsx index 89926b8..9e159f4 100644 --- a/src/components/Componentes/question-sheet.tsx +++ b/src/components/Componentes/question-sheet.tsx @@ -2,7 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; -import { getLanguageList, LANGUAGES_EN, LANGUAGES_FA } from "@/data/languages"; +import { COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries"; +import { LANGUAGE_EN_TO_FA, LANGUAGES_EN, LANGUAGES_FA } from "@/data/languages"; import type { QuestionField } from "@/lib/schema-adapter"; import { useI18n } from "@/translations/provider"; import { Button } from "./button"; @@ -81,63 +82,143 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { question.title?.includes("زبان") || question.ui_config?.dataset === "languages"; + const isCountryQuestion = + question.id?.toLowerCase().includes("nationality") || + question.id?.toLowerCase().includes("citizenship") || + question.id?.toLowerCase().includes("country") || + question.id?.toLowerCase().includes("birthplace") || + question.id?.toLowerCase().includes("residence") || + question.title?.toLowerCase().includes("nationality") || + question.title?.toLowerCase().includes("citizenship") || + question.title?.toLowerCase().includes("country") || + question.title?.includes("ملیت") || + question.title?.includes("تابعیت") || + question.title?.includes("کشور") || + question.title?.includes("سکونت") || + question.ui_config?.dataset === "countries" || + question.ui_config?.dataset === "nationalities"; + const options = useMemo(() => { const rawOptions = question.options || []; - if (!isLanguageQuestion) { - return rawOptions; + + if (isLanguageQuestion) { + const existingByLabel = new Map( + rawOptions.map((opt) => [opt.label.toLowerCase().trim(), opt]), + ); + const existingByVal = new Map( + rawOptions.map((opt) => [String(opt.value).toLowerCase().trim(), opt]), + ); + const existingById = new Map( + rawOptions.map((opt) => [opt.id.toLowerCase().trim(), opt]), + ); + + const mergedOptions: typeof rawOptions = []; + const seenIds = new Set(); + + LANGUAGES_EN.forEach((enLang, idx) => { + const localizedLabel = + (locale === "fa" || locale === "fa-ir" + ? (LANGUAGE_EN_TO_FA[enLang] || LANGUAGES_FA[idx]) + : (t as any)[enLang]) || enLang; + const cleanSlug = enLang + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, ""); + const generatedId = `${question.id}.${cleanSlug}`; + + const existing = + existingByLabel.get(enLang.toLowerCase()) || + existingByLabel.get(localizedLabel.toLowerCase()) || + existingByVal.get(cleanSlug) || + existingById.get(generatedId.toLowerCase()); + + const finalId = existing?.id || generatedId; + if (seenIds.has(finalId)) return; + seenIds.add(finalId); + + const isPersian = locale === "fa" || locale === "fa-ir"; + const displayLabel = isPersian + ? (LANGUAGE_EN_TO_FA[enLang] || localizedLabel) + : enLang; + + mergedOptions.push({ + id: finalId, + value: existing?.value ?? cleanSlug, + label: displayLabel, + order: existing?.order ?? idx, + }); + }); + + rawOptions.forEach((opt) => { + if (!seenIds.has(opt.id)) { + seenIds.add(opt.id); + mergedOptions.push(opt); + } + }); + + return mergedOptions; } - const existingByLabel = new Map( - rawOptions.map((opt) => [opt.label.toLowerCase().trim(), opt]), - ); - const existingByVal = new Map( - rawOptions.map((opt) => [String(opt.value).toLowerCase().trim(), opt]), - ); - const existingById = new Map( - rawOptions.map((opt) => [opt.id.toLowerCase().trim(), opt]), - ); + if (isCountryQuestion) { + const existingByLabel = new Map( + rawOptions.map((opt) => [opt.label.toLowerCase().trim(), opt]), + ); + const existingByVal = new Map( + rawOptions.map((opt) => [String(opt.value).toLowerCase().trim(), opt]), + ); + const existingById = new Map( + rawOptions.map((opt) => [opt.id.toLowerCase().trim(), opt]), + ); + + const mergedOptions: typeof rawOptions = []; + const seenIds = new Set(); + + COUNTRIES_EN.forEach((enCountry, idx) => { + const localizedLabel = + (locale === "fa" || locale === "fa-ir" + ? COUNTRIES_FA[idx] + : (t as any)[enCountry]) || enCountry; + const cleanSlug = enCountry + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_+|_+$/g, ""); + const generatedId = `${question.id}.${cleanSlug}`; + + const existing = + existingByLabel.get(enCountry.toLowerCase()) || + existingByLabel.get(localizedLabel.toLowerCase()) || + existingByVal.get(cleanSlug) || + existingById.get(generatedId.toLowerCase()); + + const finalId = existing?.id || generatedId; + if (seenIds.has(finalId)) return; + seenIds.add(finalId); + + const isPersian = locale === "fa" || locale === "fa-ir"; + const displayLabel = isPersian + ? (COUNTRIES_FA[idx] || localizedLabel) + : enCountry; + + mergedOptions.push({ + id: finalId, + value: existing?.value ?? cleanSlug, + label: displayLabel, + order: existing?.order ?? idx, + }); + }); - const mergedOptions: typeof rawOptions = []; - const seenIds = new Set(); - - LANGUAGES_EN.forEach((enLang, idx) => { - const localizedLabel = - (locale === "fa" || locale === "fa-ir" - ? LANGUAGES_FA[idx] - : (t as any)[enLang]) || enLang; - const cleanSlug = enLang - .toLowerCase() - .replace(/[^a-z0-9]+/g, "_") - .replace(/^_+|_+$/g, ""); - const generatedId = `${question.id}.${cleanSlug}`; - - const existing = - existingByLabel.get(enLang.toLowerCase()) || - existingByLabel.get(localizedLabel.toLowerCase()) || - existingByVal.get(cleanSlug) || - existingById.get(generatedId.toLowerCase()); - - const finalId = existing?.id || generatedId; - if (seenIds.has(finalId)) return; - seenIds.add(finalId); - - mergedOptions.push({ - id: finalId, - value: existing?.value ?? cleanSlug, - label: existing?.label ?? localizedLabel, - order: existing?.order ?? idx, + rawOptions.forEach((opt) => { + if (!seenIds.has(opt.id)) { + seenIds.add(opt.id); + mergedOptions.push(opt); + } }); - }); - rawOptions.forEach((opt) => { - if (!seenIds.has(opt.id)) { - seenIds.add(opt.id); - mergedOptions.push(opt); - } - }); + return mergedOptions; + } - return mergedOptions; - }, [question, isLanguageQuestion, locale, t]); + return rawOptions; + }, [question, isLanguageQuestion, isCountryQuestion, locale, t]); const COMPACT_OPTIONS_MAX = 6; @@ -166,7 +247,14 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { ); const getCleanLabel = (optId: string) => { - const opt = options.find((o) => o.id === optId); + const normalized = String(optId || "").toLowerCase().trim(); + const opt = options.find( + (o) => + o.id === optId || + o.id.toLowerCase() === normalized || + String(o.value).toLowerCase() === normalized || + o.id.endsWith(`.${normalized}`), + ); if (!opt) return optId; return opt.label.split(" - ")[0]; }; @@ -287,7 +375,9 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { ref={sheetRef} className={[ "flex w-full flex-col overflow-hidden rounded-t-[22px] bg-white pb-[env(safe-area-inset-bottom)] shadow-[0_-10px_32px_rgba(0,0,0,0.12)] transition-transform duration-[300ms] ease-out sm:max-w-[375px] animate-in slide-in-from-bottom", - isCompact ? "h-auto max-h-[82svh]" : "h-[82svh]", + isCompact && !showSearch + ? "h-auto max-h-[82svh]" + : "h-[82svh] min-h-[82svh] max-h-[82svh]", isClosing ? "translate-y-full" : "translate-y-0", ].join(" ")} > diff --git a/src/components/Componentes/question-snap-list.tsx b/src/components/Componentes/question-snap-list.tsx index 53d5a75..b73a6e5 100644 --- a/src/components/Componentes/question-snap-list.tsx +++ b/src/components/Componentes/question-snap-list.tsx @@ -14,20 +14,20 @@ import { useQuestionViewportCoordinator, } from "./question-viewport-coordinator"; -const WHEEL_GESTURE_IDLE_MS = 320; -const BACKGROUND_DRAG_SLOP = 10; -const OPTION_CARD_DRAG_SLOP = 16; -const FAST_FLICK_MIN_DISTANCE = 36; -const DRAG_COMMIT_RATIO = 0.3; -const DRAG_FLICK_VELOCITY = 0.4; -const SNAP_ANIMATION_MS = 340; -const SNAP_EASE = "cubic-bezier(0.22, 1, 0.36, 1)"; -const RUBBER_BAND_RESISTANCE = 0.4; +const WHEEL_GESTURE_IDLE_MS = 280; +const BACKGROUND_DRAG_SLOP = 8; +const OPTION_CARD_DRAG_SLOP = 14; +const TEXT_INPUT_DRAG_SLOP = 16; +const FAST_FLICK_MIN_DISTANCE = 24; +const DRAG_COMMIT_RATIO = 0.15; +const DRAG_FLICK_VELOCITY = 0.22; +const SNAP_ANIMATION_MS = 240; +const SNAP_EASE = "cubic-bezier(0.16, 1, 0.3, 1)"; +const RUBBER_BAND_RESISTANCE = 0.35; /** - * Text inputs and direct embedded controls that MUST be completely isolated - * from the snap/drag gesture recognizer so first-tap focus, virtual keyboard, - * and caret manipulation work 100% reliably. + * Text inputs and direct embedded controls that MUST allow clean 1st-tap focus + * while still permitting intentional vertical swipe when dragged beyond slop. */ const TEXT_INPUT_SELECTOR = [ 'input:not([type="radio"]):not([type="checkbox"])', @@ -58,6 +58,7 @@ const OPTION_CARD_SELECTOR = [ type SnapDragState = { pointerDown: boolean; hardIgnored: boolean; + isTextInput: boolean; isOptionCard: boolean; engaged: boolean; didDrag: boolean; @@ -113,6 +114,7 @@ export function QuestionSnapList({ const dragRef = useRef({ pointerDown: false, hardIgnored: false, + isTextInput: false, isOptionCard: false, engaged: false, didDrag: false, @@ -397,14 +399,19 @@ export function QuestionSnapList({ const movedPastFlick = Math.abs(drag.offset) >= FAST_FLICK_MIN_DISTANCE; let direction: 0 | 1 | -1 = 0; + let reason = "none"; if (!cancelled && (draggedFar || flicked || movedPastFlick)) { if ((drag.offset > 0 || drag.velocity > 0) && canNext) { direction = 1; + reason = flicked ? "flick_next" : (draggedFar ? "drag_far_next" : "moved_past_flick_next"); } else if ((drag.offset < 0 || drag.velocity < 0) && canPrev) { direction = -1; + reason = flicked ? "flick_prev" : (draggedFar ? "drag_far_prev" : "moved_past_flick_prev"); } } + console.log(`[Snap] FinishDrag -> dir: ${direction} (${reason}) | offset: ${drag.offset.toFixed(0)}px | vel: ${drag.velocity.toFixed(2)} | threshold: ${(drag.height * DRAG_COMMIT_RATIO).toFixed(0)}px`); + if (direction === 0) { snapPanelsTo(0); return; @@ -491,6 +498,8 @@ export function QuestionSnapList({ drag.startTime = performance.now(); drag.lastMoveTime = drag.startTime; touchStartYRef.current = drag.startY; + + console.log(`[Snap] TouchStart at (${drag.startX.toFixed(0)}, ${drag.startY.toFixed(0)}) on <${target?.tagName?.toLowerCase()}> (isOption: ${isOptionCard})`); }; const onTouchMove = (event: TouchEvent) => { @@ -526,7 +535,7 @@ export function QuestionSnapList({ } // Must be primarily vertical gesture (avoid hijacking horizontal swipe / scroll) - if (absY < absX * 1.1) { + if (absY < absX * 0.9) { return; } @@ -535,8 +544,13 @@ export function QuestionSnapList({ drag.didDrag = true; suppressNextClickRef.current = true; + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + const elapsed = Math.max(1, now - drag.startTime); drag.velocity = (drag.startY - currentY) / elapsed; + console.log(`[Snap] Drag Engaged: deltaY=${deltaY.toFixed(0)}px, slop=${slop}px`); } // Non-passive preventDefault stops native pan and guarantees gesture ownership diff --git a/src/components/Componentes/question-viewport-coordinator.ts b/src/components/Componentes/question-viewport-coordinator.ts index 111d21b..e63b281 100644 --- a/src/components/Componentes/question-viewport-coordinator.ts +++ b/src/components/Componentes/question-viewport-coordinator.ts @@ -3,7 +3,7 @@ import { useEffect } from "react"; import { viewPaddingsBridge } from "@/lib/view-paddings"; -const KEYBOARD_THRESHOLD = 80; +const KEYBOARD_THRESHOLD = 20; const QUESTION_GAP = 16; const LIFT_VARIABLE = "--question-lift-y"; @@ -82,19 +82,31 @@ export function isActiveQuestionKeyboardInput( ); } +function getDefaultKeyboardHeight(): number { + if (typeof window !== "undefined" && window.innerHeight > 0) { + // 37.5% of screen height matches standard mobile soft keyboards (e.g. 287px on 765px screen, 316px on 844px screen) + return Math.round(window.innerHeight * 0.375); + } + return 285; +} + function setLift(nextLift: number) { - currentLift = Math.max(0, nextLift); + const prevLift = currentLift; + currentLift = Math.max(0, Math.round(nextLift)); document.documentElement.style.setProperty(LIFT_VARIABLE, `${currentLift}px`); - document.body.classList.toggle( - "question-keyboard-open", - currentLift > 0 && keyboardVisible && activeQuestionInput !== null, - ); + const isOpen = currentLift > 0 && keyboardVisible && activeQuestionInput !== null; + document.body.classList.toggle("question-keyboard-open", isOpen); + console.log(`[Coord] setLift: ${prevLift}px -> ${currentLift}px | visible: ${keyboardVisible} | class: ${isOpen}`); } function getKeyboardTop() { if (!keyboardVisible || !activeQuestionInput) return null; - const flutterTop = window.innerHeight - keyboardHeight; + const fullKeyboardHeight = + lastKeyboardHeight > 200 ? lastKeyboardHeight : getDefaultKeyboardHeight(); + const targetHeight = Math.max(keyboardHeight, fullKeyboardHeight); + + const flutterTop = window.innerHeight - targetHeight; const viewport = window.visualViewport; const viewportBottom = viewport ? viewport.offsetTop + viewport.height @@ -103,11 +115,29 @@ function getKeyboardTop() { return Math.min(flutterTop, viewportBottom); } +function getBaseGeometry(content: HTMLElement) { + const item = content.closest(".question-snap-item"); + if (item && typeof item.getBoundingClientRect === "function" && content.offsetHeight > 0) { + const itemRect = item.getBoundingClientRect(); + const top = itemRect.top + content.offsetTop; + return { + baseTop: top, + baseBottom: top + content.offsetHeight, + }; + } + const rect = content.getBoundingClientRect(); + return { + baseTop: rect.top + currentLift, + baseBottom: rect.bottom + currentLift, + }; +} + function updateLift() { const content = document.querySelector( '.question-snap-item[aria-current="step"] .question-snap-content', ); if (!content) { + console.log('[Coord] updateLift: no content element found -> lift 0'); setLift(0); return; } @@ -117,29 +147,28 @@ function updateLift() { (value): value is number => value !== null, ); if (visibleBottomCandidates.length === 0) { + console.log('[Coord] updateLift: no visibleBottom candidates -> lift 0'); setLift(0); return; } - const rect = content.getBoundingClientRect(); const snapList = content.closest(".question-snap-list"); - const baseTop = rect.top + currentLift; - const baseBottom = rect.bottom + currentLift; + const { baseTop, baseBottom } = getBaseGeometry(content); const safeTop = Number.parseFloat( getComputedStyle(document.documentElement).getPropertyValue("--safe-top"), ); - setLift( - computeQuestionLift({ - baseTop, - baseBottom, - visibleTop: Math.max( - Number.isFinite(safeTop) ? safeTop : 0, - snapList?.getBoundingClientRect().top ?? 0, - ), - visibleBottom: Math.min(...visibleBottomCandidates), - }), - ); + const computed = computeQuestionLift({ + baseTop, + baseBottom, + visibleTop: Math.max( + Number.isFinite(safeTop) ? safeTop : 0, + snapList?.getBoundingClientRect().top ?? 0, + ), + visibleBottom: Math.min(...visibleBottomCandidates), + }); + console.log(`[Coord] computed lift: ${computed}px (baseTop: ${baseTop.toFixed(0)}, baseBottom: ${baseBottom.toFixed(0)}, kTop: ${keyboardTop?.toFixed(0)})`); + setLift(computed); } function scheduleLiftUpdate() { @@ -151,13 +180,34 @@ function scheduleLiftUpdate() { } export function updateQuestionKeyboardHeight(height: number) { - keyboardHeight = Math.max(0, height); - keyboardVisible = keyboardHeight > KEYBOARD_THRESHOLD; - if (keyboardVisible) { - lastKeyboardHeight = keyboardHeight; + const nextHeight = Math.max(0, height); + if (Math.abs(nextHeight - keyboardHeight) < 1) return; + + console.log(`[Coord] updateHeight: ${keyboardHeight} -> ${nextHeight}`); + + if (nextHeight > 0) { + keyboardVisible = true; + const defaultKHeight = getDefaultKeyboardHeight(); + const expectedHeight = + lastKeyboardHeight > 200 ? lastKeyboardHeight : defaultKHeight; + + if (activeQuestionInput && nextHeight < expectedHeight - 15) { + // Keyboard is in the middle of opening animation frames (e.g. Samsung 8.7px, 182px, 205px, 246px, 262px, 273px) + // Keep keyboardHeight at expectedHeight so lift doesn't stutter/jitter during intermediate animation frames. + keyboardHeight = expectedHeight; + } else { + // Settled at or above target height + lastKeyboardHeight = Math.max(lastKeyboardHeight, nextHeight); + keyboardHeight = nextHeight; + } + if (isActiveQuestionKeyboardInput(document.activeElement)) { activeQuestionInput = document.activeElement; } + } else { + // nextHeight === 0: keyboard was closed + keyboardHeight = 0; + keyboardVisible = false; } scheduleLiftUpdate(); } @@ -176,13 +226,16 @@ export function resetQuestionKeyboardState() { window.cancelAnimationFrame(geometryFrame); geometryFrame = null; } - if (isActiveQuestionKeyboardInput(document.activeElement)) { + if ( + typeof document !== "undefined" && + document.activeElement instanceof HTMLElement && + isActiveQuestionKeyboardInput(document.activeElement) + ) { document.activeElement.blur(); } - activeQuestionInput = null; keyboardVisible = false; keyboardHeight = 0; - compactSheetTop = null; + activeQuestionInput = null; setLift(0); } @@ -193,25 +246,29 @@ export function useQuestionViewportCoordinator() { const handleFocusIn = (event: FocusEvent) => { if (!isActiveQuestionKeyboardInput(event.target)) return; activeQuestionInput = event.target; - - // Start with the last known keyboard geometry so lifting begins in the - // same frame as focus; Flutter then replaces it with the exact height. - if (!keyboardVisible && lastKeyboardHeight > KEYBOARD_THRESHOLD) { - keyboardHeight = lastKeyboardHeight; - keyboardVisible = true; - } + const expectedHeight = + lastKeyboardHeight > 200 ? lastKeyboardHeight : getDefaultKeyboardHeight(); + keyboardHeight = expectedHeight; + keyboardVisible = true; + console.log('[Coord] focusin on input:', (event.target as HTMLElement).tagName, 'targetKHeight:', expectedHeight); scheduleLiftUpdate(); }; const handleFocusOut = () => { + console.log('[Coord] focusout event'); window.setTimeout(() => { activeQuestionInput = isActiveQuestionKeyboardInput( document.activeElement, ) ? document.activeElement : null; - if (!activeQuestionInput) scheduleLiftUpdate(); - }, 0); + console.log('[Coord] focusout settled, active:', activeQuestionInput?.tagName ?? 'none'); + if (!activeQuestionInput) { + keyboardVisible = false; + keyboardHeight = 0; + scheduleLiftUpdate(); + } + }, 50); }; const unsubscribeConfig = viewPaddingsBridge.subscribeConfig((config) => { @@ -222,21 +279,21 @@ export function useQuestionViewportCoordinator() { const viewportHeight = window.visualViewport?.height ?? window.innerHeight; const reduction = closedViewportHeight - viewportHeight; + console.log(`[Coord] visualViewport resize: vpHeight=${viewportHeight}, reduction=${reduction}`); if (reduction > KEYBOARD_THRESHOLD) { if (isActiveQuestionKeyboardInput(document.activeElement)) { activeQuestionInput = document.activeElement; keyboardVisible = true; - keyboardHeight = Math.max(keyboardHeight, reduction); - lastKeyboardHeight = keyboardHeight; - scheduleLiftUpdate(); + if (reduction > 250) { + lastKeyboardHeight = Math.max(lastKeyboardHeight, reduction); + keyboardHeight = reduction; + } } - } else if (keyboardHeight <= KEYBOARD_THRESHOLD) { - keyboardVisible = false; - keyboardHeight = 0; + } else if (reduction <= KEYBOARD_THRESHOLD && !keyboardVisible && !activeQuestionInput) { closedViewportHeight = viewportHeight; - scheduleLiftUpdate(); } + scheduleLiftUpdate(); }; document.addEventListener("focusin", handleFocusIn); diff --git a/src/components/Componentes/section-icon.tsx b/src/components/Componentes/section-icon.tsx new file mode 100644 index 0000000..0058642 --- /dev/null +++ b/src/components/Componentes/section-icon.tsx @@ -0,0 +1,63 @@ +"use client"; + +import type { SVGProps } from "react"; + +export type SectionIconName = + | "profile" + | "contact" + | "health" + | "education" + | "family" + | "marriage" + | "family_marital" + | "lifestyle" + | "criteria" + | "verification" + | "personality" + | "glasser" + | "details" + | "checklist" +; + +const SECTION_ICON_DATA_URIS: Record = { + "profile": "data:image/webp;base64,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", + "contact": "data:image/webp;base64,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", + "health": "data:image/webp;base64,UklGRu4OAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSIMFAAAB8HZrt2nbtm0P6E0pla5hTtu2bdu2bdu2bdu2zWG2bls1p5TeD7XUopYjYgIwpKhZ05ipYHxRa5rGTAWTVTN01MZkBLFG0FFNpyCmAJoLXfd2D3/knW5wiTUAYDqQGgAcdbmb3udRj7jjdS+0AoCYjGUALvvYL/x3T3J+/zk/fPVtjwJUB1AFjrrdm3+x7hDnY/cpX3vqVQGYjKGCFff7/l7OZynB1nPfez1ApYcYcN13ncdWLyU4f/iXjzgK0OEMeODfSbpHJklmhpcgy9dvCFgnA67zFSejeCbnM8Od5NlPaWBDGS77XdI92TcKmZ+4OEwWNTj23TOyBPumO/nnG0NlEMN9t9CDw3py00MBbRHFLU4hncOGc/ZsiAxgeB7pHL6QH1wDAyCCZ8xYkoM7+eFGpJfhRYzkmOn81cVhUMH7mcExs/ArjUqPBk9iSY5ceOY1sBIrv0xPjjzjJ2DdDLdzT45euOU2OPEHLOyaw7DwhbAuoicvMThB5+FP/Z/OCWbxm8E6GD7BwkkmyeAkg/9do7JAcZN0TjRLcKKFz4ctkl9wMhPO3HKSSIvhlgzW1/kCWIvi2/QKRZ6yUlpwyQPMCjF5C1jLM+msceEb0LT8MOvk/CV07iJbmVVKbjwOAuAumaxUXAsK4BX0OjF4DxiAr1ar8CloAPyZUa2Xt5zDrNZbYQDWVuwtLRurt656SxV7ARoA/2ZUyvmQlh9Xi7w5DMA76XVK7rkYBMCjGHUK/kNarnWIWaXCj8IAYM2pjCo5H9aGT7DUKLnn4pCW+zFq5PkDKFovsJ1ZIz4C1mL4Ar0+keuOhSy4E6M+ztfA0Cqy4r/ptcncdVFoGwxPZXWcb4ZhocixazPqkrH5AiKLYHgWvS7OZ8PQUfTIMzNqEvz3GpUuMDyYXhPnrWHobvIzej0K3wdDT8WV93vWInj6MSp9YHgeSyXS8/aysp+Y/oBeB+cLAcCkB1QuuMSogfMHl3zQE+91IUB6wHDz4lmB5Fn7SW5+09HQHjA8maUC8+FB/umi0B5o8D6WGmSSzBn/fbJIDzH9DksFFhb+UK0HRI78E0s1WPhkrOwBxYVPpdchyYjNJ6DpAcNl1tKrMB/86dWhPWC4+kb6eDkRBvfcHNoDDa65kT7aVJOFGy4ofWC45ib6dHKEJMkZ34b+hmss0SczapLM3HxyPxgufzrLZLItc4DW4BMGgOGS/2SZVg6SSdLzm0PAcOzPWCYyZkvw9EGgWPVZek4jczAyyeT+YaDAG8iYBDMGy+T8QBDF42b0SQycJJlJ8sBQgOGW57Msm/bMiFOGQ4NL/JwRyyiZ6Xz7CDDYm0hfTnT+/6JjQAX3WkfP5ZEkOeOjMa4YLvYt0ieR2StJBu8r4wAGPGEnPUbKKM6+2UJeB6Or4vLfJkuOMr9hW2aXnGdy83HjAQ3wiCXSM4dKxn/f+4ATnk7vwhbPr0InAFVc4O0HSR8oeN71GgAnbIjoxCQL7wybAtAA1/7CQQ529gqssAbPoHchGYf5bVGZBsSAKzx/P3OYpRNEILrmn/QumeQfLyiCyaoBP0of5vzjIIDhBocjFiV5ysuOhGDKK+xRHCAzeP7xczA8juktWVjuuRIQTFrk2LUZPTLJ4OmrW2B4sjNKKUEeeAhggokbns5ZD5JZ+EsIWg23/A3nN3362mgEkxdd+Xse9uyUmSU/CGuDAbd+xTtec/cLAIrlqLjUKezvfCSaBVC0m2J5Ck56zXe2Z3bK3HNRyCLAmqYxwbJVAM9i6TTjR2GoqDTNit+zLMoZt15CtCaA4nIbWZJkFCd33gaKyhqufR7phSR3f/3KUFTXcKGP7yC59NUnXApQVFiBC9/nkTc5GoAqqiyKeTNFtbVpTDBNAFZQOCBECQAAkCkAnQEqYABgAD5JHotEIqGhGcwG2CgEhLYAXz3q4n/pPNosX+f++nHIna7Op3PmAfqz0uvMz+wHrI+jn0CP6n/kes29Bfy1/3G+Cn9z/R7///WAZ3H+g/F7zxsafEUcXAC9gb+v5biRfRnwQfGfYA/mf9y9Az/o8un077Bf80/r//W7C37aexX+pokXCZ9Nlw9mlO0CBjxFA0zhHNwJNA5Dy1nWX9R5Lw53vvxelZ9Wlo2hL2JpTGOGbHqC82s2y31OMeLLu+pBA0B2GCTqJuM7VkcB6IBgTcx17hONFU63rrofIXLUHWgWdvsng16kPTYvM2GRH4ur8CFVXaNcz8IoFO2NpSLfgNLVnPUZ4qMDpHosxwKDNDlp07sUPlpujjXsZBCSZgtyKEBETl0jAQrB3SNEDpfHERyQDvNzsFgG9ydlHP5Cfxd7TsNUMrbILpyLnuAA/vumK/6HNOnjMeTAqgyzc0xqbxEXqfe23zVVCsNli1CsHNh47eDLaM/MPFtzFBfoOCKLGC1+luF1C+Ycd4/ZHjpVbP5QSAeb2JG+zlg59WAEObXcIwx4zrg2DBxF8mntk4CU682yMzvyKPTZQEpCllqwpKxWqHJr0ou2xNi+n7gZFPXeTT7FpbdAYOERjRHiDJxn0xfiu3ON0TczFqqkjo2ZgEbtKpG1wViIJLytkrQt/FaJqOAMj6jsk5keZLlR/58QBDbnJyk6GjB8W3vueiDUwpQsjVV+H747R/hRxEjEh86ZM3v+D8mB3B/bcmcWFFT0VwUs4il5RWEE1Uh6SSHxtIhMiK9OMzECFlC7usZ0iTfiN76sjE1QuD3oz9tiubiQcHmHjh2TlYju1b2CfVIXXixFR1g2N8kJ3uERyjbs6ZTQEDjqS3G60IQR5OYaMhL7cPMrXKkynok/wHEaE3DSVA/+L2vmUyj31+QAAtgpJFIazNmitpiYU7UWSzd/fq+YZiCfw/8OiVGdQ98nP5D2Om0mIOnOCprh2wsZiVT9JZ2GOChRNmEJsa8eLgef3y9R8beZUI5SaD9N8n2lVBPReFcR5DdSQCg9ZjPMH9FvC017ukAf83r4/jSZuuVVc9P8aFJ1YTCI+rnI9SH9JnuFVnRd86ZGHa0A+8VwB7Y18GxLDDQuXAnCrF56f0CU+KwCSZAUHSm6AOp9lzT87cgMjXmIc58PKeeh2n8y/B8rs5Lbg57TS7RVte141jh8FTKYi6Owr0N1HZan4AdVDT6VGKKu/tKpDrctKPrirGfYB3mhayyz7wMBTRio5ltuGIGIQ/wpcPlBnzTRWKRe/r/0QCvXtKa+05dthkmCXKMTs6s6NQzHpme9rBt/N39RzU40MspMsYPUsNL7/f7QegpKKqr/RWAWbnm68vxNTjLfZXN8loVtFK0idDBMfDS4+ARQgVdE95wFbKr2X/GTzrbNcHfYPgRmN2xjlY3eoJfZgViFwaIYj/junXkuSVH298HlvSAYe8LsuMTkXaPib0yuTCiLdmgtSIlu6zKKcENU36Orj+aZ252G/jGUOCQhHrgqoVnbvwxE3XqTTS/yU046KutsWY5X85Xe0hsyMwvYmR/aURfg6H+/x068UoZAHtd2X8UGvEMM5e+vMbErK5i3aSlwMkSdL0NgrzCdCipRzue/EaePWK76kNkkffy+wWis4SJzyLJATyv/Dz1kmvV5t0r/SYzbrwp0sVo0dmCHujWx6MESqRJaCS8PMPkGPtPFz0K8R+mA0K5gqkhzf38IXAlYhCiO6jqRZgW0vLpEWzzz93Z42uCUehL4Xt/TSxcMdD31xx23+UOlnbPyoj3mzd19ax5+24zzprRvgd/kpTHyO/M0sf5zAKkg+E7hXTd6AKrdfaJ8LU5+XeljN7nT7mk7fe1a0+lvLbwATRayiureRFSwWFOIW7QONTLMv3FNrn/x3DruSzu48UI9OcaG3+1+JGwFjDKRqWVicC+lf3vsaVLi6u5JV79+uL/TPdyMog3CjY0h/tf5ux2mqb4X4x7k1pQ29/19B92aSx4fVkcvS924uNiTDLKq8jHSJLaNMddv6SqFSxDol5+R+uhi1LHEOc6p91HkJt+XLjsMqVzAn6DLrjKlVnzCM318RSDxVTrmNy2zzekx6bIKb0j2bKBV4Fs/6/NDUKuaYyCcIDx1r/xb8TPoh3fY8nbunP4fqA53qRk/cQQfthM6f8uf8g4CMLldGdn5JHBDnpA8e+9mB48tbn0fhL7kVzWiAJy82Mnc5E+VO/gngFKl7EMWQ+lxTbt/i/o8/DW/S1v/9ZKdC8MJ/Rnd+ENO76SrHxMBzRCVrBkVpWvbhM4aalXh2Bk1K4vnYv1gED+0VtGIS55Yb39MuNWu4AbLEmFO6hbKuiw854pQvPX/FuKay3A74T2+hjdl1HyuVIA9VBx9/Suil0/8T4IqrygBoF7AQyhnGGfTWirN3iI++EgoIORxgRq1vQ7ugkNfaAv2NNO+wy2ZWrH6OL/FglYmh9k5gKWNv9VdrVZgV3FeNX84Nj/GTAyomxi3nC4UKWujfdAPwOEa2/4ppE9IoJIzx1veGOLUpNiWfUalulDg0QErmhCqCXCyrZgs2mC9CiKP7NctPBu2ei6RD4IzDn3YIegjv6HdnUWP4+IQVMqeWAUy3z1af1Kw+CPl3U5bFuDv5Bt8clp68tCqjytUWXsKEGkgjAmmWJdDeOONSGy6V/Wh2qiyNt64GVMoJHUHzArs9a8OI/ihMhviAAwyrapccpdMhTAAACX9KkpPO/3JrRtVnWFD5/no+WfYknwo2QyGURyUQobJoZqvb7Iq287YUa9A4Sp2hbZ0Fv0l4EWP10a7a933p/VaDKvUyR9mibV4e97sBIDw3dAw0prm3yLto62yvICOk+RZ06igIY7ZE77D2+vlrHmjnsVPku08SZLaDvh20ytQcyS2GWXykAnYZbLRO0yVHthXNia7f8N/1apiVY10v90LWnwjFYFNK9H12fEj3Sl18V31M9LicRxQ9Gddb5pf/auVi2rt4523/HHLY8h6cJ8hhPr8qCj50roQiebuNFXSixziUIdXC++aEg722OH11yW6N5646tde9SdxzrAwwPBDLUea57s66/OklTZXYYyuEiUg67No0smSVDIufr4iAjqmxspY0agAAAA=", + "education": "data:image/webp;base64,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", + "family": "data:image/webp;base64,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", + "marriage": "data:image/webp;base64,UklGRvwOAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSC4FAAAB8HZrkyFt27ZPaI2IOmzbtm3btm3bPk7btm3b9nnYNrfOyFg/VHV1dVVGxARguFpVioKrAIBog4gUR4E1jjxyLcAANQBiUhTFbj+fIUc/3xVmgC29KAApiOJskimRvBBY9W3/fu6Jn58MSDEMOzJSkOHBM499gY3fnEekECKz/JHOxgiSKUf4iF9VLYRhB2aOjxxsHPEwWBkqXB+pRcsU34OWwfAxdhJ8dAFIERRfo3fzzBK9ElWrq8hEhg8ydfP0Yr0RM0Vbq1RaVbi8m8y/zYJ+mgHAXMtuvv+RRx6y68ZLGgCYtFBsw+gi8d2wHogBmGenm35w18tsfv5vnz57LQAmTVrZLH9m7iBz2z4YgK3f8wDrkT3nCJIc/ebsJQETiCmA2d7JNJnze1BMWxWzHv0rkpE8B5sjeyL5zJtWBCoAK53+qftmOHnkFzbUqRlw0F/J8ODE4U4+e828mP+E77/Mbp03ASbTqbDil0nP7Dic/Nsb7yfpnrtgPPaWZQCdgigOf5KeOcVwku7B7p+9ajZYZyK4iUycck6ZU4xE/mFzqHQjYh+nBwceztfPh0gXovo5puDwM/leg3RgeDdHLGIkfmlWkYkMZzOx4+gbOeJXKpEJDFsmj66GmPgJmLQSnfs/zBxgkNEJR7wOVSvDbUwsaaTYA9pCZc1XPYrCzAcXUm2BT9NZ2MT3w8YY1p6JKA3dN4eNeycT+x3RVbTiD6FNWOhJRs+Yu2qfuR206Rg6+x59cH5p3CcjlYjx2mrQ2jz3Mfeun86rUNW2SMFC/U5EAJzFXKbgzMpQAO+jl4nOo1AB+BlzoRLfCgPwH0ahnN+DAniqJzGAzP/NDgGe60mfY0zw6cXK1HZmJSjwTMG4Vu1/xfsVc7Fizdrn6EOJmFLw5WVrlw9n6pmPLAgBdo4olPPvlQBY5DFGqb4OA4Cvh5cp8camk1mozD2blnyRUYhoF3x+UQgAw2eZCjGhxzehaNgmcimiHU9EVYPiu/ReRA9aB19YSrXBsFX0ItjvxPcATTB8jN6Dvgfv+/DG0AaRRR6JXJr662fCajDsyxTFiRTcD1ZDhZs5Kg7p8f+5RGpi+BxTeZi5K6wG0dm+xlF5UpyFqgGCWb/MFKXJPGQcVOxdpBcg2gRfWxE6BiI4/WWmXBKP70LRUgzr/5xMuaPIPQm2DOeOsDaAQU+7j8weE0VOZI5etHZ+AYoJFVjwsntIevIcYz05ydG/SO9X5l3z2ESAAXMf+rmnOKn/9db1ZrnMmaJPEY/uC5sMYgAW3+8N37v7hVx7/fHfffyM9QQAdvgH6TGFmIRBngqbDBBTAJh9ifU322LTTVaeH/VKxTDXtc+RKXfWYc6xO7QDAGKVoqWZom7Ayu9+mcyeox/MfHIJ6aYuoo0iGC8VsNJNd5GkJ/ecp8bEd6LnasCc+77nP8GeRry4Us8ANQCzr3P0rZ/6wV//+uLUmHl57wAxQ/NcDzJPLX40AACiVplh3WBMK/jYMBoVK742PTIGJGJ/y3l6HBAM53OmKXJXwaeGJDrHL5jc3RM7z/zdkCBY5FtsfiB15HzToCDATrd89jNvP2vzxR5i7iRGmw0LIhj7HqYunN/D4K1ezSIbRI4OErcZ3ljFBziaLPG90FKILnAX0yQj/n4ulVJAsc5jHEWbnPiHRSEop2Ldf5MpRy3cyS8tBEVJDQu/c4ZkTokk7zoJUJRVgbXv+MdrJPn0d0+YByIorRhQrbr/ccdutQgAQ4nV0KwmKLRoVVUmaAlWUDggqAkAAPAoAJ0BKmAAYAA+TSCMRCKiIReJ5vAoBMS2AGaCAEAz6DzMbA/fvIr1DRTe53+N61f8n6l/MF52fmc/Zb1av+R6tv7L6hv9a6jP0LfLh9jX+x/8/9zvau6gDhO/6b9Ffkr/tPCvzMcXuJB9N4DzBrLPez6216NxoNAT+Xf2b/sfcB8nOfp6b9gn+bf1X/q+uB1GX7COeGJjVDUg3rWVKaavmwdPxkwWjTomOr5Uy56QW150AJoYrpYxPcWZFIT8DHv/616ufYOoeJ8YbA4SeHryuhOtmfJrVZA6rnCkphvLoGMQ4Qm08g1cWi52MYC+Vk31NQTE5UKlydOHc4kGu9m7Q7UhyAJyCjl87o4khQP/nfAgWwPx8sQr+OA16kJ319FC9GHovhCrKraZRQd6php+F0Nt2egkwVlPgCDbX61HvoztD/kyoEx+Kpbzj+WEAAD+/Qbsv/A+vESv/L/LCxGREMmQsxvpHJ3+iyjTcXn3kgH0FO4ePJr8s9JLGxso46oL3JdOBS6gI+ET7k6NHULK9FUMyohUMeXyED2gUp9FvWceQ8BVUXaJQic38YrMH7m4PH0ZCuaqAPIsUvlrve4wW/Qyr/oePzq86YEONihU+UFzTaUGujUpir4YHKNvTspkIEV8zTVnx87qfT5SDRtkVl7Uoao0Hqnv7VS/ePk+ysDKOXLfoD7J6NeTDuFodqJEkZWgPPB/7CGhhUwn15laRPwGBGTAzd2ymdGRdoyjxRj/hU+kURnIyZ+Wi1PesD2LLDj0ZbpnBuTA5+yKttaFv1vMBD5f4YvXYf/p2zwr5jXaqpc3svgjlpUpW/PTmWXl4W/g6Z6HxwK08ioQB7C0BNM3KUB+gf83YJa0/8hs+VCETlBbGPfCFFy8JtkrcANHlH9hOYqUrROEslMPPQqNoP+GL0JEr3CczsOZ7siw3R2ZiZWGnoEmJ67uyITNUe/gGSHZSOcF5FfSsd7fAmRfp8A3vSp4mbwi6WG+F1auY8IXUGkYzSKpVu/PxofEo8CHdwkb3ZxyOYIfFjHgOIFY/0hieJnX2/e1m3BGaImXmtF5RIB0Fw/9Vt8kDwwETujV58dTMz1LsJ8k+ZZSAayA8wxGw2yu9Q4ksY8XmtCIVE+AatklFHzmA+YghoDtvmjyDL3ov5ya/1QEgHJgKdzCi+HGqPLqZN6jbcCpXDK56C+sR+vIONXgbyhPWf+ozEP2MhQfQMChuHtQaiiFbnyc0FeK/eHVC1fpHR96oFpYdPEjoWwaSi5gvYujjpD0NfYWANLcSHWsl3lWuqjovwysrkJXakLzYsu4uu5AsSUt/m28BJ99pxhESbkWIealbgv3b4+7/ytVNE24N/vnwna7gSOxceZG01Sqvsap4u0CWda/gjbq3AbuGPSFbApauQ2QmIOAfc2Hxt+5rpQqDb54/wYzcztWW8AyV3GSsBecwQdxUpgM1PX26yToSGS4wz3WQrr5+w9cTrqO+5y3tEfYcmRd1RNcIO9nrte8jJijYlLF2ij58MHvuSl0NwBeN8jE0DaYqsrL2oW3zjj2tWbRPEi+IPGGnoLwwqKZ6/L3tU2+YL7b2C1NoFHg2X2TSLLRLK6e07QxoPjhYzwT336eF285XpKU6xwUaMSHlt7cbvQTC/fhk/GHuC5IX7cG/LMeh4OJZIAC2r2sLTFGSMzvY+7jgsfm//bWm+74kFFj7TD3i8OSGn5aleFeHIZ22DKsxmfkVtKxKZi3dR9+MxxkFxAkQJfO0sV3EPa3gih0xgO0l1z67G2OUiFLKklr8v70XpSBidEKn0O70SsSPp4O28689BUJVzxNg0+i6ifxc0I3OgPc5e//SURvbd0ZRge+MUPODCQHNy0jdnE5WGzv6Qzv8SesBRBBlPXtXTApEWWit1jgvOpi9ujhxseBf4zFazNr23g+q6wMVx/qlzDpzI1FBFjY3JL6ysZTy39oiAPtFJ6grlo4ial9jfkLvY2d8UtDnkb3cYCfkXZJMePeRRMxkr5epyl1mC8BVj5epL2J/+7qnFuDE+rh23xmaJYTP50GHfzOpFhC04yHtjdVOs8c7c4Am6bRt0KpYHu9uuuPy8Va43EaNLBEtkpwjDiVjRpeqTJIH+aRtmGG7VrNG1zWvro/zg7x2T5bGVRBbdw1KCr9jR1D75Qmjf8cKvNYqIDw9LJv9tjAAvDB0TCizoaJfrOV+rxPX3TakySRlAfSctc/hq1uCNJgCO4+Ocy518cNxSkJdwHLSu+yl5YXDR5Nql+lrsycgABKlQSLaoIaQH0pcRsv25QM+iFezVV3JHX46F9XWdQ1rMnCc4EIrCmW8Hu7XHaDIpDncoecF3+uX6fa2w81sabp8M6KPhYG+t19uJg+Ne0KECgR68NJqXHr/LgySwQnKmNeBNw0nF215l+3/+RbJSNkkvE2s1uybEC9v0qoTCHnKGpa1nemhC3xu3sVyNcM9ZE+dYQX+f1yxeW03X5thWxqKrgnDgwFA3F/h57NcLisZQOyVH5jVmwcWmsA4+f5nFv+mbwtDQiGZqlC5wj+wvJ3Ys2e+vSC6RYOqcFd5DVPp4+dk8QhrQyBV3laf/t9ZjDmuXfiKCVeXc1qsH7op3oMldied4VYBdojnO3dzq/oygM6caXEOUjB/uI6DHzPUn8svA746Ld827RLzrLyrsQTZ5PxDzssroSJn7bcDxRCDkt4FpBWOKjWyPRqFhQggc1Aaff0MM4GzMtLrBX2HFIh20a3Fex2+Upw9+IHK/V7TJ38ZPf/OrWOqvlnv3981ayJBzCHrN6UgR8/Xp8bbdF2Mvf8X+ksn9FOT2njzZfldZyShIxCQcoogdB8NLxaC+eIySZKRlmv8N7jNj+RcWlWBtdQjUAebCBghD3NcSQ5v/2U8d9Miqfmi6Tx4X76/tIL+Rze44WHEkZ3fIOrUgWHuvFFgUPpFJICZRO1ARBARqcHzkOObhLWrj0zuCZmrnYU4gAAAGluGb4601ED/d++RM5xpq/4gxTsTuxdHmqbuLToWzUbz9+jX0uY6O6Mn0/RZYTfOEdUF4f5rZ6hdYZQrD6yUBUcL7faRyE5fUO1SeGfqW29s1sqha5xb528ujwsJzMbBFa1l4mcFMEFMuOl+vanAdhRLeiIbOFcNA3jZC93nQ3PBUWNCg3/rHAgPH7nvAj19lSiw1NB45tMKaYpOblkOnbEeVh7/aHrrMRVAVKBSBRgLFjwMfZCFiWeYujzNYpeR852vpQeItSsTw9Lh520A7OOhhICwfs1FWFJYUYmO7ULytvGrAAAAA==", + "family_marital": "data:image/webp;base64,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", + "lifestyle": "data:image/webp;base64,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", + "criteria": "data:image/webp;base64,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", + "verification": "data:image/webp;base64,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", + "personality": "data:image/webp;base64,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", + "glasser": "data:image/webp;base64,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", + "details": "data:image/webp;base64,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", + "checklist": "data:image/webp;base64,UklGRv4FAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSK4BAAABoHPb1rE92+dcsW3btp1Utu3WrtKlZpVOv8Ds7aSybVzXfe04uc99x983RkRMAP7bV1z6KhkSJzDpJCsKoFqXIRMmpjqud4OCgMuGosiCI/eZ/vOz62tBs6BoeZJkTELKCcn7Y+DsKZrepU8i04+JZxwLtSZa5Bg9rQY+ayBqzGEuPe16boUzpno0BkMx3qsAMaWo+YzRECMHwxnryUjLIS5CAVMOI5mY8lxnbtR/Dq3NvSU5Fzj/r78FuTdHimi+LQcgml/km0OL2wCSYyT9/sbQHAsh8mIpkfwi6TkYLtdCnIEC+cYFuTf/t9+CnEs4CS7nhuYcQ2uomdFZSHilBMSIYiCjvRB3wcFM68AMcKQdQelrjNaSeLm4iBU47IzemudkOBjqQx9tee5WFTtQbOO7aCnwRnURGBYtfpTRJzHdr/G81wEK04ISGz3TjsnnoueNjnAwLkC7DUdvPXj85vWb7/2KDJGMgTxSFw7mRQGULl+lUYOG37lRvaFnyBDIF6sKwiGLWgBpF1t+hXy9vRWgyKqI6PcXB5QdNaMx4AQ5LQ4AVJHj4pzifw4BVlA4ICoEAAAwFgCdASpgAGAAPlEkj0WjoiURygYAUAUEsQBonBNoZatxrX88Cvbv+Yr9iPWo9F/oAf2v+k9Yz6AHlt+yF+4fpCNHyAvq8ppnkP1B+ZA9hT9VWhO+M4xukIwpSiWEXRgVSg8stWid/nTUHayZQNHuaqfmdYr440NnWc+3iERfOXr/9u3UPmSHdSbIRSizNEpW4SSE+lrBNKfcuQZNPJxNgbQC8JgZOQnsXeGI4CuzopoISdhwgAgcEYAA/v6lN8///lVWWRe7Ipqy/NR/rj4CzdOl/iAPY48aGxDyAo+TgPkf+QS6ws5LsU2oId1r9k/yhF3vu9cN1bjrnfsiIQA4bGB2PnVSMXxeGc1P/+oP7GEffV+Aj2yTcLwCq6x0NUVQOXTpTVSS2CKr1zzIpBTLRO/KafDijW9v44PXmz+ehu2Iw3TRfhQOU/3Q2kqN297LuN034cjyHPBvaUvFMOhb4IW8tFVxLQGKWp57jNKjelFEh2u7v0gg38lmJZ0KH8tK9Lx/5ie1aWsOEmwQnsKqfUEWl4i6gV59oldlgLyYnv/xAk1fb8Ayr2xyYE6HGIzSyFQeln4v8tp05GI+2ya/c3X9qj6iil0Ltf4iz1jQX/nVT7rkDwxnuA/j2W6J7yR8mwWUP+Y+gKKVaZU5PrnVNBoaMN5FCvl0Ao+rTPi+E1YgDu+dQ5fEO8eAbJMiiQcehiBTATjTeal4xpseeLKy12O+ksurjn0aL+UZoZF6kbs/JAYB4bX4JcvlEurACnYfPqu/fJgG3dHXwhic52nwgwdER4wFdRqn3dhHL4zVH/LoWUIZhG5ixOYMmLRBHLWjRah6sVYwuWRmi6P+OMO55XRf6jHWB99SBvVu8zAN9REEUWmwaJI5NrG4iPZlep9zI04rXnugs9ygYuixPzUSd2l7VpAQI+lP5AefBPuB+gcY/SmGD6YOPv4DqWon1Q2OyU2QCkkpj6padpAmtD12Wp9T3V9Ye7BfG486fw01NUvIcgtCySIJy+pDSalgfLDr+d30B509VEv6cxog3mzPM/XBjPEY0vPsDeSAeLMf8g72/LfouPJubJftF4TxtTgHeCu9YQUUnPHBmbQNWigk7HpNpJRIRPIM8VnzT2bshMqWIxiFi8bBOVNUdzqYA3y88Y77w3NGvzcleXypIX+91FcIfnCRweOJEUJVbC5HEN1BJZkktu9+CXjGzFwf6A6m9TXjn7266JLtt2faLMFZUk/W3IKePlliVyc1j3WFJRMKt8Q7pt01Jc4Mz5ODGdDR8Nfh/8sr1VK0ES1Ju+DhGr3PZK1BXnGUnwc0kLW5x9/nF0IWE7vGVuNo6YSf3sHpSkaHiV6J4bMkL5Jpf3yYVsqjJzkeRq6K+XhV/vBWn7aankf7lU6cB50ZbcvWFj2KWEWAQAAAAAAA" +}; + +export function SectionIcon({ + name, + className = "size-[30px]", + ...props +}: { + name: SectionIconName; + className?: string; +} & SVGProps) { + const dataUri = SECTION_ICON_DATA_URIS[name]; + if (!dataUri) return null; + + return ( + + ); +} + +export default SectionIcon; diff --git a/src/components/Componentes/slider-page.test.tsx b/src/components/Componentes/slider-page.test.tsx index b0f678c..3077a8c 100644 --- a/src/components/Componentes/slider-page.test.tsx +++ b/src/components/Componentes/slider-page.test.tsx @@ -32,6 +32,7 @@ vi.mock('@/hooks/marriage/query-keys', () => ({ vi.mock('next/navigation', () => ({ useRouter: () => ({ replace: mockReplace, + prefetch: vi.fn(), back: vi.fn(), }), })); diff --git a/src/components/Componentes/slider-page.tsx b/src/components/Componentes/slider-page.tsx index 7cc1677..60b30c0 100644 --- a/src/components/Componentes/slider-page.tsx +++ b/src/components/Componentes/slider-page.tsx @@ -1,7 +1,6 @@ "use client"; import { useRouter } from "next/navigation"; -import type { TouchEvent } from "react"; import { useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic"; @@ -21,10 +20,13 @@ import { SliderSlideThree } from "./slider-slide-three"; import { SliderSlideTwo } from "./slider-slide-two"; const FINAL_SLIDE_COUNT = 5; -const SWIPE_THRESHOLD = 40; const SLIDER_ANSWERS_STORAGE_KEY = "marriage-slider-answers"; -export default function SliderPage() { +export type SliderPageProps = { + onClose?: () => void; +}; + +export default function SliderPage({ onClose }: SliderPageProps = {}) { const router = useRouter(); const { locale } = useI18n(); const [activeSlide, setActiveSlide] = useState(0); @@ -32,7 +34,6 @@ export default function SliderPage() { const [selectedRegistration, setSelectedRegistration] = useState("self"); const [hasReadRules, setHasReadRules] = useState(false); - const [touchStartX, setTouchStartX] = useState(null); const updateProfileBasicMutation = useUpdateMarriageProfileBasicMutation(); const queryClient = useQueryClient(); const [submitError, setSubmitError] = useState(null); @@ -109,42 +110,17 @@ export default function SliderPage() { window.localStorage.removeItem(SLIDER_ANSWERS_STORAGE_KEY); } - router.replace(localizePath(getSubmitPath(freshProfile), locale)); + const targetPath = getSubmitPath(freshProfile); + const localizedTarget = localizePath(targetPath, locale); + router.prefetch?.(localizedTarget); + router.replace(localizedTarget); } catch (error) { console.error("Failed to complete onboarding:", error); setSubmitError(t["Failed to update profile basic details. Please try again."] || "Failed to update profile basic details. Please try again."); - } finally { setIsSubmitting(false); } }; - const handleTouchStart = (event: TouchEvent) => { - setTouchStartX(event.touches[0]?.clientX ?? null); - }; - - const handleTouchEnd = (event: TouchEvent) => { - if (touchStartX === null) { - return; - } - - const touchEndX = event.changedTouches[0]?.clientX ?? touchStartX; - const deltaX = touchStartX - touchEndX; - - if (Math.abs(deltaX) >= SWIPE_THRESHOLD) { - if (deltaX > 0) { - if (activeSlide === 0 && !hasReadRules) { - setTouchStartX(null); - return; - } - goToNextSlide(); - } else { - goToPreviousSlide(); - } - } - - setTouchStartX(null); - }; - return (
@@ -153,6 +129,10 @@ export default function SliderPage() { icon="back" iconLabel="Close slider" onClick={() => { + if (activeSlide > 0) { + goToPreviousSlide(); + return; + } if (typeof window !== "undefined") { try { window.sessionStorage.setItem("skip_intro_redirect", "true"); @@ -160,7 +140,11 @@ export default function SliderPage() { console.warn("sessionStorage is not accessible:", e); } } - router.back(); + if (onClose) { + onClose(); + } else { + router.back(); + } }} />
-
+
); @@ -285,6 +266,7 @@ function SliderFinalActions({ onBack, onFinish, }: SliderFinalActionsProps) { + const { dictionary: t } = useI18n(); return (
); diff --git a/src/components/Componentes/slider-slide-two.tsx b/src/components/Componentes/slider-slide-two.tsx index 5b117f5..9d08344 100644 --- a/src/components/Componentes/slider-slide-two.tsx +++ b/src/components/Componentes/slider-slide-two.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import { useState } from "react"; import { useMarriageConfigQuery } from "@/hooks/marriage/use-marriage-config"; +import { useI18n } from "@/translations/provider"; import { SliderHeader } from "./slider-header"; import type { SliderSlideProps } from "./slider-slide"; import VideoPlayer from "./video-player"; @@ -11,6 +12,7 @@ import NetworkImage from "./network-image"; export function SliderSlideTwo({ index }: SliderSlideProps) { const [isPlayerOpen, setIsPlayerOpen] = useState(false); const { data: config } = useMarriageConfigQuery(); + const { dictionary: t } = useI18n(); const thumbnailSrc = config?.video_step_2_thumbnail_url || @@ -24,13 +26,16 @@ export function SliderSlideTwo({ index }: SliderSlideProps) { aria-label={`Slide ${index + 1}`} className="flex h-full min-h-0 w-full shrink-0 flex-col" > -
- +
+
+ +
+
setIsPlayerOpen(true)} > -
+
-
+ +
Dr. Hasti Masoudi

- Dr. Hasti Masoudi + {t["Dr. Hasti Masoudi"] || "Dr. Hasti Masoudi"}

-

- We have come together with the goal of creating a secure and - confidential path for "permanent marriage" among Muslims +

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

diff --git a/src/components/Componentes/ui-config.test.tsx b/src/components/Componentes/ui-config.test.tsx index 0ba597f..ff72739 100644 --- a/src/components/Componentes/ui-config.test.tsx +++ b/src/components/Componentes/ui-config.test.tsx @@ -78,6 +78,49 @@ describe("UI Config based behavior", () => { expect(screen.queryByText("خودکار")).toBeNull(); }); + it("should auto-detect and display city and country when ui_config.enable_geoip is true", async () => { + const qWithGeo = { + id: "q_residence", + title: "Current Residence", + type: "birthplace", + order: 1, + required: true, + isVisible: true, + description: "", + tooltip: "", + extras: {}, + options: [], + ui_config: { enable_geoip: true }, + } as any; + + const { setStoredUserGeoRegion } = await import("@/lib/geo-region"); + const { marriageQueryKeys } = await import("@/hooks/marriage/query-keys"); + setStoredUserGeoRegion({ + city: "Tehran", + country: "Iran", + countryCode: "IR", + phoneCode: "+98", + }); + + queryClient.setQueryData(marriageQueryKeys.sectionData("test-geoip"), { + status: "success", + data: [], + version: 1, + }); + + render( + + + + + , + ); + + await waitFor(() => { + expect(screen.getByText(/Tehran.*(Iran|ایران)/i)).toBeDefined(); + }); + }); + it("should trigger currency behavior only when ui_config.currency_enabled is true", () => { // Title is random, but currency_enabled is true const qWithCurrency = { diff --git a/src/data/countries.ts b/src/data/countries.ts index c429779..fa74570 100644 --- a/src/data/countries.ts +++ b/src/data/countries.ts @@ -399,3 +399,52 @@ export function getCountryList(locale: string): string[] { } return COUNTRIES_EN; } + +export function resolveCountryName( + countryOrCode: string | undefined | null, + locale: string = "fa", +): string { + if (!countryOrCode) return ""; + const trimmed = countryOrCode.trim(); + if (!trimmed) return ""; + + const isFa = String(locale || "en").toLowerCase().startsWith("fa"); + + // 1. If 2-letter ISO code (e.g. "DE", "IR", "US", "TJ") + if (trimmed.length === 2 && /^[a-zA-Z]{2}$/.test(trimmed)) { + try { + const displayNames = new Intl.DisplayNames([isFa ? "fa" : "en"], { + type: "region", + }); + const name = displayNames.of(trimmed.toUpperCase()); + if (name) return name; + } catch {} + } + + // 2. If it matches an English country name in COUNTRIES_EN + const enIndex = COUNTRIES_EN.findIndex( + (c) => c.toLowerCase() === trimmed.toLowerCase(), + ); + if (enIndex !== -1) { + return isFa ? COUNTRIES_FA[enIndex] || COUNTRIES_EN[enIndex] : COUNTRIES_EN[enIndex]; + } + + // 3. If it matches a Persian country name in COUNTRIES_FA + const faIndex = COUNTRIES_FA.findIndex((c) => c === trimmed); + if (faIndex !== -1) { + return isFa ? COUNTRIES_FA[faIndex] : COUNTRIES_EN[faIndex] || COUNTRIES_FA[faIndex]; + } + + // 4. Try Intl.DisplayNames fallback if 2-3 chars + if (trimmed.length <= 3) { + try { + const displayNames = new Intl.DisplayNames([isFa ? "fa" : "en"], { + type: "region", + }); + const name = displayNames.of(trimmed.toUpperCase()); + if (name) return name; + } catch {} + } + + return trimmed; +} diff --git a/src/data/languages.ts b/src/data/languages.ts index 3610934..6765184 100644 --- a/src/data/languages.ts +++ b/src/data/languages.ts @@ -153,6 +153,85 @@ export const LANGUAGES_FA = [ "سایر", ]; +export const LANGUAGE_EN_TO_FA: Record = { + Afrikaans: "آفریکانس", + Albanian: "آلبانیایی", + Amharic: "امهری", + Arabic: "عربی", + Armenian: "ارمنی", + Azerbaijani: "آذربایجانی", + Balochi: "بلوچی", + Bengali: "بنگالی", + Bosnian: "بوسنیایی", + Bulgarian: "بلغاری", + Burmese: "میانماری (برمه‌ای)", + Catalan: "کاتالان", + Chinese: "چینی", + Croatian: "کرواتی", + Czech: "چکی", + Danish: "دانمارکی", + Dutch: "هلندی", + English: "انگلیسی", + Estonian: "استونیایی", + Finnish: "فنلاندی", + French: "فرانسوی", + Georgian: "گرجی", + German: "آلمانی", + Greek: "یونانی", + Gujarati: "گجراتی", + Hausa: "هوسا", + Hebrew: "عبری", + Hindi: "هندی", + Hungarian: "مجاری", + Icelandic: "ایسلندی", + Indonesian: "اندونزیایی", + Irish: "ایرلندی", + Italian: "ایتالیایی", + Japanese: "ژاپنی", + Kashmiri: "کشمیری", + Kazakh: "قزاقی", + Khmer: "خمری", + Korean: "کره‌ای", + Kurdish: "کردی", + Kyrgyz: "قرقیزی", + Latvian: "لتونیایی", + Lithuanian: "لیتوانیایی", + Macedonian: "مقدونیه‌ای", + Malay: "مالایی", + Maltese: "مالتی", + Mongolian: "مغولی", + Nepali: "نپالی", + Norwegian: "نروژی", + Pashto: "پشتو", + Persian: "فارسی", + "Persian (Farsi)": "فارسی", + Polish: "لهستانی", + Portuguese: "پرتغالی", + Punjabi: "پنجابی", + Romanian: "رومانیایی", + Russian: "روسی", + Serbian: "صربی", + Sinhala: "سینهالی", + Slovak: "اسلواکی", + Slovenian: "اسلوونیایی", + Somali: "سومالیایی", + Spanish: "اسپانیایی", + Swahili: "سواحیلی", + Swedish: "سوئدی", + Tajik: "تاجیکی", + Tamil: "تامیلی", + Telugu: "تلوگو", + Thai: "تایلندی", + Turkish: "ترکی", + Turkmen: "ترکمنی", + Ukrainian: "اوکراینی", + Urdu: "اردو", + "Urdu Roman (Latin)": "اردو (لاتین)", + Uzbek: "ازبکی", + Vietnamese: "ویتنامی", + Other: "سایر", +}; + export function getLanguageList(locale: string): string[] { const normalized = String(locale || "en").toLowerCase(); if (normalized === "fa" || normalized === "fa-ir") { diff --git a/src/hooks/marriage/use-profile-main.test.ts b/src/hooks/marriage/use-profile-main.test.ts index 0652a93..cd09895 100644 --- a/src/hooks/marriage/use-profile-main.test.ts +++ b/src/hooks/marriage/use-profile-main.test.ts @@ -32,7 +32,7 @@ describe("getMarriageProfile", () => { expect(mocks.setCachedEntryPath).toHaveBeenCalledWith("/questions-list"); }); - it("clears the route for an incomplete onboarding profile", async () => { + it("caches the intro route for an incomplete onboarding profile", async () => { mocks.get.mockResolvedValue({ data: { status: "pending_onboarding", @@ -43,7 +43,7 @@ describe("getMarriageProfile", () => { await getMarriageProfile(); - expect(mocks.setCachedEntryPath).toHaveBeenCalledWith(null); + expect(mocks.setCachedEntryPath).toHaveBeenCalledWith("/intro"); }); it("overwrites the old questions route when the backend advances state", async () => { diff --git a/src/hooks/marriage/use-profile-main.ts b/src/hooks/marriage/use-profile-main.ts index de7e027..3151832 100644 --- a/src/hooks/marriage/use-profile-main.ts +++ b/src/hooks/marriage/use-profile-main.ts @@ -31,7 +31,7 @@ export async function getMarriageProfile() { }); setCachedMarriageEntryPath( - hasCompletedMarriageProfileBasics(data) ? getSubmitPath(data) : null, + hasCompletedMarriageProfileBasics(data) ? getSubmitPath(data) : "/intro", ); return data; diff --git a/src/lib/entry-route-cache.test.ts b/src/lib/entry-route-cache.test.ts index 707a4e6..143e75a 100644 --- a/src/lib/entry-route-cache.test.ts +++ b/src/lib/entry-route-cache.test.ts @@ -21,7 +21,8 @@ describe("entry route cache", () => { ); expect(normalizeCachedMarriageEntryPath("https://example.com")).toBeNull(); expect(normalizeCachedMarriageEntryPath("//example.com")).toBeNull(); - expect(normalizeCachedMarriageEntryPath("/intro")).toBeNull(); + expect(normalizeCachedMarriageEntryPath("/intro")).toBe("/intro"); + expect(normalizeCachedMarriageEntryPath("/invalid-path")).toBeNull(); }); it("requires a real token before trusting the cached route", () => { diff --git a/src/lib/entry-route-cache.ts b/src/lib/entry-route-cache.ts index 3e2ceca..9a688ef 100644 --- a/src/lib/entry-route-cache.ts +++ b/src/lib/entry-route-cache.ts @@ -7,6 +7,7 @@ import { export const MARRIAGE_ENTRY_PATH_COOKIE = "HABIB_MARRIAGE_ENTRY_PATH"; const AUTHENTICATED_ENTRY_PATHS = new Set([ + "/intro", "/terms", "/questions-list", "/finding-match", diff --git a/src/lib/geo-region.ts b/src/lib/geo-region.ts new file mode 100644 index 0000000..7948f3f --- /dev/null +++ b/src/lib/geo-region.ts @@ -0,0 +1,236 @@ +"use client"; + +import { PhoneNumberUtil } from "google-libphonenumber"; +import { http } from "./http"; +import { resolveCountryName } from "@/data/countries"; + +export type UserGeoRegion = { + ip?: string; + city?: string; + country?: string; + countryCode?: string; // e.g. "IR", "US", "GB" + phoneCode?: string; // e.g. "+98", "+1", "+44" +}; + +const phoneUtil = PhoneNumberUtil.getInstance(); + +const STORAGE_KEY = "user_geo_region"; +const PHONE_STORAGE_KEY = "geoIPPhoneCode"; + +let cachedRegion: UserGeoRegion | null = null; +let geoRegionPromise: Promise | null = null; +const listeners = new Set<(region: UserGeoRegion) => void>(); + +export function resetUserGeoRegionForTesting() { + cachedRegion = null; + geoRegionPromise = null; + listeners.clear(); +} + +export function subscribeToUserGeoRegion( + fn: (region: UserGeoRegion) => void, +): () => void { + listeners.add(fn); + return () => { + listeners.delete(fn); + }; +} + +export function getStoredUserGeoRegion(): UserGeoRegion | null { + if (cachedRegion) return cachedRegion; + if (typeof window !== "undefined") { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored) as UserGeoRegion; + if (parsed && (parsed.country || parsed.phoneCode)) { + cachedRegion = parsed; + return parsed; + } + } + } catch {} + } + return null; +} + +export function setStoredUserGeoRegion(region: UserGeoRegion) { + cachedRegion = region; + if (typeof window !== "undefined") { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(region)); + if (region.phoneCode) { + localStorage.setItem(PHONE_STORAGE_KEY, region.phoneCode); + } + } catch {} + } + listeners.forEach((fn) => fn(region)); +} + +function resolvePhoneCodeFromCountryCode(isoCode?: string): string | undefined { + if (!isoCode) return undefined; + try { + const callingCode = phoneUtil.getCountryCodeForRegion( + isoCode.toUpperCase(), + ); + if (callingCode) { + return `+${callingCode}`; + } + } catch {} + return undefined; +} + +export function getUserGeoRegion(force = false): Promise { + if (!force) { + const existing = getStoredUserGeoRegion(); + if (existing && (existing.city || existing.country)) { + return Promise.resolve(existing); + } + } else { + cachedRegion = null; + geoRegionPromise = null; + } + + if (geoRegionPromise) { + return geoRegionPromise; + } + + geoRegionPromise = (async () => { + try { + // 1. Primary: Habib Backend User Region API (/account/auth/user/region/) + try { + console.log("[GEO_AUTO_LOG] 🚀 Requesting /account/auth/user/region/ from backend..."); + const response = await http.get<{ + ip?: string; + country?: string; + country_code?: string; + city?: string; + }>("/account/auth/user/region/", { + timeout: 4000, + }); + + const data = response.data; + console.log("[GEO_AUTO_LOG] 📥 Backend Raw Response:", JSON.stringify(data)); + if (data && (data.country || data.country_code || data.city)) { + const isoCode = + data.country_code || + (data.country && data.country.trim().length === 2 + ? data.country.trim().toUpperCase() + : undefined); + const phoneCode = resolvePhoneCodeFromCountryCode(isoCode); + const countryName = + resolveCountryName(data.country || isoCode, "en") || data.country; + const countryFa = + resolveCountryName(data.country || isoCode, "fa") || countryName; + + console.log( + `[GEO_AUTO_LOG] 🌍 Resolved Country: FA=${countryFa} | EN=${countryName} | Code=${isoCode} | City=${data.city || "None"} | IP=${data.ip || "None"}`, + ); + + const region: UserGeoRegion = { + ip: data.ip, + city: data.city, + country: countryName, + countryCode: isoCode, + phoneCode: phoneCode || "+44", + }; + setStoredUserGeoRegion(region); + return region; + } + } catch (err: any) { + console.error( + "[GEO_AUTO_LOG] ❌ Backend Region Error:", + err?.response?.data || err?.message || err, + ); + } + + // 2. Secondary fallback: ipapi.co with 2s timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 2000); + try { + const res = await fetch("https://ipapi.co/json/", { + signal: controller.signal, + }); + clearTimeout(timeoutId); + if (res?.ok) { + const data = await res.json(); + if ( + data && + (data.country_name || data.city || data.country_calling_code) + ) { + const rawPhone = data.country_calling_code + ? String(data.country_calling_code).trim() + : ""; + const phoneCode = rawPhone.startsWith("+") + ? rawPhone + : rawPhone + ? `+${rawPhone}` + : "+44"; + const region: UserGeoRegion = { + ip: data.ip, + city: data.city, + country: data.country_name, + countryCode: data.country_code, + phoneCode, + }; + setStoredUserGeoRegion(region); + return region; + } + } + } catch { + clearTimeout(timeoutId); + } + + // 3. Tertiary fallback: ipwho.is with 2s timeout + const secondaryController = new AbortController(); + const secondaryTimeoutId = setTimeout( + () => secondaryController.abort(), + 2000, + ); + try { + const res = await fetch("https://ipwho.is/", { + signal: secondaryController.signal, + }); + clearTimeout(secondaryTimeoutId); + if (res?.ok) { + const data = await res.json(); + if (data && (data.country || data.city || data.calling_code)) { + const rawPhone = data.calling_code + ? String(data.calling_code).trim() + : ""; + const phoneCode = rawPhone.startsWith("+") + ? rawPhone + : rawPhone + ? `+${rawPhone}` + : "+44"; + const region: UserGeoRegion = { + ip: data.ip, + city: data.city, + country: data.country, + countryCode: data.country_code, + phoneCode, + }; + setStoredUserGeoRegion(region); + return region; + } + } + } catch { + clearTimeout(secondaryTimeoutId); + } + + // 4. Default fallback + const defaultRegion: UserGeoRegion = { + phoneCode: "+44", + }; + setStoredUserGeoRegion(defaultRegion); + return defaultRegion; + } catch { + const defaultRegion: UserGeoRegion = { + phoneCode: "+44", + }; + setStoredUserGeoRegion(defaultRegion); + return defaultRegion; + } + })(); + + return geoRegionPromise; +} diff --git a/src/lib/marriage-field-formatter.ts b/src/lib/marriage-field-formatter.ts index 25d0c74..9a4ad25 100644 --- a/src/lib/marriage-field-formatter.ts +++ b/src/lib/marriage-field-formatter.ts @@ -36,7 +36,7 @@ export function isMarriagePhoneFieldValue( } export function formatFieldValue(value: MarriageFieldValue): string | null { - if (value === null || value === "") { + if (value === null || value === undefined || value === "") { return null; } @@ -48,6 +48,16 @@ export function formatFieldValue(value: MarriageFieldValue): string | null { return value ? "Yes" : "No"; } + if (typeof value === "object") { + if ("country" in value || "city" in value || "state" in value) { + const v = value as { country?: string; state?: string; city?: string }; + const parts = [v.country, v.state, v.city] + .map((p) => (typeof p === "string" ? p.trim() : "")) + .filter(Boolean); + return parts.join(", "); + } + } + return String(value); } diff --git a/src/translations/locales/ar.json b/src/translations/locales/ar.json index 0b92805..a4d1241 100644 --- a/src/translations/locales/ar.json +++ b/src/translations/locales/ar.json @@ -141,7 +141,7 @@ "Current Housing Status": "حالة السكن الحالية", "Current Marital Status": "الحالة الاجتماعية الحالية", "Current Nationality / Citizenship": "الجنسية الحالية / المواطنة", - "Current Residence": "الإقامة الحالية", + "Current Residence": "مكان الإقامة الحالي", "Current user": "المستخدم الحالي", "Currently building suitable financial conditions": "حاليا بناء الظروف المالية المناسبة", "Curvy/Full": "متعرج / كامل", @@ -260,7 +260,7 @@ "Height in Centimeters": "Height in Centimeters", "Help": "يساعد", "High School Diploma": "دبلوم المدرسة الثانوية", - "Highest Level of Education": "أعلى مستوى من التعليم", + "Highest Level of Education": "أعلى مستوى تعليمي", "Homemaker": "ربة منزل", "Homeowner": "صاحب المنزل", "Hookah": "الشيشة", @@ -308,7 +308,7 @@ "Iraq": "Iraq", "Items Per Page": "العناصر لكل صفحة", "Job Seeking / Unemployed": "Job Seeking / Unemployed", - "Job Title": "مسمى وظيفي", + "Job Title": "المسمى الوظيفي", "Joint custody or periodic visitation/relocation between parents.": "الحضانة المشتركة أو الزيارة الدورية/الانتقال بين الوالدين.", "Kurdish": "كردي", "Language Learning": "تعلم اللغة", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "لديك حق الوصول النشط لعرض المرشحين.", "You will be contacted by your consultant.": "سيتم التواصل معك من قبل المستشار الخاص بك.", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "لقد أكملت كافة الحقول المطلوبة. ومع ذلك، فإن ملء جميع الأقسام سيساعدنا في العثور على تطابقات أفضل لك", - "Your Hobbies and Main Interests": "هواياتك واهتماماتك الرئيسية", + "Your Hobbies and Main Interests": "الهوايات والاهتمامات الرئيسية", "Your Personality Traits": "سمات شخصيتك", "Your details are only used for the matching process.": "يتم استخدام التفاصيل الخاصة بك فقط لعملية المطابقة.", "Your information is kept strictly confidential.": "يتم الاحتفاظ بمعلوماتك بسرية تامة.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "أدخل اسم الدواء وسبب الاستخدام...", "نام دارو و دلیل مصرف را وارد نمایید...": "أدخل اسم الدواء وسبب الاستخدام...", "وضعیت سلامت جسمانی": "حالة الصحة الجسدية", - "توضیحات وضعیت جسمانی": "وصف الصحة الجسدية" + "توضیحات وضعیت جسمانی": "وصف الصحة الجسدية", + "Name": "الاسم" } \ No newline at end of file diff --git a/src/translations/locales/az.json b/src/translations/locales/az.json index c1101a5..be8ef8f 100644 --- a/src/translations/locales/az.json +++ b/src/translations/locales/az.json @@ -141,7 +141,7 @@ "Current Housing Status": "Cari Mənzil Vəziyyəti", "Current Marital Status": "Cari Ailə Vəziyyəti", "Current Nationality / Citizenship": "Hazırkı Vətəndaşlıq / Vətəndaşlıq", - "Current Residence": "Hazırkı Yaşayış yeri", + "Current Residence": "Hazırkı yaşayış yeri", "Current user": "Cari istifadəçi", "Currently building suitable financial conditions": "Hal-hazırda uyğun maliyyə şəraiti yaratmaq", "Curvy/Full": "Qıvrımlı/Tam", @@ -199,7 +199,7 @@ "Education, Career, and Economic Status": "Təhsil, Karyera və İqtisadi Vəziyyət", "Emotional": "Emosional", "Employed": "İşlədi", - "Employment Status": "Məşğulluq Vəziyyəti", + "Employment Status": "Məşğulluq vəziyyəti", "English": "İngilis dili", "English, French, etc.": "İngilis, Fransız və s.", "Enter details here...": "Təfərrüatları bura daxil edin...", @@ -219,7 +219,7 @@ "Father alive": "Ata sağdır", "Father has passed away": "Ata rəhmətə gedib", "Feel free to briefly explain your decision...": "Qərarınızı qısaca izah etməkdən çekinmeyin...", - "Field of Study": "Tədqiqat sahəsi", + "Field of Study": "Təhsil sahəsi", "Final Consent": "Yekun razılıq", "Final Match Introduction": "Final matçının təqdimatı", "Final Notice": "Yekun bildiriş", @@ -235,7 +235,7 @@ "Full Hijab with modest clothing - Modest styling with hair completely covered.": "Təvazökar geyimli tam hicab - Tamamilə örtülmüş saçlarla təvazökar üslub.", "Full Islamic covering (Maximum Hijab) - Abaya, Jilbab, Chador, or Niqab with full observance.": "Tam İslam örtüyü (Maksimum Hicab) - Abaya, Cilbab, Çador və ya Niqab tam riayət etməklə.", "Full Islamic covering mandatory": "Tam İslami örtük məcburidir", - "Full Name": "Tam adı", + "Full Name": "Tam ad", "Full makeup": "Tam makiyaj", "Full-time Employed": "Tam iş günü", "Fully able to support expenses": "Xərcləri tam dəstəkləyə bilir", @@ -260,7 +260,7 @@ "Height in Centimeters": "Hündürlüyü Santimetrlə", "Help": "Kömək edin", "High School Diploma": "Orta məktəb diplomu", - "Highest Level of Education": "Ən Yüksək Təhsil Səviyyəsi", + "Highest Level of Education": "Ən yüksək təhsil səviyyəsi", "Homemaker": "Evdar", "Homeowner": "Ev sahibi", "Hookah": "Qəlyan", @@ -308,7 +308,7 @@ "Iraq": "İraq", "Items Per Page": "Səhifə Başına Elementlər", "Job Seeking / Unemployed": "İş Axtarıram/İşsiz", - "Job Title": "Vəzifə adı", + "Job Title": "Vəzifə / Peşə adı", "Joint custody or periodic visitation/relocation between parents.": "Valideynlər arasında birgə qəyyumluq və ya dövri ziyarət/köçürmə.", "Kurdish": "kürd", "Language Learning": "Dil Öyrənilməsi", @@ -684,7 +684,7 @@ "View More Details": "Ətraflı Məlumata Baxın", "View Profile": "Profilə baxın", "View contact number": "Əlaqə nömrəsinə baxın", - "View more details": "Ətraflı təfərrüata baxın", + "View more details": "Daha çox təfərrüata baxın", "View profile": "Profilə baxın", "Watch Video": "Videoya baxın", "We did not reach an agreement": "Razılığa gəlmədik", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "Namizədlərə baxmaq üçün aktiv girişiniz var.", "You will be contacted by your consultant.": "Məsləhətçiniz sizinlə əlaqə saxlayacaq.", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "Bütün tələb olunan sahələri doldurmusunuz. Bununla belə, bütün bölmələri doldurmaq bizə sizin üçün daha yaxşı uyğunluqlar tapmağa kömək edəcək", - "Your Hobbies and Main Interests": "Hobbiləriniz və Əsas Maraqlarınız", + "Your Hobbies and Main Interests": "Hobbiləriniz və əsas maraqlarınız", "Your Personality Traits": "Şəxsiyyət xüsusiyyətləriniz", "Your details are only used for the matching process.": "Detallarınız yalnız uyğunlaşma prosesi üçün istifadə olunur.", "Your information is kept strictly confidential.": "Məlumatlarınız ciddi şəkildə məxfi saxlanılır.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "Dərmanın adını və istifadə səbəbini daxil edin...", "نام دارو و دلیل مصرف را وارد نمایید...": "Dərmanın adını və istifadə səbəbini daxil edin...", "وضعیت سلامت جسمانی": "Fiziki Sağlamlıq Vəziyyəti", - "توضیحات وضعیت جسمانی": "Fiziki Sağlamlıq Təsviri" + "توضیحات وضعیت جسمانی": "Fiziki Sağlamlıq Təsviri", + "Name": "Ad" } \ No newline at end of file diff --git a/src/translations/locales/bn.json b/src/translations/locales/bn.json index 2c933da..cd3a011 100644 --- a/src/translations/locales/bn.json +++ b/src/translations/locales/bn.json @@ -199,7 +199,7 @@ "Education, Career, and Economic Status": "শিক্ষা, কর্মজীবন, এবং অর্থনৈতিক অবস্থা", "Emotional": "আবেগপ্রবণ", "Employed": "নিযুক্ত", - "Employment Status": "কর্মসংস্থানের অবস্থা", + "Employment Status": "কর্মসংস্থান অবস্থা", "English": "ইংরেজি", "English, French, etc.": "ইংরেজি, ফরাসি, ইত্যাদি", "Enter details here...": "এখানে বিস্তারিত লিখুন...", @@ -260,7 +260,7 @@ "Height in Centimeters": "সেন্টিমিটারে উচ্চতা", "Help": "সাহায্য", "High School Diploma": "High School Diploma", - "Highest Level of Education": "শিক্ষার সর্বোচ্চ স্তর", + "Highest Level of Education": "সর্বোচ্চ শিক্ষাগত যোগ্যতা", "Homemaker": "গৃহিণী", "Homeowner": "বাড়ির মালিক", "Hookah": "হুক্কা", @@ -308,7 +308,7 @@ "Iraq": "ইরাক", "Items Per Page": "পৃষ্ঠা প্রতি আইটেম", "Job Seeking / Unemployed": "চাকরির খোঁজ / বেকার", - "Job Title": "চাকরির শিরোনাম", + "Job Title": "কাজের পদবি", "Joint custody or periodic visitation/relocation between parents.": "যৌথ হেফাজত বা পিতামাতার মধ্যে পর্যায়ক্রমিক পরিদর্শন/স্থানান্তর।", "Kurdish": "কুর্দি", "Language Learning": "ভাষা শিক্ষা", @@ -684,7 +684,7 @@ "View More Details": "আরো বিস্তারিত দেখুন", "View Profile": "প্রোফাইল দেখুন", "View contact number": "যোগাযোগের নম্বর দেখুন", - "View more details": "আরো বিস্তারিত দেখুন", + "View more details": "আরও বিস্তারিত দেখুন", "View profile": "প্রোফাইল দেখুন", "Watch Video": "ভিডিও দেখুন", "We did not reach an agreement": "আমরা সমঝোতায় পৌঁছাইনি", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "প্রার্থীদের দেখতে আপনার সক্রিয় অ্যাক্সেস আছে।", "You will be contacted by your consultant.": "আপনার পরামর্শদাতা দ্বারা আপনার সাথে যোগাযোগ করা হবে।", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "আপনি সমস্ত প্রয়োজনীয় ক্ষেত্র সম্পূর্ণ করেছেন। যাইহোক, সমস্ত বিভাগ পূরণ করা আমাদের আপনার জন্য আরও ভাল মিল খুঁজে পেতে সাহায্য করবে", - "Your Hobbies and Main Interests": "আপনার শখ এবং প্রধান আগ্রহ", + "Your Hobbies and Main Interests": "আপনার শখ ও প্রধান আগ্রহসমূহ", "Your Personality Traits": "আপনার ব্যক্তিত্ব বৈশিষ্ট্য", "Your details are only used for the matching process.": "আপনার বিবরণ শুধুমাত্র মিল প্রক্রিয়ার জন্য ব্যবহার করা হয়.", "Your information is kept strictly confidential.": "আপনার তথ্য কঠোরভাবে গোপন রাখা হয়.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "ওষুধের নাম এবং কারণ লিখুন...", "نام دارو و دلیل مصرف را وارد نمایید...": "ওষুধের নাম এবং কারণ লিখুন...", "وضعیت سلامت جسمانی": "শারীরিক স্বাস্থ্যের অবস্থা", - "توضیحات وضعیت جسمانی": "শারীরিক স্বাস্থ্যের বিবরণ" + "توضیحات وضعیت جسمانی": "শারীরিক স্বাস্থ্যের বিবরণ", + "Name": "নাম" } \ No newline at end of file diff --git a/src/translations/locales/da.json b/src/translations/locales/da.json index 2882d48..6ca63b0 100644 --- a/src/translations/locales/da.json +++ b/src/translations/locales/da.json @@ -141,7 +141,7 @@ "Current Housing Status": "Nuværende boligstatus", "Current Marital Status": "Nuværende ægteskabelig status", "Current Nationality / Citizenship": "Nuværende nationalitet / statsborgerskab", - "Current Residence": "Nuværende bolig", + "Current Residence": "Nuværende bopæl", "Current user": "Nuværende bruger", "Currently building suitable financial conditions": "Opbygger i øjeblikket passende økonomiske forhold", "Curvy/Full": "Kurvet/fuld", @@ -199,7 +199,7 @@ "Education, Career, and Economic Status": "Uddannelse, karriere og økonomisk status", "Emotional": "Følelsesmæssigt", "Employed": "Ansat", - "Employment Status": "Ansættelsesstatus", + "Employment Status": "Beskæftigelsesstatus", "English": "engelsk", "English, French, etc.": "engelsk, fransk osv.", "Enter details here...": "Indtast detaljer her...", @@ -235,7 +235,7 @@ "Full Hijab with modest clothing - Modest styling with hair completely covered.": "Fuld Hijab med beskedent tøj - Beskeden styling med hår helt dækket.", "Full Islamic covering (Maximum Hijab) - Abaya, Jilbab, Chador, or Niqab with full observance.": "Fuld islamisk dækning (Maksimal Hijab) - Abaya, Jilbab, Chador eller Niqab med fuld overholdelse.", "Full Islamic covering mandatory": "Fuld islamisk dækning obligatorisk", - "Full Name": "Fuldt navn", + "Full Name": "Fulde navn", "Full makeup": "Fuld makeup", "Full-time Employed": "Fuldtidsansat", "Fully able to support expenses": "Fuldt ud i stand til at dække udgifter", @@ -308,7 +308,7 @@ "Iraq": "Irak", "Items Per Page": "Items Per Page", "Job Seeking / Unemployed": "Jobsøgende / ledig", - "Job Title": "Jobtitel", + "Job Title": "Stilling", "Joint custody or periodic visitation/relocation between parents.": "Fælles forældremyndighed eller periodisk samvær/flytning mellem forældre.", "Kurdish": "kurdisk", "Language Learning": "Sprogindlæring", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "Du har aktiv adgang til at se kandidater.", "You will be contacted by your consultant.": "Du vil blive kontaktet af din konsulent.", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "You've completed all required fields. However, filling in all sections will help us find better matches for you", - "Your Hobbies and Main Interests": "Dine hobbyer og hovedinteresser", + "Your Hobbies and Main Interests": "Dine hobbyer og vigtigste interesser", "Your Personality Traits": "Dine personlighedstræk", "Your details are only used for the matching process.": "Your details are only used for the matching process.", "Your information is kept strictly confidential.": "Your information is kept strictly confidential.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "Indtast medicinnavn og årsag...", "نام دارو و دلیل مصرف را وارد نمایید...": "Indtast medicinnavn og årsag...", "وضعیت سلامت جسمانی": "Fysisk helbredstilstand", - "توضیحات وضعیت جسمانی": "Beskrivelse af fysisk helbred" + "توضیحات وضعیت جسمانی": "Beskrivelse af fysisk helbred", + "Name": "Navn" } \ No newline at end of file diff --git a/src/translations/locales/de.json b/src/translations/locales/de.json index 33ff350..4f19576 100644 --- a/src/translations/locales/de.json +++ b/src/translations/locales/de.json @@ -141,7 +141,7 @@ "Current Housing Status": "Aktueller Wohnstatus", "Current Marital Status": "Aktueller Familienstand", "Current Nationality / Citizenship": "Aktuelle Nationalität/Staatsbürgerschaft", - "Current Residence": "Aktueller Wohnsitz", + "Current Residence": "Aktueller Wohnort", "Current user": "Aktueller Benutzer", "Currently building suitable financial conditions": "Derzeit werden geeignete finanzielle Rahmenbedingungen aufgebaut", "Curvy/Full": "Kurvig/voll", @@ -260,7 +260,7 @@ "Height in Centimeters": "Höhe in Zentimetern", "Help": "Help", "High School Diploma": "High-School-Diplom", - "Highest Level of Education": "Höchstes Bildungsniveau", + "Highest Level of Education": "Höchster Bildungsabschluss", "Homemaker": "Hausfrau", "Homeowner": "Hausbesitzer", "Hookah": "Shisha", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "Geben Sie den Medikamentennamen und den Grund ein...", "نام دارو و دلیل مصرف را وارد نمایید...": "Geben Sie den Medikamentennamen und den Grund ein...", "وضعیت سلامت جسمانی": "Körperlicher Gesundheitszustand", - "توضیحات وضعیت جسمانی": "Beschreibung des körperlichen Zustands" + "توضیحات وضعیت جسمانی": "Beschreibung des körperlichen Zustands", + "Name": "Name" } \ No newline at end of file diff --git a/src/translations/locales/en.json b/src/translations/locales/en.json index ba5a3dc..a34416a 100644 --- a/src/translations/locales/en.json +++ b/src/translations/locales/en.json @@ -843,5 +843,7 @@ "Do you have a history of treatment or counseling related to mental health in the past?": "Do you have a history of treatment or counseling related to mental health in the past?", "Do you currently take any medication on an ongoing basis?": "Do you currently take any medication on an ongoing basis?", "Medication Name and Reason for Use": "Medication Name and Reason for Use", - "Enter medication name and reason for use...": "Enter medication name and reason for use..." + "Enter medication name and reason for use...": "Enter medication name and reason for use...", + "Enter a valid phone number with country code.": "Enter a valid phone number with country code.", + "Name": "Name" } \ No newline at end of file diff --git a/src/translations/locales/es.json b/src/translations/locales/es.json index 14548b4..cdbd522 100644 --- a/src/translations/locales/es.json +++ b/src/translations/locales/es.json @@ -46,7 +46,7 @@ "Additional Comments on Economic and Housing Status": "Comentarios adicionales sobre la situación económica y de vivienda", "Additional details about family responsibility": "Detalles adicionales sobre la responsabilidad familiar", "Afghanistan": "Afganistán", - "Age": "edad", + "Age": "Edad", "Alcohol is a serious red line": "El alcohol es una línea roja grave", "Alcohol is a serious red line for me": "El alcohol es una línea roja grave para mí.", "Alcoholic Beverages": "Bebidas Alcohólicas", @@ -199,7 +199,7 @@ "Education, Career, and Economic Status": "Educación, carrera y situación económica", "Emotional": "Emocional", "Employed": "Empleado", - "Employment Status": "Estado de empleo", + "Employment Status": "Estado laboral", "English": "ingles", "English, French, etc.": "inglés, francés, etc.", "Enter details here...": "Ingrese los detalles aquí...", @@ -260,7 +260,7 @@ "Height in Centimeters": "Altura en centímetros", "Help": "Help", "High School Diploma": "Diploma de escuela secundaria", - "Highest Level of Education": "Nivel más alto de educación", + "Highest Level of Education": "Nivel educativo más alto", "Homemaker": "ama de casa", "Homeowner": "Propietario de casa", "Hookah": "cachimba", @@ -308,7 +308,7 @@ "Iraq": "Irak", "Items Per Page": "Items Per Page", "Job Seeking / Unemployed": "Buscando empleo / Desempleado", - "Job Title": "Título del trabajo", + "Job Title": "Título del puesto", "Joint custody or periodic visitation/relocation between parents.": "Custodia compartida o visitas periódicas/reubicación entre padres.", "Kurdish": "kurdo", "Language Learning": "Aprendizaje de idiomas", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "Ingrese el nombre del medicamento y el motivo...", "نام دارو و دلیل مصرف را وارد نمایید...": "Ingrese el nombre del medicamento y el motivo...", "وضعیت سلامت جسمانی": "Estado de salud física", - "توضیحات وضعیت جسمانی": "Descripción de la salud física" + "توضیحات وضعیت جسمانی": "Descripción de la salud física", + "Name": "Nombre" } \ No newline at end of file diff --git a/src/translations/locales/fa.json b/src/translations/locales/fa.json index 3bbd532..bb11a05 100644 --- a/src/translations/locales/fa.json +++ b/src/translations/locales/fa.json @@ -855,5 +855,6 @@ "Enter medication name and reason for use...": "نام دارو و دلیل مصرف را وارد نمایید...", "نام دارو و دلیل مصرف را وارد نمایید...": "نام دارو و دلیل مصرف را وارد نمایید...", "وضعیت سلامت جسمانی": "وضعیت سلامت جسمانی", - "توضیحات وضعیت جسمانی": "توضیحات وضعیت جسمانی" + "توضیحات وضعیت جسمانی": "توضیحات وضعیت جسمانی", + "Name": "نام" } \ No newline at end of file diff --git a/src/translations/locales/fr.json b/src/translations/locales/fr.json index be5d825..282e887 100644 --- a/src/translations/locales/fr.json +++ b/src/translations/locales/fr.json @@ -199,7 +199,7 @@ "Education, Career, and Economic Status": "Éducation, carrière et statut économique", "Emotional": "Émotionnel", "Employed": "Employé", - "Employment Status": "Statut d'emploi", + "Employment Status": "Statut professionnel", "English": "Anglais", "English, French, etc.": "Anglais, français, etc.", "Enter details here...": "Entrez les détails ici...", @@ -219,7 +219,7 @@ "Father alive": "Père vivant", "Father has passed away": "Le père est décédé", "Feel free to briefly explain your decision...": "Your explanatory text ...", - "Field of Study": "Domaine d'études", + "Field of Study": "Domaine d’études", "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", @@ -260,7 +260,7 @@ "Height in Centimeters": "Hauteur en centimètres", "Help": "Help", "High School Diploma": "Diplôme d'études secondaires", - "Highest Level of Education": "Niveau d'éducation le plus élevé", + "Highest Level of Education": "Plus haut niveau d’études", "Homemaker": "Femme au foyer", "Homeowner": "Propriétaire", "Hookah": "Narguilé", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "Vous disposez d'un accès actif pour visualiser les candidats.", "You will be contacted by your consultant.": "Vous serez contacté par votre consultant.", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "You've completed all required fields. However, filling in all sections will help us find better matches for you", - "Your Hobbies and Main Interests": "Vos passe-temps et principaux intérêts", + "Your Hobbies and Main Interests": "Vos loisirs et principaux centres d’intérêt", "Your Personality Traits": "Vos traits de personnalité", "Your details are only used for the matching process.": "Your details are only used for the matching process.", "Your information is kept strictly confidential.": "Your information is kept strictly confidential.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "Entrez le nom du médicament et le motif...", "نام دارو و دلیل مصرف را وارد نمایید...": "Entrez le nom du médicament et le motif...", "وضعیت سلامت جسمانی": "État de santé physique", - "توضیحات وضعیت جسمانی": "Description de la santé physique" + "توضیحات وضعیت جسمانی": "Description de la santé physique", + "Name": "Nom" } \ No newline at end of file diff --git a/src/translations/locales/gu.json b/src/translations/locales/gu.json index f559cb0..fe99916 100644 --- a/src/translations/locales/gu.json +++ b/src/translations/locales/gu.json @@ -141,7 +141,7 @@ "Current Housing Status": "વર્તમાન હાઉસિંગ સ્થિતિ", "Current Marital Status": "વર્તમાન વૈવાહિક સ્થિતિ", "Current Nationality / Citizenship": "વર્તમાન રાષ્ટ્રીયતા / નાગરિકતા", - "Current Residence": "વર્તમાન રહેઠાણ", + "Current Residence": "હાલનું રહેઠાણ", "Current user": "વર્તમાન વપરાશકર્તા", "Currently building suitable financial conditions": "હાલમાં યોગ્ય નાણાકીય પરિસ્થિતિઓ બનાવી રહી છે", "Curvy/Full": "કર્વી/પૂર્ણ", @@ -219,7 +219,7 @@ "Father alive": "પિતા જીવંત", "Father has passed away": "પિતા ગુજરી ગયા છે", "Feel free to briefly explain your decision...": "Your explanatory text ...", - "Field of Study": "અભ્યાસ ક્ષેત્ર", + "Field of Study": "અભ્યાસનું ક્ષેત્ર", "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", @@ -260,7 +260,7 @@ "Height in Centimeters": "સેન્ટીમીટરમાં ઊંચાઈ", "Help": "Help", "High School Diploma": "હાઇ સ્કૂલ ડિપ્લોમા", - "Highest Level of Education": "શિક્ષણનું ઉચ્ચતમ સ્તર", + "Highest Level of Education": "ઉચ્ચતમ શિક્ષણ સ્તર", "Homemaker": "ગૃહિણી", "Homeowner": "ઘરમાલિક", "Hookah": "હુક્કો", @@ -308,7 +308,7 @@ "Iraq": "ઈરાક", "Items Per Page": "Items Per Page", "Job Seeking / Unemployed": "નોકરીની શોધ / બેરોજગાર", - "Job Title": "જોબ શીર્ષક", + "Job Title": "નોકરીનું પદ", "Joint custody or periodic visitation/relocation between parents.": "સંયુક્ત કસ્ટડી અથવા માતા-પિતા વચ્ચે સમયાંતરે મુલાકાત/સ્થાપન.", "Kurdish": "કુર્દિશ", "Language Learning": "ભાષા શીખવી", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "દવાનું નામ અને કારણ દાખલ કરો...", "نام دارو و دلیل مصرف را وارد نمایید...": "દવાનું નામ અને કારણ દાખલ કરો...", "وضعیت سلامت جسمانی": "શારીરિક સ્વાસ્થ્ય સ્થિતિ", - "توضیحات وضعیت جسمانی": "શારીરિક સ્વાસ્થ્ય વર્ણન" + "توضیحات وضعیت جسمانی": "શારીરિક સ્વાસ્થ્ય વર્ણન", + "Name": "નામ" } \ No newline at end of file diff --git a/src/translations/locales/ha.json b/src/translations/locales/ha.json index 2b08607..986c768 100644 --- a/src/translations/locales/ha.json +++ b/src/translations/locales/ha.json @@ -141,7 +141,7 @@ "Current Housing Status": "Matsayin Gidajen Yanzu", "Current Marital Status": "Matsayin Aure na Yanzu", "Current Nationality / Citizenship": "Dan Kasa / Dan Kasa na Yanzu", - "Current Residence": "Mazauni na Yanzu", + "Current Residence": "Wurin zama na yanzu", "Current user": "Mai amfani na yanzu", "Currently building suitable financial conditions": "A halin yanzu gina yanayin kuɗi masu dacewa", "Curvy/Full": "Curvy/cikakke", @@ -219,7 +219,7 @@ "Father alive": "Uba a raye", "Father has passed away": "Uban ya rasu", "Feel free to briefly explain your decision...": "Your explanatory text ...", - "Field of Study": "Filin Karatu", + "Field of Study": "Fannin Karatu", "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", @@ -260,7 +260,7 @@ "Height in Centimeters": "Tsawon santimita", "Help": "Help", "High School Diploma": "High School Diploma", - "Highest Level of Education": "Highest Level of Education", + "Highest Level of Education": "Mafi girman matakin ilimi", "Homemaker": "Homemaker", "Homeowner": "Mai gida", "Hookah": "Hookah", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "Kuna da damar shiga don duba 'yan takara.", "You will be contacted by your consultant.": "Mai ba da shawara zai tuntube ku.", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "You've completed all required fields. However, filling in all sections will help us find better matches for you", - "Your Hobbies and Main Interests": "Abubuwan sha'awarku da Babban Sha'awar ku", + "Your Hobbies and Main Interests": "Abubuwan sha’awa da manyan bukatu", "Your Personality Traits": "Halayen Halinku", "Your details are only used for the matching process.": "Your details are only used for the matching process.", "Your information is kept strictly confidential.": "Your information is kept strictly confidential.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "Shigar da sunan magani da dalilin sha...", "نام دارو و دلیل مصرف را وارد نمایید...": "Shigar da sunan magani da dalilin sha...", "وضعیت سلامت جسمانی": "Yanayin Lafiyar Jiki", - "توضیحات وضعیت جسمانی": "Bayanin Lafiyar Jiki" + "توضیحات وضعیت جسمانی": "Bayanin Lafiyar Jiki", + "Name": "Suna" } \ No newline at end of file diff --git a/src/translations/locales/he.json b/src/translations/locales/he.json index cbe0f30..0fbd3ef 100644 --- a/src/translations/locales/he.json +++ b/src/translations/locales/he.json @@ -346,5 +346,14 @@ "Physical Health Status": "מצב בריאות פיזית", "وضعیت سلامت جسمانی": "מצב בריאות פיזית", "Physical Health Description": "תיאור בריאות פיזית", - "توضیحات وضعیت جسمانی": "תיאור בריאות פיזית" + "توضیحات وضعیت جسمانی": "תיאור בריאות פיזית", + "Full Name": "שם מלא", + "Name": "שם", + "Current Residence": "מקום מגורים נוכחי", + "Highest Level of Education": "רמת ההשכלה הגבוהה ביותר", + "Field of Study": "תחום לימודים", + "Job Title": "תואר התפקיד", + "Employment Status": "מצב תעסוקתי", + "Your Hobbies and Main Interests": "התחביבים ותחומי העניין העיקריים שלך", + "View more details": "הצג פרטים נוספים" } \ No newline at end of file diff --git a/src/translations/locales/hi.json b/src/translations/locales/hi.json index 9b44413..edfc79b 100644 --- a/src/translations/locales/hi.json +++ b/src/translations/locales/hi.json @@ -199,7 +199,7 @@ "Education, Career, and Economic Status": "शिक्षा, करियर और आर्थिक स्थिति", "Emotional": "भावुक", "Employed": "कार्यरत", - "Employment Status": "रोजगार की स्थिति", + "Employment Status": "रोज़गार की स्थिति", "English": "अंग्रेजी", "English, French, etc.": "अंग्रेजी, फ्रेंच, आदि", "Enter details here...": "यहां विवरण दर्ज करें...", @@ -260,7 +260,7 @@ "Height in Centimeters": "ऊंचाई सेंटीमीटर में", "Help": "Help", "High School Diploma": "हाई स्कूल डिप्लोमा", - "Highest Level of Education": "शिक्षा का उच्चतम स्तर", + "Highest Level of Education": "उच्चतम शिक्षा स्तर", "Homemaker": "गृहिणी", "Homeowner": "गृहस्वामी", "Hookah": "हुक्का", @@ -308,7 +308,7 @@ "Iraq": "इराक", "Items Per Page": "Items Per Page", "Job Seeking / Unemployed": "नौकरी तलाशने वाले/बेरोजगार", - "Job Title": "नौकरी का शीर्षक", + "Job Title": "पद / व्यवसाय", "Joint custody or periodic visitation/relocation between parents.": "माता-पिता के बीच संयुक्त अभिरक्षा या आवधिक मुलाक़ात/स्थानांतरण।", "Kurdish": "कुर्दिश", "Language Learning": "भाषा सीखना", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "उम्मीदवारों को देखने के लिए आपके पास सक्रिय पहुंच है।", "You will be contacted by your consultant.": "आपका सलाहकार आपसे संपर्क करेगा.", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "You've completed all required fields. However, filling in all sections will help us find better matches for you", - "Your Hobbies and Main Interests": "आपके शौक और मुख्य रुचियाँ", + "Your Hobbies and Main Interests": "आपके शौक और मुख्य रुचियां", "Your Personality Traits": "आपके व्यक्तित्व के लक्षण", "Your details are only used for the matching process.": "Your details are only used for the matching process.", "Your information is kept strictly confidential.": "Your information is kept strictly confidential.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "दवा का नाम और उपयोग का कारण दर्ज करें...", "نام دارو و دلیل مصرف را وارد نمایید...": "दवा का नाम और उपयोग का कारण दर्ज करें...", "وضعیت سلامت جسمانی": "शारीरिक स्वास्थ्य की स्थिति", - "توضیحات وضعیت جسمانی": "शारीरिक स्वास्थ्य का विवरण" + "توضیحات وضعیت جسمانی": "शारीरिक स्वास्थ्य का विवरण", + "Name": "नाम" } \ No newline at end of file diff --git a/src/translations/locales/id.json b/src/translations/locales/id.json index bd256cd..15bb748 100644 --- a/src/translations/locales/id.json +++ b/src/translations/locales/id.json @@ -346,5 +346,14 @@ "Physical Health Status": "Status Kesehatan Fisik", "وضعیت سلامت جسمانی": "Status Kesehatan Fisik", "Physical Health Description": "Deskripsi Kesehatan Fisik", - "توضیحات وضعیت جسمانی": "Deskripsi Kesehatan Fisik" + "توضیحات وضعیت جسمانی": "Deskripsi Kesehatan Fisik", + "Full Name": "Nama Lengkap", + "Name": "Nama", + "Current Residence": "Tempat Tinggal Saat Ini", + "Highest Level of Education": "Tingkat Pendidikan Tertinggi", + "Field of Study": "Bidang Studi", + "Job Title": "Jabatan / Pekerjaan", + "Employment Status": "Status Pekerjaan", + "Your Hobbies and Main Interests": "Hobi dan Minat Utama Anda", + "View more details": "Lihat detail selengkapnya" } \ No newline at end of file diff --git a/src/translations/locales/ks.json b/src/translations/locales/ks.json index 1202a0b..ff18a96 100644 --- a/src/translations/locales/ks.json +++ b/src/translations/locales/ks.json @@ -285,7 +285,7 @@ "Day": "دوہ", "Month": "ماہ", "Year": "ؤری", - "Age": "عمر", + "Age": "وٲنٛس", "Confirm": "تصدیق کریں", "Failed to load advisors.": "مشیرن لوڈ کرنأک ناکام۔", "Try again": "دوبآ کوشش کریو", @@ -346,5 +346,14 @@ "Physical Health Status": "جسمانی صحتٕچ حالت", "وضعیت سلامت جسمانی": "جسمانی صحتٕچ حالت", "Physical Health Description": "جسمانی صحتٕچ وضاحت", - "توضیحات وضعیت جسمانی": "جسمانی صحتٕچ وضاحت" + "توضیحات وضعیت جسمانی": "جسمانی صحتٕچ وضاحت", + "Full Name": "پوٗرٕ ناڤ", + "Name": "ناو", + "Current Residence": "مَوجوٗدٕ رِہایش", + "Highest Level of Education": "ساروِی کھۄتہٕ زیٛادٕ تٲلیٖم", + "Field of Study": "تٲلیٖمی شۄعبہٕ", + "Job Title": "کٲم ہُنٛد ناو", + "Employment Status": "مُلازمتٕچ حالت", + "Your Hobbies and Main Interests": "تُہنٛدؠ شۄق تہٕ اَہَم دِلچسپی", + "View more details": "مزید تفصیل وُچھِو" } \ No newline at end of file diff --git a/src/translations/locales/pt.json b/src/translations/locales/pt.json index 4929988..5ed018f 100644 --- a/src/translations/locales/pt.json +++ b/src/translations/locales/pt.json @@ -346,5 +346,14 @@ "Physical Health Status": "Estado de Saúde Física", "وضعیت سلامت جسمانی": "Estado de Saúde Física", "Physical Health Description": "Descrição da Saúde Física", - "توضیحات وضعیت جسمانی": "Descrição da Saúde Física" + "توضیحات وضعیت جسمانی": "Descrição da Saúde Física", + "Full Name": "Nome completo", + "Name": "Nome", + "Current Residence": "Residência atual", + "Highest Level of Education": "Nível de escolaridade mais alto", + "Field of Study": "Área de estudo", + "Job Title": "Cargo / Título profissional", + "Employment Status": "Situação profissional", + "Your Hobbies and Main Interests": "Seus hobbies e principais interesses", + "View more details": "Ver mais detalhes" } \ No newline at end of file diff --git a/src/translations/locales/ru.json b/src/translations/locales/ru.json index 6b50212..268671b 100644 --- a/src/translations/locales/ru.json +++ b/src/translations/locales/ru.json @@ -221,7 +221,7 @@ "Father alive": "Отец жив", "Father has passed away": "Отец скончался", "Feel free to briefly explain your decision...": "Не стесняйтесь кратко объяснить свое решение...", - "Field of Study": "Область исследования", + "Field of Study": "Специальность / область обучения", "Final Consent": "Окончательное согласие", "Final Match Introduction": "Введение в финальный матч", "Final Notice": "Последнее уведомление", @@ -262,7 +262,7 @@ "Height in Centimeters": "Рост в сантиметрах", "Help": "Помощь", "High School Diploma": "Диплом средней школы", - "Highest Level of Education": "Высший уровень образования", + "Highest Level of Education": "Наивысший уровень образования", "Homemaker": "Домохозяйка", "Homeowner": "домовладелец", "Hookah": "Кальян", @@ -310,7 +310,7 @@ "Iraq": "Ирак", "Items Per Page": "Элементов на странице", "Job Seeking / Unemployed": "Ищу работу / Безработный", - "Job Title": "Должность", + "Job Title": "Должность / Профессия", "Joint custody or periodic visitation/relocation between parents.": "Совместная опека или периодическое посещение/переезд родителей.", "Kurdish": "курдский", "Language Learning": "Изучение языка", @@ -693,7 +693,7 @@ "View More Details": "Посмотреть более подробную информацию", "View Profile": "Посмотреть профиль", "View contact number": "Посмотреть контактный номер", - "View more details": "Посмотреть более подробную информацию", + "View more details": "Посмотреть подробнее", "View profile": "Посмотреть профиль", "Watch Video": "Посмотреть видео", "We are in the acquaintance/proposal process and nothing is finalized yet": "Мы находимся в процессе знакомства/предложения, и еще ничего не решено", @@ -811,5 +811,6 @@ "Enter medication name and reason for use...": "Введите название препарата и причину приема...", "نام دارو و دلیل مصرف را وارد نمایید...": "Введите название препарата и причину приема...", "وضعیت سلامت جسمانی": "Физическое состояние здоровья", - "توضیحات وضعیت جسمانی": "Описание физического состояния" + "توضیحات وضعیت جسمانی": "Описание физического состояния", + "Name": "Имя" } \ No newline at end of file diff --git a/src/translations/locales/sw.json b/src/translations/locales/sw.json index 48ad34d..224c42e 100644 --- a/src/translations/locales/sw.json +++ b/src/translations/locales/sw.json @@ -346,5 +346,14 @@ "Physical Health Status": "Hali ya Afya ya Kimwili", "وضعیت سلامت جسمانی": "Hali ya Afya ya Kimwili", "Physical Health Description": "Maelezo ya Afya ya Kimwili", - "توضیحات وضعیت جسمانی": "Maelezo ya Afya ya Kimwili" + "توضیحات وضعیت جسمانی": "Maelezo ya Afya ya Kimwili", + "Full Name": "Jina Kamili", + "Name": "Jina", + "Current Residence": "Makazi ya Sasa", + "Highest Level of Education": "Kiwango cha Juu cha Elimu", + "Field of Study": "Uwanja wa Masomo", + "Job Title": "Wadhifa wa Kazi", + "Employment Status": "Hali ya Ajira", + "Your Hobbies and Main Interests": "Mambo unayopenda na Maslahi Kuu", + "View more details": "Angalia maelezo zaidi" } \ No newline at end of file diff --git a/src/translations/locales/tg.json b/src/translations/locales/tg.json index 68d67ba..b0dbf5e 100644 --- a/src/translations/locales/tg.json +++ b/src/translations/locales/tg.json @@ -346,5 +346,14 @@ "Physical Health Status": "Вазъи саломатии ҷисмонӣ", "وضعیت سلامت جسمانی": "Вазъи саломатии ҷисмонӣ", "Physical Health Description": "Тавсифи саломатии ҷисмонӣ", - "توضیحات وضعیت جسمانی": "Тавсифи саломатии ҷисмонӣ" + "توضیحات وضعیت جسمانی": "Тавсифи саломатии ҷисмонӣ", + "Full Name": "Ному насаб", + "Name": "Ном", + "Current Residence": "Ҷои истиқомати ҳозира", + "Highest Level of Education": "Дараҷаи олии таҳсилот", + "Field of Study": "Соҳаи таҳсил", + "Job Title": "Унвони вазифа", + "Employment Status": "Вазъи шуғл", + "Your Hobbies and Main Interests": "Машғулиятҳо ва манфиатҳои асосии шумо", + "View more details": "Дидани тафсилоти бештар" } \ No newline at end of file diff --git a/src/translations/locales/tr.json b/src/translations/locales/tr.json index 9b909ff..0f5be69 100644 --- a/src/translations/locales/tr.json +++ b/src/translations/locales/tr.json @@ -346,5 +346,14 @@ "Physical Health Status": "Fiziksel Sağlık Durumu", "وضعیت سلامت جسمانی": "Fiziksel Sağlık Durumu", "Physical Health Description": "Fiziksel Sağlık Açıklaması", - "توضیحات وضعیت جسمانی": "Fiziksel Sağlık Açıklaması" + "توضیحات وضعیت جسمانی": "Fiziksel Sağlık Açıklaması", + "Full Name": "Tam Adı", + "Name": "İsim", + "Current Residence": "Mevcut İkametgah", + "Highest Level of Education": "En Yüksek Eğitim Düzeyi", + "Field of Study": "Öğrenim Alanı / Bölüm", + "Job Title": "Meslek / Unvan", + "Employment Status": "Çalışma Durumu", + "Your Hobbies and Main Interests": "Hobileriniz ve Temel İlgi Alanlarınız", + "View more details": "Daha fazla ayrıntı gör" } \ No newline at end of file diff --git a/src/translations/locales/ul.json b/src/translations/locales/ul.json index 96bc765..3ba710b 100644 --- a/src/translations/locales/ul.json +++ b/src/translations/locales/ul.json @@ -285,7 +285,7 @@ "Day": "كۈن", "Month": "ئاي", "Year": "يىل", - "Age": "يېشى", + "Age": "Age", "Confirm": "جەزملەش", "Failed to load advisors.": "مشاورین لوڈ نہیں ہو سکے۔", "Try again": "دوبارہ کوشش کریں", @@ -346,5 +346,14 @@ "Physical Health Status": "Jismoniy salomatlik", "وضعیت سلامت جسمانی": "Jismoniy salomatlik", "Physical Health Description": "Jismoniy salomatlik tavsifi", - "توضیحات وضعیت جسمانی": "Jismoniy salomatlik tavsifi" + "توضیحات وضعیت جسمانی": "Jismoniy salomatlik tavsifi", + "Full Name": "Full Name", + "Name": "Name", + "Current Residence": "Current Residence", + "Highest Level of Education": "Highest Level of Education", + "Field of Study": "Field of Study", + "Job Title": "Job Title", + "Employment Status": "Employment Status", + "Your Hobbies and Main Interests": "Your Hobbies and Main Interests", + "View more details": "View more details" } \ No newline at end of file diff --git a/src/translations/locales/ur.json b/src/translations/locales/ur.json index 23a0461..7677d0e 100644 --- a/src/translations/locales/ur.json +++ b/src/translations/locales/ur.json @@ -346,5 +346,14 @@ "Physical Health Status": "جسمانی صحت کی حالت", "وضعیت سلامت جسمانی": "جسمانی صحت کی حالت", "Physical Health Description": "جسمانی صحت کی تفصیل", - "توضیحات وضعیت جسمانی": "جسمانی صحت کی تفصیل" + "توضیحات وضعیت جسمانی": "جسمانی صحت کی تفصیل", + "Full Name": "مکمل نام", + "Name": "نام", + "Current Residence": "موجودہ رہائش", + "Highest Level of Education": "اعلیٰ ترین تعلیمی قابلیت", + "Field of Study": "شعبہ تعلیم", + "Job Title": "عہدہ / ملازمت کا عنوان", + "Employment Status": "ملازمت کی صورتحال", + "Your Hobbies and Main Interests": "آپ کے مشاغل اور اہم دلچسپیاں", + "View more details": "مزید تفصیلات دیکھیں" } \ No newline at end of file diff --git a/src/translations/locales/uz.json b/src/translations/locales/uz.json index a62fba7..9c69367 100644 --- a/src/translations/locales/uz.json +++ b/src/translations/locales/uz.json @@ -346,5 +346,14 @@ "Physical Health Status": "Jismoniy salomatlik holati", "وضعیت سلامت جسمانی": "Jismoniy salomatlik holati", "Physical Health Description": "Jismoniy salomatlik tavsifi", - "توضیحات وضعیت جسمانی": "Jismoniy salomatlik tavsifi" + "توضیحات وضعیت جسمانی": "Jismoniy salomatlik tavsifi", + "Full Name": "Toʻliq ism", + "Name": "Ism", + "Current Residence": "Hozirgi yashash joyi", + "Highest Level of Education": "Eng yuqori taʼlim darajasi", + "Field of Study": "Oʻqish sohasi / Yoʻnalishi", + "Job Title": "Kasb / Lavozim", + "Employment Status": "Bandlik holati", + "Your Hobbies and Main Interests": "Qiziqishlaringiz va asosiy mashgʻulotlaringiz", + "View more details": "Batafsil maʼlumotni koʻrish" } \ No newline at end of file diff --git a/src/translations/locales/zh.json b/src/translations/locales/zh.json index ba0e472..9cd097a 100644 --- a/src/translations/locales/zh.json +++ b/src/translations/locales/zh.json @@ -46,7 +46,7 @@ "Additional Comments on Economic and Housing Status": "关于经济和住房状况的补充意见", "Additional details about family responsibility": "有关家庭责任的其他详细信息", "Afghanistan": "Afghanistan", - "Age": "Age", + "Age": "年龄", "Alcohol is a serious red line": "酒精是一条严重的红线", "Alcohol is a serious red line for me": "Alcohol is a serious red line for me", "Alcoholic Beverages": "Alcoholic Beverages", @@ -141,7 +141,7 @@ "Current Housing Status": "目前的住房状况", "Current Marital Status": "Current Marital Status", "Current Nationality / Citizenship": "Current Nationality / Citizenship", - "Current Residence": "Current Residence", + "Current Residence": "现居住地", "Current user": "当前用户", "Currently building suitable financial conditions": "目前正在建立适当的财务条件", "Curvy/Full": "曲线/丰满", @@ -219,7 +219,7 @@ "Father alive": "父亲还活着", "Father has passed away": "父亲去世了", "Feel free to briefly explain your decision...": "Your explanatory text ...", - "Field of Study": "研究领域", + "Field of Study": "专业 / 学习领域", "Final Consent": "Final Consent", "Final Match Introduction": "Final Match Introduction", "Final Notice": "Final Notice", @@ -260,7 +260,7 @@ "Height in Centimeters": "高度(厘米)", "Help": "Help", "High School Diploma": "高中文凭", - "Highest Level of Education": "最高教育水平", + "Highest Level of Education": "最高学历", "Homemaker": "家庭主妇", "Homeowner": "房主", "Hookah": "水烟", @@ -725,7 +725,7 @@ "You have active access to view candidates.": "您可以主动访问查看候选人。", "You will be contacted by your consultant.": "您的顾问将与您联系。", "You've completed all required fields. However, filling in all sections will help us find better matches for you": "You've completed all required fields. However, filling in all sections will help us find better matches for you", - "Your Hobbies and Main Interests": "你的爱好和主要兴趣", + "Your Hobbies and Main Interests": "您的爱好和主要兴趣", "Your Personality Traits": "你的性格特征", "Your details are only used for the matching process.": "Your details are only used for the matching process.", "Your information is kept strictly confidential.": "Your information is kept strictly confidential.", @@ -807,5 +807,6 @@ "Enter medication name and reason for use...": "输入药物名称和使用原因...", "نام دارو و دلیل مصرف را وارد نمایید...": "输入药物名称和使用原因...", "وضعیت سلامت جسمانی": "身体健康状况", - "توضیحات وضعیت جسمانی": "身体健康说明" + "توضیحات وضعیت جسمانی": "身体健康说明", + "Name": "姓名" } \ No newline at end of file diff --git a/src/translations/provider.tsx b/src/translations/provider.tsx index f1dad0b..aa6bd1b 100644 --- a/src/translations/provider.tsx +++ b/src/translations/provider.tsx @@ -1,8 +1,9 @@ "use client"; -import { createContext, type ReactNode, useContext, useMemo } from "react"; +import { createContext, type ReactNode, useContext, useEffect, useMemo } from "react"; import { defaultLocale, type Locale } from "@/translations/config"; import { type Dictionary, getDictionary } from "@/translations/dictionaries"; +import { getUserGeoRegion } from "@/lib/geo-region"; type I18nContextValue = { locale: Locale; @@ -20,6 +21,10 @@ type I18nProviderProps = { }; export function I18nProvider({ children, locale }: I18nProviderProps) { + useEffect(() => { + void getUserGeoRegion(true); + }, []); + const value = useMemo( () => ({ locale,