You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
836 lines
29 KiB
836 lines
29 KiB
"use client";
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
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";
|
|
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,
|
|
subscribeToUserGeoRegion,
|
|
} from "@/lib/geo-region";
|
|
import {
|
|
isInFlutterWebView,
|
|
requestAutoLocation,
|
|
pickManualLocation,
|
|
} from "@/lib/webview-actions";
|
|
|
|
const EXIT_ANIMATION_MS = 220;
|
|
|
|
type QuestionBirthplaceProps = {
|
|
question: QuestionField;
|
|
disabled?: boolean;
|
|
};
|
|
|
|
type BirthplaceValue = {
|
|
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 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)
|
|
) {
|
|
return {
|
|
country: rawCity,
|
|
city: rawCountry,
|
|
};
|
|
}
|
|
// If only country is provided but it is actually a city
|
|
if (rawCountry && !rawCity && !isKnownCountry(rawCountry)) {
|
|
return {
|
|
country: "",
|
|
city: rawCountry,
|
|
};
|
|
}
|
|
// If only city is provided but it is actually a country
|
|
if (!rawCountry && rawCity && isKnownCountry(rawCity)) {
|
|
return {
|
|
country: rawCity,
|
|
city: "",
|
|
};
|
|
}
|
|
|
|
return {
|
|
country: rawCountry,
|
|
city: rawCity,
|
|
};
|
|
}
|
|
|
|
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)",
|
|
"United Kingdom (UK)",
|
|
);
|
|
str = str.replace("United States (US, USA, America)", "United States (US)");
|
|
str = str.replace(
|
|
"United Arab Emirates (UAE, Dubai, Abu Dhabi)",
|
|
"United Arab Emirates (UAE)",
|
|
);
|
|
str = str.replace("Congo (Congo-Brazzaville)", "Congo");
|
|
str = str.replace("Czechia (Czech Republic)", "Czechia");
|
|
str = str.replace("Myanmar (formerly Burma)", "Myanmar");
|
|
str = str.replace(
|
|
"امارات متحده عربی (دبی، ابوظبی، UAE)",
|
|
"امارات متحده عربی (UAE)",
|
|
);
|
|
str = str.replace(
|
|
"بریتانیا (انگلستان، اسکاتلند، ولز، ایرلند شمالی، UK)",
|
|
"بریتانیا (UK)",
|
|
);
|
|
str = str.replace(
|
|
"ایالات متحده آمریکا (آمریکا، US، USA)",
|
|
"ایالات متحده آمریکا (US)",
|
|
);
|
|
|
|
const splitLocation = (part1: string, part2: string) => {
|
|
// 1. If part1 is a known country and part2 is not (or both), part1 is country, part2 is city
|
|
if (isKnownCountry(part1)) {
|
|
return { country: part1, city: part2 };
|
|
}
|
|
// 2. If part1 is not a known country, but part2 is a known country (e.g. "Mashhad, Iran" or "مشهد، ایران")
|
|
if (isKnownCountry(part2)) {
|
|
return { country: part2, city: part1 };
|
|
}
|
|
// 3. Fallback: assume first part is country
|
|
return { country: part1, city: part2 };
|
|
};
|
|
|
|
if (str.includes(",")) {
|
|
const idx = str.indexOf(",");
|
|
return splitLocation(str.slice(0, idx).trim(), str.slice(idx + 1).trim());
|
|
}
|
|
if (str.includes("،")) {
|
|
const idx = str.indexOf("،");
|
|
return splitLocation(str.slice(0, idx).trim(), str.slice(idx + 1).trim());
|
|
}
|
|
if (str.includes(" - ")) {
|
|
const idx = str.indexOf(" - ");
|
|
return splitLocation(str.slice(0, idx).trim(), str.slice(idx + 3).trim());
|
|
}
|
|
|
|
if (isKnownCountry(str)) {
|
|
return { country: str, city: "" };
|
|
}
|
|
return { country: "", city: str };
|
|
}
|
|
|
|
return { country: "", city: "" };
|
|
}
|
|
|
|
export function QuestionBirthplace({
|
|
question,
|
|
disabled,
|
|
}: QuestionBirthplaceProps) {
|
|
const { dictionary: t, locale } = useI18n();
|
|
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 hasSavedAnswer = Boolean(
|
|
initial.country?.trim() ||
|
|
initial.city?.trim() ||
|
|
(typeof rawValue === "string" && rawValue.trim().length > 0),
|
|
);
|
|
|
|
const [mode, setMode] = useState<"auto" | "manual">(() => {
|
|
if (typeof window !== "undefined") {
|
|
const stored = localStorage.getItem(`residence_mode_${question.id}`);
|
|
if (stored === "manual") return "manual";
|
|
if (stored === "auto") return "auto";
|
|
}
|
|
return "auto";
|
|
});
|
|
|
|
const isInitialManual = mode === "manual";
|
|
|
|
const localizedInitialCountry = hasSavedAnswer
|
|
? resolveCountryName(initial.country, locale) || initial.country
|
|
: "";
|
|
|
|
const initialCity = hasSavedAnswer ? initial.city || "" : "";
|
|
|
|
const initialLoc =
|
|
localizedInitialCountry || initialCity
|
|
? [localizedInitialCountry, initialCity].filter(Boolean).join(", ")
|
|
: "";
|
|
|
|
const [selectedCountry, setSelectedCountry] = useState(
|
|
() => localizedInitialCountry || "",
|
|
);
|
|
const [cityInput, setCityInput] = useState(() => initialCity);
|
|
|
|
const cityInputStateRef = useRef(initialCity);
|
|
const selectedCountryStateRef = useRef(localizedInitialCountry || "");
|
|
|
|
const lastCoordsRef = useRef<{ latitude?: number; longitude?: number } | undefined>(
|
|
storedRegion?.latitude && storedRegion?.longitude
|
|
? { latitude: storedRegion.latitude, longitude: storedRegion.longitude }
|
|
: undefined,
|
|
);
|
|
|
|
useEffect(() => {
|
|
cityInputStateRef.current = cityInput;
|
|
}, [cityInput]);
|
|
|
|
useEffect(() => {
|
|
selectedCountryStateRef.current = selectedCountry;
|
|
}, [selectedCountry]);
|
|
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [isClosing, setIsClosing] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const cityInputRef = useRef<HTMLInputElement>(null);
|
|
const listRef = useRef<HTMLDivElement>(null);
|
|
const isMountedRef = useRef(true);
|
|
const isFocusedRef = useRef(false);
|
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const [isDetecting, setIsDetecting] = useState(false);
|
|
const [detectedLocation, setDetectedLocation] = useState(initialLoc);
|
|
|
|
useEffect(() => {
|
|
isMountedRef.current = true;
|
|
return () => {
|
|
isMountedRef.current = false;
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const closeSheet = useCallback(() => {
|
|
if (isClosing) return;
|
|
setIsClosing(true);
|
|
window.setTimeout(() => {
|
|
if (isMountedRef.current) {
|
|
setIsOpen(false);
|
|
setIsClosing(false);
|
|
setSearchQuery("");
|
|
}
|
|
}, EXIT_ANIMATION_MS);
|
|
}, [isClosing]);
|
|
|
|
const openSheet = useCallback(() => {
|
|
if (disabled) return;
|
|
setIsOpen(true);
|
|
setIsClosing(false);
|
|
}, [disabled]);
|
|
|
|
useSheetScrollLock(isOpen, { onBack: closeSheet });
|
|
|
|
// Handle escape key
|
|
useEffect(() => {
|
|
if (!isOpen) return;
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") {
|
|
closeSheet();
|
|
}
|
|
};
|
|
|
|
window.addEventListener("keydown", handleKeyDown);
|
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
}, [isOpen, closeSheet]);
|
|
|
|
const lastInternalAnswerRef = useRef<BirthplaceValue | string | null>(null);
|
|
|
|
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],
|
|
);
|
|
|
|
const handleAutoClick = async () => {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(`residence_mode_${question.id}`, "auto");
|
|
}
|
|
setMode("auto");
|
|
setIsDetecting(true);
|
|
|
|
try {
|
|
if (isInFlutterWebView()) {
|
|
const data = await requestAutoLocation();
|
|
if (!isMountedRef.current) return;
|
|
lastCoordsRef.current = {
|
|
latitude: data.latitude,
|
|
longitude: data.longitude,
|
|
};
|
|
const rawCountry = data.country || data.country_code || "";
|
|
const country =
|
|
resolveCountryName(rawCountry, locale) || rawCountry || "";
|
|
const city = data.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);
|
|
}
|
|
} else {
|
|
const region = await getUserGeoRegion(true);
|
|
if (!isMountedRef.current) return;
|
|
const city = region.city || "";
|
|
const rawCountry = region.country || region.countryCode || "";
|
|
const country =
|
|
resolveCountryName(rawCountry, locale) || rawCountry || "";
|
|
|
|
if (region.latitude && region.longitude) {
|
|
lastCoordsRef.current = {
|
|
latitude: region.latitude,
|
|
longitude: region.longitude,
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn("Auto location error:", err);
|
|
} finally {
|
|
if (isMountedRef.current) {
|
|
setIsDetecting(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleManualClick = async () => {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(`residence_mode_${question.id}`, "manual");
|
|
}
|
|
setMode("manual");
|
|
|
|
if (isInFlutterWebView()) {
|
|
try {
|
|
const data = await pickManualLocation(lastCoordsRef.current);
|
|
if (data && isMountedRef.current) {
|
|
lastCoordsRef.current = {
|
|
latitude: data.latitude,
|
|
longitude: data.longitude,
|
|
};
|
|
const rawCountry = data.country || data.country_code || "";
|
|
const country =
|
|
resolveCountryName(rawCountry, locale) || rawCountry || "";
|
|
const city = data.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);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn("Manual map pick error:", err);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Synchronize state ONLY if rawValue changes externally (e.g. draft fetch or reset)
|
|
useEffect(() => {
|
|
if (isFocusedRef.current) {
|
|
return;
|
|
}
|
|
if (rawValue === lastInternalAnswerRef.current) {
|
|
return;
|
|
}
|
|
if (
|
|
typeof rawValue === "object" &&
|
|
rawValue !== null &&
|
|
typeof lastInternalAnswerRef.current === "object" &&
|
|
lastInternalAnswerRef.current !== null
|
|
) {
|
|
const currentObj = lastInternalAnswerRef.current as BirthplaceValue;
|
|
const rawObj = rawValue as BirthplaceValue;
|
|
if (
|
|
(rawObj.country?.trim() || "") === (currentObj.country?.trim() || "") &&
|
|
(rawObj.city?.trim() || "") === (currentObj.city?.trim() || "")
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
const updated = parseValue(rawValue);
|
|
const resolvedC =
|
|
resolveCountryName(updated.country, locale) || updated.country;
|
|
if (resolvedC && resolvedC !== selectedCountry) {
|
|
setSelectedCountry(resolvedC);
|
|
selectedCountryStateRef.current = resolvedC;
|
|
}
|
|
if (updated.city !== cityInput) {
|
|
setCityInput(updated.city);
|
|
cityInputStateRef.current = updated.city;
|
|
}
|
|
if (resolvedC || updated.city) {
|
|
setDetectedLocation([resolvedC, updated.city].filter(Boolean).join(", "));
|
|
}
|
|
lastInternalAnswerRef.current = rawValue as BirthplaceValue | string | null;
|
|
}, [rawValue, locale, selectedCountry, cityInput]);
|
|
|
|
const options = getCountryList(locale);
|
|
const filteredOptions = options.filter((option) =>
|
|
option.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
);
|
|
|
|
const handleSelectCountry = (country: string) => {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(`residence_mode_${question.id}`, "manual");
|
|
}
|
|
setSelectedCountry(country);
|
|
selectedCountryStateRef.current = country;
|
|
closeSheet();
|
|
updateAnswers(country, cityInputStateRef.current);
|
|
setDetectedLocation(
|
|
[country, cityInputStateRef.current].filter(Boolean).join(", "),
|
|
);
|
|
window.setTimeout(() => {
|
|
cityInputRef.current?.focus({ preventScroll: true });
|
|
}, EXIT_ANIMATION_MS);
|
|
};
|
|
|
|
const handleCityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (typeof window !== "undefined") {
|
|
localStorage.setItem(`residence_mode_${question.id}`, "manual");
|
|
}
|
|
const newCity = e.target.value;
|
|
cityInputStateRef.current = newCity;
|
|
setCityInput(newCity);
|
|
setDetectedLocation(
|
|
[selectedCountryStateRef.current, newCity].filter(Boolean).join(", "),
|
|
);
|
|
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
debounceTimerRef.current = setTimeout(() => {
|
|
updateAnswers(selectedCountryStateRef.current, newCity);
|
|
}, 200);
|
|
};
|
|
|
|
const handleCityFocus = () => {
|
|
isFocusedRef.current = true;
|
|
};
|
|
|
|
const handleCityBlur = () => {
|
|
isFocusedRef.current = false;
|
|
if (debounceTimerRef.current) {
|
|
clearTimeout(debounceTimerRef.current);
|
|
}
|
|
updateAnswers(selectedCountryStateRef.current, cityInputStateRef.current);
|
|
};
|
|
|
|
const isRtl = locale === "fa" || locale === "ar" || locale === "ur";
|
|
const selectCountryPlaceholder = isRtl ? "انتخاب کشور" : "Select country";
|
|
const cityPlaceholder =
|
|
question.extras?.placeHolder ||
|
|
(isRtl ? "شهر، منطقه یا محله" : "City, region, or neighborhood");
|
|
|
|
const searchPlaceholder =
|
|
locale === "fa"
|
|
? "جستجو..."
|
|
: locale === "ar"
|
|
? "بحث..."
|
|
: locale === "tr"
|
|
? "Ara..."
|
|
: "Search...";
|
|
|
|
const noResultsText =
|
|
locale === "fa"
|
|
? "موردی یافت نشد"
|
|
: locale === "ar"
|
|
? "لم يتم العثور على نتائج"
|
|
: "No options found";
|
|
|
|
return (
|
|
<div
|
|
className={[
|
|
"relative flex w-full flex-col gap-3 transition-opacity duration-200",
|
|
disabled ? "pointer-events-none opacity-30" : "",
|
|
].join(" ")}
|
|
>
|
|
<QuestionTitle question={question} />
|
|
|
|
{isResidence ? (
|
|
<>
|
|
{/* GeoIP Auto/Manual selector UI */}
|
|
<div className="flex flex-col gap-3.5 w-full py-1">
|
|
{/* Left Side: Location Info */}
|
|
<div className="flex items-center gap-2.5 min-w-0">
|
|
<svg
|
|
width="16"
|
|
height="20"
|
|
viewBox="0 0 16 20"
|
|
fill="none"
|
|
className="shrink-0 text-[#181818]"
|
|
>
|
|
<path
|
|
d="M8 0C3.58 0 0 3.58 0 8C0 13.54 8 20 8 20C8 20 16 13.54 16 8C16 3.58 12.42 0 8 0ZM8 11C6.34 11 5 9.66 5 8C5 6.34 6.34 5 8 5C9.66 5 11 6.34 11 8C11 9.66 9.66 11 8 11Z"
|
|
fill="currentColor"
|
|
/>
|
|
</svg>
|
|
<span className="text-[16px] font-bold text-[#181818]">
|
|
{isDetecting
|
|
? locale === "fa"
|
|
? "در حال شناسایی..."
|
|
: "Detecting..."
|
|
: detectedLocation ||
|
|
(locale === "fa" ? "نامشخص" : "Unknown")}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Right Side: Toggle Buttons */}
|
|
<div className="flex items-center gap-2 w-full">
|
|
{/* Auto Button */}
|
|
<button
|
|
type="button"
|
|
onClick={handleAutoClick}
|
|
disabled={isDetecting}
|
|
className={[
|
|
"flex flex-1 items-center justify-center gap-1.5 h-[46px] rounded-xl text-[14px] font-bold cursor-pointer transition-all shadow-sm",
|
|
mode === "auto"
|
|
? "bg-[#F2465F] text-white"
|
|
: "bg-white border border-[#D0D5DD] text-[#344054] hover:bg-gray-50",
|
|
].join(" ")}
|
|
>
|
|
{isDetecting ? (
|
|
<LoadingThreeDot />
|
|
) : (
|
|
<>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
className="shrink-0"
|
|
>
|
|
<circle
|
|
cx="8"
|
|
cy="8"
|
|
r="6"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
/>
|
|
<circle cx="8" cy="8" r="2" fill="currentColor" />
|
|
<path
|
|
d="M8 0V3M8 13V16M0 8H3M13 8H16"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
<span>{locale === "fa" ? "خودکار" : "Auto"}</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
{/* Manual Button */}
|
|
<button
|
|
type="button"
|
|
onClick={handleManualClick}
|
|
className={[
|
|
"flex flex-1 items-center justify-center gap-1.5 h-[46px] rounded-xl text-[14px] font-bold cursor-pointer transition-all shadow-sm",
|
|
mode === "manual"
|
|
? "bg-[#F2465F] text-white"
|
|
: "bg-white border border-[#D0D5DD] text-[#344054] hover:bg-gray-50",
|
|
].join(" ")}
|
|
>
|
|
<svg
|
|
width="16"
|
|
height="16"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
className="shrink-0"
|
|
>
|
|
<path
|
|
d="M1 3.5L5 1.5L11 4.5L15 2.5V12.5L11 14.5L5 11.5L1 13.5V3.5Z"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
<path
|
|
d="M5 1.5V11.5M11 4.5V14.5"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
/>
|
|
</svg>
|
|
<span>{locale === "fa" ? "دستی" : "Manual"}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* 1. Country Selection Trigger */}
|
|
<div className="relative w-full">
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
onClick={openSheet}
|
|
className={[
|
|
"flex h-[54px] w-full items-center justify-between rounded-[16px] border bg-white px-4.5 text-start transition-all cursor-pointer outline-none",
|
|
isOpen
|
|
? "border-[#6F6F6F] ring-1 ring-[#6F6F6F]"
|
|
: "border-[#D0D5DD] hover:border-[#98A2B3]",
|
|
].join(" ")}
|
|
>
|
|
<span
|
|
className={[
|
|
"text-[15px] font-medium truncate",
|
|
selectedCountry ? "text-[#181818]" : "text-[#667085]",
|
|
].join(" ")}
|
|
>
|
|
{selectedCountry || selectCountryPlaceholder}
|
|
</span>
|
|
<svg
|
|
width="16"
|
|
height="10"
|
|
viewBox="0 0 16 10"
|
|
fill="none"
|
|
className={[
|
|
"shrink-0 transition-transform duration-200",
|
|
isOpen ? "rotate-180" : "",
|
|
].join(" ")}
|
|
>
|
|
<path
|
|
d="M14.75 1.25L7.75 8.25L0.75 1.25"
|
|
stroke="#344054"
|
|
strokeWidth="1.75"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* 2. City Text Input */}
|
|
<div className="w-full">
|
|
<input
|
|
ref={cityInputRef}
|
|
type="text"
|
|
data-no-auto-focus
|
|
disabled={disabled}
|
|
value={cityInput}
|
|
onChange={handleCityChange}
|
|
onFocus={handleCityFocus}
|
|
onBlur={handleCityBlur}
|
|
placeholder={cityPlaceholder}
|
|
className="h-[54px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 text-[15px] font-medium text-[#181818] placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] outline-none transition-all"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Country Selection Bottom Sheet Modal */}
|
|
{isOpen &&
|
|
createPortal(
|
|
<div
|
|
className={[
|
|
"fixed inset-0 z-50 flex items-end justify-center transition-all duration-[220ms] animate-in fade-in",
|
|
isClosing ? "bg-black/0 opacity-0" : "bg-black/55 opacity-100",
|
|
].join(" ")}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={selectCountryPlaceholder}
|
|
onWheel={(event) => event.stopPropagation()}
|
|
onTouchStart={(event) => event.stopPropagation()}
|
|
onTouchMove={(event) => event.stopPropagation()}
|
|
onTouchEnd={(event) => event.stopPropagation()}
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) {
|
|
closeSheet();
|
|
}
|
|
}}
|
|
>
|
|
<section
|
|
className={[
|
|
"flex h-[82svh] min-h-[82svh] max-h-[82svh] 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-[220ms] ease-out will-change-transform sm:max-w-[375px] animate-in slide-in-from-bottom",
|
|
isClosing ? "translate-y-full" : "translate-y-0",
|
|
].join(" ")}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Drag Handle Notch */}
|
|
<div className="flex w-full justify-center pt-3 pb-1 cursor-grab">
|
|
<div className="h-1.25 w-10 rounded-full bg-[#D0D5DD]" />
|
|
</div>
|
|
|
|
{/* Header with Title and Close Button */}
|
|
<div className="flex items-center justify-between px-5 pt-2 pb-3 border-b border-[#F2F4F7]">
|
|
<h3 className="text-[17px] font-bold text-[#181818] truncate pr-2">
|
|
{selectCountryPlaceholder}
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={closeSheet}
|
|
className="flex size-8 shrink-0 items-center justify-center rounded-full text-[#667085] hover:bg-[#F2F4F7] hover:text-[#181818] transition-colors cursor-pointer"
|
|
aria-label="Close"
|
|
>
|
|
<svg
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2.2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Search Bar */}
|
|
<div className="px-5 pt-3.5 pb-2">
|
|
<div className="flex h-[46px] w-full items-center gap-2.5 rounded-[14px] bg-[#F2F4F7] px-3.5 transition-colors focus-within:bg-[#EAECF0]">
|
|
<svg
|
|
aria-hidden="true"
|
|
width="18"
|
|
height="18"
|
|
viewBox="0 0 18 18"
|
|
fill="none"
|
|
className="shrink-0 text-[#667085]"
|
|
>
|
|
<path
|
|
d="M8.25 14.25C11.5637 14.25 14.25 11.5637 14.25 8.25C14.25 4.93629 11.5637 2.25 8.25 2.25C4.93629 2.25 2.25 4.93629 2.25 8.25C2.25 11.5637 4.93629 14.25 8.25 14.25Z"
|
|
stroke="#667085"
|
|
strokeWidth="1.75"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
<path
|
|
d="M15.75 15.75L12.5 12.5"
|
|
stroke="#667085"
|
|
strokeWidth="1.75"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder={searchPlaceholder}
|
|
className="flex-1 bg-transparent text-[14px] font-medium text-[#181818] outline-none placeholder:text-[#98A2B3]"
|
|
/>
|
|
{searchQuery ? (
|
|
<button
|
|
type="button"
|
|
onClick={() => setSearchQuery("")}
|
|
className="text-[#667085] hover:text-[#181818] text-xs font-semibold p-1 cursor-pointer"
|
|
>
|
|
✕
|
|
</button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Country Options List */}
|
|
<div
|
|
ref={listRef}
|
|
className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overscroll-contain px-5 py-3"
|
|
>
|
|
{filteredOptions.length > 0 ? (
|
|
filteredOptions.map((option) => {
|
|
const isSelected = selectedCountry === option;
|
|
|
|
return (
|
|
<button
|
|
key={option}
|
|
type="button"
|
|
onClick={() => handleSelectCountry(option)}
|
|
className={[
|
|
"flex w-full items-center gap-3.5 rounded-[16px] p-3.5 text-start transition-all cursor-pointer border",
|
|
isSelected
|
|
? "bg-[#FFF4F5] border-[#F0445B]/30 text-[#181818] shadow-xs"
|
|
: "bg-[#FAFAFA] hover:bg-[#F2F4F7] border-transparent text-[#181818]",
|
|
].join(" ")}
|
|
>
|
|
<div
|
|
className={[
|
|
"size-[22px] shrink-0 rounded-full transition-all duration-150 flex items-center justify-center",
|
|
isSelected
|
|
? "border-[6px] border-[#F0445B] bg-white"
|
|
: "border-[2px] border-[#98A2B3] bg-white",
|
|
].join(" ")}
|
|
/>
|
|
<span
|
|
className={
|
|
isSelected
|
|
? "font-bold text-[#181818] text-[15px]"
|
|
: "font-semibold text-[#344054] text-[15px]"
|
|
}
|
|
>
|
|
{option}
|
|
</span>
|
|
</button>
|
|
);
|
|
})
|
|
) : (
|
|
<div className="py-8 text-center text-[14px] text-[#667085]">
|
|
{noResultsText}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>,
|
|
document.body,
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default QuestionBirthplace;
|