diff --git a/src/components/Componentes/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx index 25a706e..6533db8 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, resolveCountryName, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries"; +import { getCountryList, resolveCountryName, isKnownCountry, 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"; @@ -30,13 +30,15 @@ function parseValue(rawValue: unknown): { country: string; city: string } { if (typeof rawValue === "object" && rawValue !== null) { const obj = rawValue as BirthplaceValue; return { - country: typeof obj.country === "string" ? obj.country : "", - city: typeof obj.city === "string" ? obj.city : "", + country: typeof obj.country === "string" ? obj.country.trim() : "", + city: typeof obj.city === "string" ? obj.city.trim() : "", }; } if (typeof rawValue === "string") { let str = rawValue.trim(); + if (!str) return { country: "", city: "" }; + // Clean legacy country names with parentheses containing commas str = str.replace( "United Kingdom (UK, England, Wales, Scotland, Northern Ireland)", @@ -63,16 +65,33 @@ function parseValue(rawValue: unknown): { country: string; city: string } { "ایالات متحده آمریکا (US)", ); + let parts: string[] = []; if (str.includes(",")) { - const parts = str.split(",").map((s) => s.trim()); - return { city: parts[0] || "", country: parts[1] || "" }; + parts = str.split(",").map((s) => s.trim()); + } else if (str.includes("،")) { + parts = str.split("،").map((s) => s.trim()); + } else if (str.includes(" - ")) { + parts = str.split(" - ").map((s) => s.trim()); } - if (str.includes(" - ")) { - const parts = str.split(" - ").map((s) => s.trim()); - return { country: parts[0] || "", city: parts[1] || "" }; + + if (parts.length >= 2) { + const part0 = parts[0] || ""; + const part1 = parts.slice(1).join(", ").trim(); + + const isPart0Country = isKnownCountry(part0); + const isPart1Country = isKnownCountry(part1); + + if (isPart0Country && !isPart1Country) { + return { country: part0, city: part1 }; + } + if (isPart1Country && !isPart0Country) { + return { country: part1, city: part0 }; + } + // Default standard order: City first, Country second + return { city: part0, country: part1 }; } - const isCountry = COUNTRIES_EN.includes(str) || COUNTRIES_FA.includes(str); - if (isCountry) { + + if (isKnownCountry(str)) { return { country: str, city: "" }; } return { country: "", city: str }; @@ -120,6 +139,7 @@ export function QuestionBirthplace({ const [mode, setMode] = useState<"auto" | "manual">("auto"); const [isDetecting, setIsDetecting] = useState(false); const [detectedLocation, setDetectedLocation] = useState(initialLoc); + const hasAutoDetectedRef = useRef(false); useEffect(() => { isMountedRef.current = true; @@ -163,8 +183,12 @@ export function QuestionBirthplace({ }, [isOpen, closeSheet]); const updateAnswers = (country: string, city: string) => { + const cleanCountry = country?.trim() || ""; + const cleanCity = city?.trim() || ""; const formatted = - city && country ? `${city}, ${country}` : city || country || null; + cleanCity && cleanCountry + ? `${cleanCity}, ${cleanCountry}` + : cleanCity || cleanCountry || null; setAnswerValue(question, formatted); }; @@ -176,8 +200,8 @@ export function QuestionBirthplace({ const cName = resolveCountryName(parsed.country, locale); if (cName || parsed.city) { const loc = [parsed.city, cName].filter(Boolean).join(", "); - setSelectedCountry(cName); - setCityInput(parsed.city); + if (cName) setSelectedCountry(cName); + if (parsed.city) setCityInput(parsed.city); setDetectedLocation(loc); setMode("auto"); return; @@ -217,7 +241,8 @@ export function QuestionBirthplace({ useEffect(() => { if (isLoading) return; - if (isResidence) { + if (isResidence && !hasAutoDetectedRef.current) { + hasAutoDetectedRef.current = true; void detectLocation(); } }, [isResidence, isLoading, detectLocation]); @@ -248,7 +273,7 @@ export function QuestionBirthplace({ if (resolvedC && resolvedC !== selectedCountry) { setSelectedCountry(resolvedC); } - if (updated.city.trim() !== cityInput.trim()) { + if (updated.city && updated.city.trim() !== cityInput.trim()) { setCityInput(updated.city); } if (resolvedC || updated.city) { diff --git a/src/components/Componentes/question-number.tsx b/src/components/Componentes/question-number.tsx index 597a342..479cf50 100644 --- a/src/components/Componentes/question-number.tsx +++ b/src/components/Componentes/question-number.tsx @@ -6,6 +6,7 @@ import { useI18n } from "@/translations/provider"; import { useQuestionAnswers } from "./question-answer-storage"; import QuestionTitle from "./question-title"; import { Input } from "@/components/ui/input"; +import { isKnownCountry } from "@/data/countries"; type QuestionNumberProps = { question: QuestionField; @@ -466,8 +467,13 @@ function getCountryFromStorage(): string { ); const value = field?.value; if (typeof value === "string") { - const parts = value.split(","); - return parts[0]?.trim() || ""; + const parts = value.split(",").map((p) => p.trim()); + if (parts.length >= 2) { + if (isKnownCountry(parts[1])) return parts[1]; + if (isKnownCountry(parts[0])) return parts[0]; + return parts[1]; + } + return parts[0] || ""; } } catch { // Ignore diff --git a/src/components/Componentes/ui-config.test.tsx b/src/components/Componentes/ui-config.test.tsx index ff72739..e0a2e17 100644 --- a/src/components/Componentes/ui-config.test.tsx +++ b/src/components/Componentes/ui-config.test.tsx @@ -280,4 +280,72 @@ describe("UI Config based behavior", () => { expect(document.body.classList.contains("dropdown-open")).toBe(false); }); }); + + it("should preserve country when typing city after selecting country", async () => { + const qBirthplace = { + id: "birthplace_select_first", + title: "محل تولد", + type: "birthplace", + order: 1, + required: true, + isVisible: true, + description: "", + tooltip: "", + extras: { placeHolder: "شهر، منطقه یا محله" }, + options: [], + ui_config: {}, + } as any; + + render( + + + + + , + ); + + // 1. Open country dropdown first + const countryButton = screen.getByText("انتخاب کشور"); + fireEvent.click(countryButton); + + // 2. Select Germany (آلمان) + const germanyOption = screen.getByRole("button", { name: "آلمان" }); + fireEvent.click(germanyOption); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + // Country button should show Germany + expect(screen.getByText("آلمان")).toBeDefined(); + + // 3. Type city in the input field + const cityInput = screen.getByPlaceholderText("شهر، منطقه یا محله"); + fireEvent.change(cityInput, { target: { value: "برلین" } }); + + // Country button MUST still show Germany and city must show Berlin + expect(screen.getByText("آلمان")).toBeDefined(); + expect((cityInput as HTMLInputElement).value).toBe("برلین"); + + // 4. Type more characters into city field + fireEvent.change(cityInput, { target: { value: "برلین مرکزی" } }); + expect(screen.getByText("آلمان")).toBeDefined(); + expect((cityInput as HTMLInputElement).value).toBe("برلین مرکزی"); + + // 5. Change country to France (فرانسه) + const updatedCountryButton = screen.getByText("آلمان"); + fireEvent.click(updatedCountryButton); + + const franceOption = screen.getByRole("button", { name: "فرانسه" }); + fireEvent.click(franceOption); + + await waitFor(() => { + expect(screen.queryByRole("dialog")).toBeNull(); + }); + + expect(screen.getByText("فرانسه")).toBeDefined(); + expect((cityInput as HTMLInputElement).value).toBe("برلین مرکزی"); + }); }); + + diff --git a/src/data/countries.ts b/src/data/countries.ts index fa74570..234d22b 100644 --- a/src/data/countries.ts +++ b/src/data/countries.ts @@ -257,7 +257,7 @@ export const COUNTRIES_FA = [ "فنلاند", "فرانسه", "گابن", - "Gambia", + "گامبیا", "گرجستان", "آلمان", "غنا", @@ -448,3 +448,23 @@ export function resolveCountryName( return trimmed; } + +export function isKnownCountry(countryOrCode: string | undefined | null): boolean { + if (!countryOrCode) return false; + const trimmed = countryOrCode.trim(); + if (!trimmed) return false; + + if (trimmed.length === 2 && /^[a-zA-Z]{2}$/.test(trimmed)) { + return true; + } + + const lower = trimmed.toLowerCase(); + if (COUNTRIES_EN.some((c) => c.toLowerCase() === lower)) { + return true; + } + if (COUNTRIES_FA.some((c) => c.toLowerCase() === lower)) { + return true; + } + + return false; +}