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.
 
 
 
 
 

959 lines
31 KiB

"use client";
import { 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 type { QuestionField } from "@/lib/schema-adapter";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
import { useSheetScrollLock } from "./use-sheet-scroll-lock";
const EXIT_ANIMATION_MS = 300;
type QuestionPhoneProps = {
question: QuestionField;
countryCode?: string;
disabled?: boolean;
};
type PhoneValueParts = {
codeValue: string;
phoneValue: string;
};
const phoneUtil = PhoneNumberUtil.getInstance();
// Module-level singleton state for IP phone country resolution
let cachedGeoCountryCode: string | null = null;
let geoIpPromise: Promise<string | null> | null = null;
const geoListeners = new Set<(code: string) => void>();
export function resetGeoPhoneStateForTesting() {
cachedGeoCountryCode = null;
geoIpPromise = null;
geoListeners.clear();
}
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;
}
export function setManuallySelectedGeoCode(code: string) {
cachedGeoCountryCode = code;
if (typeof window !== "undefined") {
try {
localStorage.setItem("geoIPPhoneCode", code);
localStorage.setItem("hasCheckedGeoIPPhone", "true");
} catch {}
}
}
export function fetchGeoCountryCode(defaultCode = "+44"): Promise<string> {
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);
}
function isMarriagePhoneFieldValue(
value: unknown,
): value is MarriagePhoneFieldValue {
if (!value || typeof value !== "object") {
return false;
}
const phoneValue = value as Partial<MarriagePhoneFieldValue>;
return (
typeof phoneValue.countryCode === "string" &&
typeof phoneValue.phoneNumber === "string"
);
}
function readPhoneValue(value: unknown, fallbackCode: string): PhoneValueParts {
if (value === null) {
return {
codeValue: fallbackCode,
phoneValue: "",
};
}
if (isMarriagePhoneFieldValue(value)) {
return {
codeValue: value.countryCode
? `+${value.countryCode.replace(/^\+/, "")}`
: fallbackCode,
phoneValue: value.phoneNumber,
};
}
if (typeof value !== "string") {
return {
codeValue: fallbackCode,
phoneValue: "",
};
}
if (value.length === 0) {
return {
codeValue: fallbackCode,
phoneValue: "",
};
}
const separatorIndex = value.indexOf(" ");
if (separatorIndex >= 0) {
return {
codeValue: value.slice(0, separatorIndex),
phoneValue: value.slice(separatorIndex + 1),
};
}
if (value.startsWith("+")) {
try {
const parsedNumber = phoneUtil.parse(value);
const countryCode = parsedNumber.getCountryCode();
const nationalNumber = String(parsedNumber.getNationalNumber());
return {
codeValue: countryCode ? `+${countryCode}` : fallbackCode,
phoneValue: nationalNumber,
};
} catch {
return {
codeValue: fallbackCode,
phoneValue: value,
};
}
}
return {
codeValue: fallbackCode,
phoneValue: value,
};
}
function getMaxLengthForCountry(codeValue: string): number {
try {
const cleanCode = codeValue.replace(/[^\d]/g, "");
if (!cleanCode) return 15;
const countryCode = Number(cleanCode);
const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
if (!regionCode || regionCode === "ZZ") return 15;
const exampleMobile = phoneUtil.getExampleNumberForType(
regionCode,
1, // MOBILE
);
const exampleGeneral = phoneUtil.getExampleNumber(regionCode);
const lenMobile = exampleMobile
? String(exampleMobile.getNationalNumber()).length
: 0;
const lenGeneral = exampleGeneral
? String(exampleGeneral.getNationalNumber()).length
: 0;
const baseLen = Math.max(lenMobile, lenGeneral, 8);
return baseLen + 1;
} catch {
return 15;
}
}
function writePhoneValue(codeValue: string, phoneValue: string) {
if (!codeValue && !phoneValue) {
return null;
}
if (codeValue && phoneValue) {
return `${codeValue} ${phoneValue}`;
}
if (codeValue) {
return codeValue.startsWith("+") ? codeValue : `${codeValue} `;
}
return phoneValue;
}
function toStoredPhoneValue(
codeValue: string,
phoneValue: string,
): MarriagePhoneFieldValue | null {
const normalizedCountryCode = sanitizeCountryCode(codeValue).replace(
/^\+/,
"",
);
const normalizedPhoneNumber = phoneValue.trim().replace(/\s+/g, "");
if (!normalizedCountryCode && !normalizedPhoneNumber) {
return null;
}
return {
countryCode: normalizedCountryCode,
phoneNumber: normalizedPhoneNumber,
};
}
function sanitizeCountryCode(value: string) {
const sanitized = value.replace(/[^\d+]/g, "");
if (sanitized.length === 0) {
return "";
}
return sanitized.startsWith("+")
? `+${sanitized.slice(1).replace(/\+/g, "")}`
: `+${sanitized.replace(/\+/g, "")}`;
}
function sanitizePhoneNumber(value: string) {
return value.replace(/[^\d\s\-().]/g, "");
}
function getNormalizedPhoneValue(codeValue: string, phoneValue: string) {
const nextCodeValue = sanitizeCountryCode(codeValue);
const nextPhoneValue = phoneValue.trim();
if (nextCodeValue.length === 0 && nextPhoneValue.length === 0) {
return {
isValid: !nextPhoneValue.length,
normalizedValue: null,
};
}
if (nextCodeValue.length === 0 || nextPhoneValue.length === 0) {
return {
isValid: false,
normalizedValue: null,
};
}
try {
const parsedNumber = phoneUtil.parse(`${nextCodeValue} ${nextPhoneValue}`);
if (!phoneUtil.isValidNumber(parsedNumber)) {
return {
isValid: false,
normalizedValue: null,
};
}
return {
isValid: true,
normalizedValue: phoneUtil.format(parsedNumber, PhoneNumberFormat.E164),
};
} catch {
return {
isValid: false,
normalizedValue: null,
};
}
}
export function QuestionPhone({
question,
countryCode = "+44",
disabled,
}: QuestionPhoneProps) {
const { dictionary: t, locale } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const value = getAnswerValue(question);
const defaultCodeValue = countryCode.trim() || "+44";
const userInteractedRef = useRef(false);
// Check if we already have a saved / existing value from profile / backend
const hasExplicitValue = useMemo(() => {
if (isMarriagePhoneFieldValue(value) && value.countryCode) {
return true;
}
if (typeof value === "string" && value.trim().length > 0) {
const parts = readPhoneValue(value, defaultCodeValue);
return Boolean(parts.codeValue && parts.codeValue !== defaultCodeValue);
}
return false;
}, [value, defaultCodeValue]);
const initialCachedCode = useMemo(() => {
return getStoredGeoCode();
}, []);
const [isResolvingCountry, setIsResolvingCountry] = useState(() => {
if (hasExplicitValue) return false;
if (initialCachedCode) return false;
return true;
});
const initialCode = useMemo(() => {
if (isMarriagePhoneFieldValue(value) && value.countryCode) {
return value.countryCode.startsWith("+")
? value.countryCode
: `+${value.countryCode}`;
}
if (typeof value === "string" && value.length > 0) {
const parts = readPhoneValue(value, defaultCodeValue);
if (parts.codeValue && parts.codeValue !== defaultCodeValue) {
return parts.codeValue;
}
}
const cached = getStoredGeoCode();
if (cached) return cached;
return defaultCodeValue;
}, [value, defaultCodeValue]);
const initialPhone = useMemo(() => {
return readPhoneValue(value, defaultCodeValue).phoneValue;
}, [value, defaultCodeValue]);
const [codeValue, setCodeValue] = useState(initialCode);
const [phoneValue, setPhoneValue] = useState(initialPhone);
const lastCommittedValueRef = useRef(value);
const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const listRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const closeSheet = useCallback(() => {
setIsClosing(true);
window.setTimeout(() => {
setIsOpen(false);
setIsClosing(false);
setSearchQuery("");
}, EXIT_ANIMATION_MS);
}, []);
const openSheet = useCallback(() => {
if (disabled || isResolvingCountry) return;
setIsOpen(true);
setIsClosing(false);
}, [disabled, isResolvingCountry]);
// IP Resolution Effect
useEffect(() => {
if (hasExplicitValue || !isResolvingCountry || userInteractedRef.current) {
return;
}
let isMounted = true;
const onGeoCodeResolved = (resolvedCode: string) => {
if (!isMounted || userInteractedRef.current) return;
setCodeValue(resolvedCode);
setIsResolvingCountry(false);
};
geoListeners.add(onGeoCodeResolved);
fetchGeoCountryCode(defaultCodeValue)
.then((resolvedCode) => {
if (!isMounted) return;
if (!userInteractedRef.current) {
setCodeValue(resolvedCode || defaultCodeValue);
}
setIsResolvingCountry(false);
})
.catch(() => {
if (!isMounted) return;
if (!userInteractedRef.current) {
setCodeValue(defaultCodeValue);
}
setIsResolvingCountry(false);
});
return () => {
isMounted = false;
geoListeners.delete(onGeoCodeResolved);
};
}, [hasExplicitValue, isResolvingCountry, defaultCodeValue]);
const normalizedPhoneState = getNormalizedPhoneValue(codeValue, phoneValue);
const showInvalidState =
codeValue.trim().length > 0 &&
phoneValue.trim().length > 0 &&
!normalizedPhoneState.isValid;
const isAnswered =
question.required === false
? normalizedPhoneState.isValid || phoneValue.trim().length === 0
: normalizedPhoneState.isValid;
const countryList = useMemo(() => {
const regions = phoneUtil.getSupportedRegions();
const displayNames = new Intl.DisplayNames([locale || "en"], {
type: "region",
});
const list: { name: string; code: string; flag: string }[] = [];
const seen = new Set<string>();
for (const region of regions) {
try {
const callingCode = phoneUtil.getCountryCodeForRegion(region);
if (!callingCode) continue;
let name = displayNames.of(region);
if (!name) continue;
if (region === "US")
name = locale === "fa" ? "ایالات متحده آمریکا" : "United States";
if (region === "GB")
name = locale === "fa" ? "بریتانیا" : "United Kingdom";
const key = `${name}_+${callingCode}`;
if (seen.has(key)) continue;
seen.add(key);
const codePoints = region
.toUpperCase()
.split("")
.map((char) => 127397 + char.charCodeAt(0));
const flag = String.fromCodePoint(...codePoints);
list.push({
name,
code: `+${callingCode}`,
flag,
});
} catch {}
}
return list.sort((a, b) => a.name.localeCompare(b.name));
}, [locale]);
const activeFlag = useMemo(() => {
if (!codeValue) return "🏳️";
const cleanActiveCode = codeValue.replace(/[^\d]/g, "");
if (cleanActiveCode === "44") {
const gb = countryList.find(
(c) => c.name.includes("United Kingdom") || c.name.includes("بریتانیا"),
);
if (gb) return gb.flag;
}
if (cleanActiveCode === "1") {
const us = countryList.find(
(c) =>
c.name.includes("United States") || c.name.includes("ایالات متحده"),
);
if (us) return us.flag;
}
const match = countryList.find(
(c) => c.code.replace(/[^\d]/g, "") === cleanActiveCode,
);
return match ? match.flag : "🏳️";
}, [codeValue, countryList]);
const selectedCountry = useMemo(() => {
const cleanActiveCode = codeValue.replace(/[^\d]/g, "");
if (cleanActiveCode === "44") {
const gb = countryList.find(
(c) => c.name.includes("United Kingdom") || c.name.includes("بریتانیا"),
);
if (gb) return gb;
}
if (cleanActiveCode === "1") {
const us = countryList.find(
(c) =>
c.name.includes("United States") || c.name.includes("ایالات متحده"),
);
if (us) return us;
}
return countryList.find(
(country) => country.code.replace(/[^\d]/g, "") === cleanActiveCode,
);
}, [codeValue, countryList]);
const filteredCountries = useMemo(() => {
const q = searchQuery.toLowerCase().trim();
if (!q) return countryList;
return countryList.filter(
(c) =>
c.name.toLowerCase().includes(q) || c.code.toLowerCase().includes(q),
);
}, [countryList, searchQuery]);
useSheetScrollLock(isOpen, { onBack: closeSheet });
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") closeSheet();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [closeSheet, isOpen]);
// Sync state if external value changes (e.g. backend data loaded)
useEffect(() => {
if (value === lastCommittedValueRef.current) {
return;
}
const nextValue = readPhoneValue(value, defaultCodeValue);
const cachedCode = getStoredGeoCode();
const explicit =
(isMarriagePhoneFieldValue(value) && Boolean(value.countryCode)) ||
(typeof value === "string" &&
value.trim().length > 0 &&
nextValue.codeValue !== defaultCodeValue);
if (explicit) {
setIsResolvingCountry(false);
setCodeValue(nextValue.codeValue);
const maxLen = getMaxLengthForCountry(nextValue.codeValue);
setPhoneValue(nextValue.phoneValue.slice(0, maxLen));
} else if (value !== null && value !== undefined) {
const resolvedCode = cachedCode || defaultCodeValue;
setCodeValue(resolvedCode);
const maxLen = getMaxLengthForCountry(resolvedCode);
setPhoneValue(nextValue.phoneValue.slice(0, maxLen));
}
lastCommittedValueRef.current = value;
}, [defaultCodeValue, value]);
const updateStoredValue = (nextCodeValue: string, nextPhoneValue: string) => {
const draftValue = writePhoneValue(nextCodeValue, nextPhoneValue);
const nextPhoneState = getNormalizedPhoneValue(
nextCodeValue,
nextPhoneValue,
);
const nextValue =
draftValue === null
? null
: nextPhoneState.isValid
? toStoredPhoneValue(nextCodeValue, nextPhoneValue)
: null;
lastCommittedValueRef.current = nextValue;
setAnswerValue(question, nextValue);
};
const handleSelectCountryCode = (selectedCode: string) => {
userInteractedRef.current = true;
setIsResolvingCountry(false);
setManuallySelectedGeoCode(selectedCode);
const maxLen = getMaxLengthForCountry(selectedCode);
const truncatedPhone = phoneValue.slice(0, maxLen);
setCodeValue(selectedCode);
setPhoneValue(truncatedPhone);
updateStoredValue(selectedCode, truncatedPhone);
closeSheet();
};
const selectCountryTitle = t["Select country"] || question.title;
const searchPlaceholder =
locale === "fa"
? "جستجو..."
: locale === "ar"
? "بحث..."
: locale === "tr"
? "Ara..."
: "Search...";
const noResultsText =
locale === "fa"
? "موردی یافت نشد"
: locale === "ar"
? "لم يتم العثور على نتائج"
: "No options found";
return (
<div
data-question-answered={isAnswered ? "true" : "false"}
data-question-type={question.type}
className={[
"relative flex w-full flex-col gap-2 transition-opacity duration-200",
disabled ? "pointer-events-none opacity-30" : "",
].join(" ")}
>
<QuestionTitle question={question} />
<div
dir="ltr"
data-snap-drag-ignore="true"
className={[
"flex h-[54px] w-full items-center rounded-[16px] border bg-white text-[#181818] focus-within:border-[#6F6F6F] focus-within:ring-1 focus-within:ring-[#6F6F6F] transition-all",
showInvalidState
? "border-[#F2465F] ring-1 ring-[#F2465F]"
: "border-[#D0D5DD] hover:border-[#98A2B3]",
].join(" ")}
>
<div className="flex shrink-0 items-center pl-2.5 pr-2">
<button
type="button"
disabled={disabled || isResolvingCountry}
onClick={openSheet}
className="flex items-center gap-1.5 h-full px-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer outline-none border-0 text-[15px] font-medium leading-none text-[#181818] tabular-nums"
>
{isResolvingCountry ? (
<div
className="flex items-center gap-1.5 py-1"
aria-hidden="true"
>
<span className="h-[18px] w-6 rounded-[4px] shimmer-bg inline-block shrink-0" />
<span className="h-[18px] w-8 rounded-[4px] shimmer-bg inline-block shrink-0" />
<span className="h-2.5 w-2.5 rounded-[2px] shimmer-bg inline-block shrink-0 opacity-60" />
</div>
) : (
<>
<span>{activeFlag}</span>
<span>{codeValue || defaultCodeValue}</span>
<svg
aria-hidden="true"
width="10"
height="6"
viewBox="0 0 10 6"
fill="none"
className={[
"shrink-0 transition-transform duration-200 text-[#344054]",
isOpen ? "rotate-180" : "",
].join(" ")}
>
<path
d="M1 1L5 5L9 1"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</>
)}
</button>
<span aria-hidden="true" className="h-5 w-px bg-[#181818]/35 ml-1" />
</div>
<span className="flex min-w-0 flex-1 items-center pr-4">
<input
type="tel"
inputMode="tel"
data-snap-drag-ignore="true"
autoComplete="off"
autoCorrect="off"
autoCapitalize="none"
spellCheck="false"
aria-autocomplete="none"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
disabled={disabled}
placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")}
value={phoneValue}
maxLength={getMaxLengthForCountry(codeValue)}
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);
setPhoneValue(truncatedPhone);
updateStoredValue(codeValue, truncatedPhone);
}}
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]"
/>
</span>
</div>
{showInvalidState ? (
<span className="block group-10 font-semibold text-[#F2465F]">
Enter a valid phone number with country code.
</span>
) : null}
{/* The phone field moves up while the country sheet enters from below. */}
{isOpen &&
createPortal(
<div
className="fixed inset-0 z-50 flex items-end justify-center bg-transparent"
role="dialog"
aria-label={selectCountryTitle}
onKeyDown={(event) => {
if (event.key === "Escape") closeSheet();
}}
onClick={(event) => {
if (event.target === event.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-[300ms] ease-out sm:max-w-[375px] animate-in slide-in-from-bottom",
isClosing ? "translate-y-full" : "translate-y-0",
].join(" ")}
>
{/* 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">
{selectCountryTitle}
</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
aria-hidden="true"
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
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
className="shrink-0 text-[#667085]"
aria-hidden="true"
>
<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
ref={searchInputRef}
type="text"
name="country_search_field"
autoComplete="off"
autoCorrect="off"
autoCapitalize="none"
spellCheck="false"
aria-autocomplete="none"
data-lpignore="true"
data-1p-ignore="true"
data-bwignore="true"
data-form-type="other"
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}
onTouchStart={(event) => event.stopPropagation()}
onTouchMove={(event) => event.stopPropagation()}
onTouchEnd={(event) => event.stopPropagation()}
className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto overscroll-contain px-5 py-3"
>
{filteredCountries.length > 0 ? (
filteredCountries.map((c) => {
const cleanCode = c.code.replace(/[^\d]/g, "");
const activeCleanCode = (codeValue || "").replace(
/[^\d]/g,
"",
);
const isSelected = cleanCode === activeCleanCode;
return (
<button
key={`${c.name}-${c.code}`}
type="button"
onClick={() => handleSelectCountryCode(c.code)}
className={[
"flex min-h-12 w-full items-center gap-3 rounded-lg border px-3 py-3 text-start transition-colors cursor-pointer",
isSelected
? "bg-[#FFF4F5] border-[#F0445B]/30 text-[#181818]"
: "bg-white hover:bg-[#F2F4F7] border-[#EAECF0] text-[#181818]",
].join(" ")}
>
{/* Radio Indicator */}
<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(" ")}
/>
{/* Country Flag */}
<span
className="text-[20px] shrink-0"
aria-hidden="true"
>
{c.flag}
</span>
{/* Country Name & Dial Code */}
<span className="flex min-w-0 flex-1 items-center justify-between gap-2">
<span
className={[
"truncate text-[15px]",
isSelected
? "font-bold text-[#181818]"
: "font-semibold text-[#344054]",
].join(" ")}
>
{c.name}
</span>
<span
dir="ltr"
className={[
"shrink-0 text-[14px] font-medium tabular-nums",
isSelected ? "text-[#F0445B]" : "text-[#667085]",
].join(" ")}
>
{c.code}
</span>
</span>
</button>
);
})
) : (
<div className="py-8 text-center text-[14px] text-[#667085]">
{noResultsText}
</div>
)}
</div>
</section>
</div>,
document.body,
)}
</div>
);
}
export default QuestionPhone;