|
|
|
@ -2,7 +2,11 @@ |
|
|
|
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react"; |
|
|
|
import { createPortal } from "react-dom"; |
|
|
|
import { getCountryList, resolveCountryName, isKnownCountry } from "@/data/countries"; |
|
|
|
import { |
|
|
|
getCountryList, |
|
|
|
resolveCountryName, |
|
|
|
isKnownCountry, |
|
|
|
} from "@/data/countries"; |
|
|
|
import type { QuestionField } from "@/lib/schema-adapter"; |
|
|
|
import { useI18n } from "@/translations/provider"; |
|
|
|
import { useQuestionAnswers } from "./question-answer-storage"; |
|
|
|
@ -10,7 +14,11 @@ 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"; |
|
|
|
import { |
|
|
|
getUserGeoRegion, |
|
|
|
getStoredUserGeoRegion, |
|
|
|
subscribeToUserGeoRegion, |
|
|
|
} from "@/lib/geo-region"; |
|
|
|
|
|
|
|
const EXIT_ANIMATION_MS = 220; |
|
|
|
|
|
|
|
@ -24,16 +32,25 @@ type BirthplaceValue = { |
|
|
|
city?: string; |
|
|
|
}; |
|
|
|
|
|
|
|
export function parseValue(rawValue: unknown): { country: string; city: string } { |
|
|
|
export function parseValue(rawValue: unknown): { |
|
|
|
country: string; |
|
|
|
city: string; |
|
|
|
} { |
|
|
|
if (!rawValue) return { country: "", city: "" }; |
|
|
|
|
|
|
|
if (typeof rawValue === "object" && rawValue !== null) { |
|
|
|
const obj = rawValue as BirthplaceValue; |
|
|
|
const rawCountry = typeof obj.country === "string" ? obj.country.trim() : ""; |
|
|
|
const rawCountry = |
|
|
|
typeof obj.country === "string" ? obj.country.trim() : ""; |
|
|
|
const rawCity = typeof obj.city === "string" ? obj.city.trim() : ""; |
|
|
|
|
|
|
|
// If obj has country and city inverted (e.g. { country: "Mashhad", city: "Iran" })
|
|
|
|
if (rawCountry && rawCity && !isKnownCountry(rawCountry) && isKnownCountry(rawCity)) { |
|
|
|
if ( |
|
|
|
rawCountry && |
|
|
|
rawCity && |
|
|
|
!isKnownCountry(rawCountry) && |
|
|
|
isKnownCountry(rawCity) |
|
|
|
) { |
|
|
|
return { |
|
|
|
country: rawCity, |
|
|
|
city: rawCountry, |
|
|
|
@ -146,22 +163,31 @@ export function QuestionBirthplace({ |
|
|
|
const [mode, setMode] = useState<"auto" | "manual">(() => { |
|
|
|
if (typeof window !== "undefined") { |
|
|
|
const stored = localStorage.getItem(`residence_mode_${question.id}`); |
|
|
|
if (stored === "manual" || stored === "auto") return stored; |
|
|
|
if (stored === "manual") return "manual"; |
|
|
|
if (stored === "auto") return "auto"; |
|
|
|
} |
|
|
|
return "auto"; |
|
|
|
}); |
|
|
|
|
|
|
|
const isInitialManual = mode === "manual"; |
|
|
|
|
|
|
|
const defaultCountryFallback = |
|
|
|
isResidence && locale === "fa" |
|
|
|
? resolveCountryName("IR", "fa") || "ایران" |
|
|
|
: ""; |
|
|
|
|
|
|
|
const localizedInitialCountry = |
|
|
|
resolveCountryName(initial.country, locale) || |
|
|
|
initial.country || |
|
|
|
(!hasSavedAnswer && !isInitialManual && storedRegion?.country |
|
|
|
? resolveCountryName(storedRegion.country, locale) || storedRegion.country |
|
|
|
: ""); |
|
|
|
: defaultCountryFallback); |
|
|
|
|
|
|
|
const initialCity = |
|
|
|
initial.city || (!hasSavedAnswer && !isInitialManual && storedRegion?.city ? storedRegion.city : ""); |
|
|
|
initial.city || |
|
|
|
(!hasSavedAnswer && !isInitialManual && storedRegion?.city |
|
|
|
? storedRegion.city |
|
|
|
: ""); |
|
|
|
|
|
|
|
const initialLoc = |
|
|
|
localizedInitialCountry || initialCity |
|
|
|
@ -171,9 +197,7 @@ export function QuestionBirthplace({ |
|
|
|
const [selectedCountry, setSelectedCountry] = useState( |
|
|
|
() => localizedInitialCountry || "", |
|
|
|
); |
|
|
|
const [cityInput, setCityInput] = useState( |
|
|
|
() => initialCity, |
|
|
|
); |
|
|
|
const [cityInput, setCityInput] = useState(() => initialCity); |
|
|
|
|
|
|
|
const cityInputStateRef = useRef(initialCity); |
|
|
|
const selectedCountryStateRef = useRef(localizedInitialCountry || ""); |
|
|
|
@ -245,16 +269,63 @@ export function QuestionBirthplace({ |
|
|
|
|
|
|
|
const lastInternalAnswerRef = useRef<BirthplaceValue | string | null>(null); |
|
|
|
|
|
|
|
const updateAnswers = (country: string, city: string) => { |
|
|
|
const cleanCountry = country?.trim() || ""; |
|
|
|
const cleanCity = city?.trim() || ""; |
|
|
|
const payload = |
|
|
|
cleanCountry || cleanCity |
|
|
|
? { country: cleanCountry, city: cleanCity } |
|
|
|
: null; |
|
|
|
lastInternalAnswerRef.current = payload; |
|
|
|
setAnswerValue(question, payload); |
|
|
|
}; |
|
|
|
const updateAnswers = useCallback( |
|
|
|
(country: string, city: string) => { |
|
|
|
const cleanCountry = country?.trim() || ""; |
|
|
|
const cleanCity = city?.trim() || ""; |
|
|
|
const payload = |
|
|
|
cleanCountry || cleanCity |
|
|
|
? { country: cleanCountry, city: cleanCity } |
|
|
|
: null; |
|
|
|
lastInternalAnswerRef.current = payload; |
|
|
|
setAnswerValue(question, payload); |
|
|
|
}, |
|
|
|
[question, setAnswerValue], |
|
|
|
); |
|
|
|
|
|
|
|
// Subscribe to live geo region updates (e.g. when Flutter bridge responds asynchronously)
|
|
|
|
useEffect(() => { |
|
|
|
if (!isResidence) return; |
|
|
|
|
|
|
|
const unsubscribe = subscribeToUserGeoRegion((region) => { |
|
|
|
if (!isMountedRef.current) return; |
|
|
|
// If user has already switched to manual mode, do not overwrite manual edits
|
|
|
|
const currentStoredMode = |
|
|
|
typeof window !== "undefined" |
|
|
|
? localStorage.getItem(`residence_mode_${question.id}`) |
|
|
|
: null; |
|
|
|
if (currentStoredMode === "manual" || mode === "manual") return; |
|
|
|
|
|
|
|
const rawCountry = region.country || region.countryCode || ""; |
|
|
|
const country = |
|
|
|
resolveCountryName(rawCountry, locale) || |
|
|
|
rawCountry || |
|
|
|
defaultCountryFallback; |
|
|
|
const city = region.city || ""; |
|
|
|
|
|
|
|
if (country || city) { |
|
|
|
setSelectedCountry(country); |
|
|
|
selectedCountryStateRef.current = country; |
|
|
|
setCityInput(city); |
|
|
|
cityInputStateRef.current = city; |
|
|
|
const loc = [country, city].filter(Boolean).join(", "); |
|
|
|
setDetectedLocation(loc); |
|
|
|
updateAnswers(country, city); |
|
|
|
setIsDetecting(false); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
return () => { |
|
|
|
unsubscribe(); |
|
|
|
}; |
|
|
|
}, [ |
|
|
|
isResidence, |
|
|
|
mode, |
|
|
|
locale, |
|
|
|
question.id, |
|
|
|
defaultCountryFallback, |
|
|
|
updateAnswers, |
|
|
|
]); |
|
|
|
|
|
|
|
// GeoIP detection logic using unified getUserGeoRegion
|
|
|
|
const detectLocation = useCallback( |
|
|
|
@ -298,7 +369,10 @@ export function QuestionBirthplace({ |
|
|
|
|
|
|
|
const city = region.city || ""; |
|
|
|
const rawCountry = region.country || region.countryCode || ""; |
|
|
|
const country = resolveCountryName(rawCountry, locale) || rawCountry; |
|
|
|
const country = |
|
|
|
resolveCountryName(rawCountry, locale) || |
|
|
|
rawCountry || |
|
|
|
defaultCountryFallback; |
|
|
|
|
|
|
|
if (country || city) { |
|
|
|
setSelectedCountry(country); |
|
|
|
@ -308,29 +382,19 @@ export function QuestionBirthplace({ |
|
|
|
const loc = [country, city].filter(Boolean).join(", "); |
|
|
|
setDetectedLocation(loc); |
|
|
|
updateAnswers(country, city); |
|
|
|
setMode("auto"); |
|
|
|
if (typeof window !== "undefined") { |
|
|
|
localStorage.setItem(`residence_mode_${question.id}`, "auto"); |
|
|
|
} |
|
|
|
} else { |
|
|
|
setMode("manual"); |
|
|
|
if (typeof window !== "undefined") { |
|
|
|
localStorage.setItem(`residence_mode_${question.id}`, "manual"); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch { |
|
|
|
if (isMountedRef.current) { |
|
|
|
setMode("manual"); |
|
|
|
} |
|
|
|
// Keep in auto mode on error, do not force manual
|
|
|
|
} finally { |
|
|
|
if (isMountedRef.current) { |
|
|
|
setIsDetecting(false); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
[rawValue, locale, question, setAnswerValue], |
|
|
|
[rawValue, locale, question.id, defaultCountryFallback, updateAnswers], |
|
|
|
); |
|
|
|
|
|
|
|
// Auto-detect and pre-fill on initial mount
|
|
|
|
useEffect(() => { |
|
|
|
if (isLoading) return; |
|
|
|
if (isResidence && !hasAutoDetectedRef.current) { |
|
|
|
@ -345,6 +409,11 @@ export function QuestionBirthplace({ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Pre-fill answer immediately if initial values exist and no answer recorded yet
|
|
|
|
if (!hasSavedAnswer && (localizedInitialCountry || initialCity)) { |
|
|
|
updateAnswers(localizedInitialCountry, initialCity); |
|
|
|
} |
|
|
|
|
|
|
|
const parsed = parseValue(rawValue); |
|
|
|
if (!parsed.country && !parsed.city) { |
|
|
|
void detectLocation(false); |
|
|
|
@ -352,7 +421,17 @@ export function QuestionBirthplace({ |
|
|
|
void detectLocation(false); |
|
|
|
} |
|
|
|
} |
|
|
|
}, [isResidence, isLoading, detectLocation, rawValue, question.id]); |
|
|
|
}, [ |
|
|
|
isResidence, |
|
|
|
isLoading, |
|
|
|
detectLocation, |
|
|
|
rawValue, |
|
|
|
question.id, |
|
|
|
hasSavedAnswer, |
|
|
|
localizedInitialCountry, |
|
|
|
initialCity, |
|
|
|
updateAnswers, |
|
|
|
]); |
|
|
|
|
|
|
|
const handleAutoClick = () => { |
|
|
|
if (typeof window !== "undefined") { |
|
|
|
@ -371,7 +450,8 @@ export function QuestionBirthplace({ |
|
|
|
const resolvedC = |
|
|
|
resolveCountryName(selectedCountry || parsed.country, locale) || |
|
|
|
selectedCountry || |
|
|
|
parsed.country; |
|
|
|
parsed.country || |
|
|
|
defaultCountryFallback; |
|
|
|
const country = resolvedC; |
|
|
|
const city = cityInput !== "" ? cityInput : parsed.city; |
|
|
|
setSelectedCountry(country); |
|
|
|
@ -406,8 +486,9 @@ export function QuestionBirthplace({ |
|
|
|
} |
|
|
|
} |
|
|
|
const updated = parseValue(rawValue); |
|
|
|
const resolvedC = resolveCountryName(updated.country, locale) || updated.country; |
|
|
|
if (resolvedC !== selectedCountry) { |
|
|
|
const resolvedC = |
|
|
|
resolveCountryName(updated.country, locale) || updated.country; |
|
|
|
if (resolvedC && resolvedC !== selectedCountry) { |
|
|
|
setSelectedCountry(resolvedC); |
|
|
|
selectedCountryStateRef.current = resolvedC; |
|
|
|
} |
|
|
|
@ -419,7 +500,7 @@ export function QuestionBirthplace({ |
|
|
|
setDetectedLocation([resolvedC, updated.city].filter(Boolean).join(", ")); |
|
|
|
} |
|
|
|
lastInternalAnswerRef.current = rawValue as BirthplaceValue | string | null; |
|
|
|
}, [rawValue, locale]); |
|
|
|
}, [rawValue, locale, selectedCountry, cityInput]); |
|
|
|
|
|
|
|
const options = getCountryList(locale); |
|
|
|
const filteredOptions = options.filter((option) => |
|
|
|
@ -434,7 +515,9 @@ export function QuestionBirthplace({ |
|
|
|
selectedCountryStateRef.current = country; |
|
|
|
closeSheet(); |
|
|
|
updateAnswers(country, cityInputStateRef.current); |
|
|
|
setDetectedLocation([country, cityInputStateRef.current].filter(Boolean).join(", ")); |
|
|
|
setDetectedLocation( |
|
|
|
[country, cityInputStateRef.current].filter(Boolean).join(", "), |
|
|
|
); |
|
|
|
window.setTimeout(() => { |
|
|
|
cityInputRef.current?.focus({ preventScroll: true }); |
|
|
|
}, EXIT_ANIMATION_MS); |
|
|
|
@ -447,7 +530,9 @@ export function QuestionBirthplace({ |
|
|
|
const newCity = e.target.value; |
|
|
|
cityInputStateRef.current = newCity; |
|
|
|
setCityInput(newCity); |
|
|
|
setDetectedLocation([selectedCountryStateRef.current, newCity].filter(Boolean).join(", ")); |
|
|
|
setDetectedLocation( |
|
|
|
[selectedCountryStateRef.current, newCity].filter(Boolean).join(", "), |
|
|
|
); |
|
|
|
|
|
|
|
if (debounceTimerRef.current) { |
|
|
|
clearTimeout(debounceTimerRef.current); |
|
|
|
|