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.
 
 
 
 
 

716 lines
26 KiB

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { getCountryList, COUNTRIES_EN, COUNTRIES_FA } from "@/data/countries";
import type { QuestionField } from "@/data/question-data";
import { useI18n } from "@/translations/provider";
import { useQuestionAnswers } from "./question-answer-storage";
import QuestionTitle from "./question-title";
type QuestionBirthplaceProps = {
question: QuestionField;
questionIndex: number;
disabled?: boolean;
};
type BirthplaceValue = {
country?: string;
city?: string;
};
function parseValue(rawValue: unknown): { country: string; city: string } {
if (!rawValue) return { country: "", city: "" };
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 : "",
};
}
if (typeof rawValue === "string") {
let str = rawValue.trim();
// 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)",
);
if (str.includes(",")) {
const parts = str.split(",").map((s) => s.trim());
return { city: parts[0] || "", country: parts[1] || "" };
}
if (str.includes(" - ")) {
const parts = str.split(" - ").map((s) => s.trim());
return { country: parts[0] || "", city: parts[1] || "" };
}
const isCountry = COUNTRIES_EN.includes(str) || COUNTRIES_FA.includes(str);
if (isCountry) {
return { country: str, city: "" };
}
return { country: "", city: str };
}
return { country: "", city: "" };
}
export function QuestionBirthplace({
question,
questionIndex,
disabled,
}: QuestionBirthplaceProps) {
const { dictionary: t, locale } = useI18n();
const { getAnswerValue, setAnswerValue } = useQuestionAnswers();
const rawValue = getAnswerValue(question, questionIndex);
const initial = parseValue(rawValue);
const [selectedCountry, setSelectedCountry] = useState(initial.country);
const [cityInput, setCityInput] = useState(initial.city);
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const isResidence =
question.title.toLowerCase().includes("residence") ||
question.title.includes("سکونت");
const [mode, setMode] = useState<"auto" | "manual">("auto");
const [isDetecting, setIsDetecting] = useState(false);
const [detectedLocation, setDetectedLocation] = useState("");
const updateAnswers = (country: string, city: string) => {
const formatted =
city && country ? `${city}, ${country}` : city || country || null;
setAnswerValue(question, questionIndex, 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 {
setMode("auto");
}
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}`);
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(() => {
setIsDetecting(false);
});
};
useEffect(() => {
if (isResidence) {
detectLocation();
}
}, [isResidence]);
const handleAutoClick = () => {
setMode("auto");
detectLocation(true);
};
const handleManualClick = () => {
if (typeof window !== "undefined") {
localStorage.setItem("hasCheckedGeoIPResidence", "true");
}
setMode("manual");
setSelectedCountry("");
setCityInput("");
updateAnswers("", "");
};
// Synchronize state if rawValue changes externally
useEffect(() => {
const updated = parseValue(rawValue);
if (updated.country !== selectedCountry) {
setSelectedCountry(updated.country);
}
if (updated.city.trim() !== cityInput.trim()) {
setCityInput(updated.city);
}
if (updated.country && updated.city) {
setDetectedLocation(`${updated.city}, ${updated.country}`);
}
}, [rawValue]);
// Lock page scroll when dropdown is open
useEffect(() => {
if (isOpen) {
document.body.classList.add("dropdown-open");
document.body.style.overflow = "hidden";
document.documentElement.style.overflow = "hidden";
} else {
document.body.classList.remove("dropdown-open");
document.body.style.overflow = "";
document.documentElement.style.overflow = "";
}
return () => {
document.body.classList.remove("dropdown-open");
document.body.style.overflow = "";
document.documentElement.style.overflow = "";
};
}, [isOpen]);
useEffect(() => {
if (isOpen) {
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 50);
return () => clearTimeout(timer);
}
}, [isOpen]);
const handleListWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
e.stopPropagation();
const el = listRef.current;
if (!el) return;
const atTop = el.scrollTop <= 0 && e.deltaY < 0;
const atBottom =
el.scrollTop + el.clientHeight >= el.scrollHeight && e.deltaY > 0;
if (atTop || atBottom) {
e.preventDefault();
}
}, []);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const options = getCountryList(locale);
const filteredOptions = options.filter((option) =>
option.toLowerCase().includes(searchQuery.toLowerCase()),
);
const handleSelectCountry = (country: string) => {
if (typeof window !== "undefined") {
localStorage.setItem("hasCheckedGeoIPResidence", "true");
}
setSelectedCountry(country);
setCityInput("");
setIsOpen(false);
updateAnswers(country, "");
};
const handleCityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (typeof window !== "undefined") {
localStorage.setItem("hasCheckedGeoIPResidence", "true");
}
const newCity = e.target.value;
setCityInput(newCity);
updateAnswers(selectedCountry, newCity);
};
const isRtl = locale === "fa" || locale === "ar" || locale === "ur";
const selectCountryPlaceholder = isRtl ? "انتخاب کشور" : "Select country";
const cityPlaceholder =
question.extras?.placeHolder ||
(isRtl ? "شهر، منطقه یا محله" : "City, region, or neighborhood");
return (
<div
ref={containerRef}
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}
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(" ")}
>
<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>
{mode === "manual" && (
<div className="flex flex-col gap-3 w-full animate-in fade-in slide-in-from-top-2 duration-200">
{/* Country Selection Dropdown */}
<div className="relative w-full">
<button
type="button"
disabled={disabled}
onClick={() => setIsOpen(!isOpen)}
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>
{isOpen && (
<div className="absolute top-[calc(100%+8px)] left-0 z-50 flex w-full flex-col gap-3.5 rounded-[20px] bg-white p-4.5 shadow-[0_12px_36px_rgba(0,0,0,0.09)] border border-[#EAECF0] animate-in fade-in zoom-in-95 duration-150">
{options.length > 3 || searchQuery ? (
<div className="flex h-[46px] w-full items-center gap-2.5 rounded-[12px] bg-[#EFEFEF] px-3.5 transition-colors focus-within:bg-[#E8E8E8]">
<svg
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
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search..."
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"
>
</button>
) : null}
</div>
) : null}
<div
ref={listRef}
onWheel={handleListWheel}
onTouchStart={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
className="flex max-h-[220px] flex-col gap-3.5 overflow-y-auto overscroll-contain pr-1"
>
{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 text-start cursor-pointer group/opt py-0.5"
>
<div
className={[
"size-[20px] shrink-0 rounded-full transition-all duration-150",
isSelected
? "bg-[#F2465F] shadow-xs"
: "border-[2px] border-[#344054] bg-transparent group-hover/opt:border-[#181818]",
].join(" ")}
/>
<span className="text-[15px] font-bold text-[#181818]">
{option}
</span>
</button>
);
})
) : (
<span className="py-2 text-[13px] text-[#667085]">
No options found
</span>
)}
</div>
</div>
)}
</div>
{/* City Text Input */}
<div className="w-full">
<input
type="text"
disabled={disabled}
value={cityInput}
onChange={handleCityChange}
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>
</div>
)}
</>
) : (
<>
{/* 1. Country Selection Dropdown */}
<div className="relative w-full">
<button
type="button"
disabled={disabled}
onClick={() => setIsOpen(!isOpen)}
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>
{isOpen && (
<div className="absolute top-[calc(100%+8px)] left-0 z-50 flex w-full flex-col gap-3.5 rounded-[20px] bg-white p-4.5 shadow-[0_12px_36px_rgba(0,0,0,0.09)] border border-[#EAECF0] animate-in fade-in zoom-in-95 duration-150">
{options.length > 3 || searchQuery ? (
<div className="flex h-[46px] w-full items-center gap-2.5 rounded-[12px] bg-[#EFEFEF] px-3.5 transition-colors focus-within:bg-[#E8E8E8]">
<svg
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
ref={searchInputRef}
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search..."
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"
>
</button>
) : null}
</div>
) : null}
<div
ref={listRef}
onWheel={handleListWheel}
onTouchStart={(e) => e.stopPropagation()}
onTouchMove={(e) => e.stopPropagation()}
onTouchEnd={(e) => e.stopPropagation()}
className="flex max-h-[220px] flex-col gap-3.5 overflow-y-auto overscroll-contain pr-1"
>
{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 text-start cursor-pointer group/opt py-0.5"
>
<div
className={[
"size-[20px] shrink-0 rounded-full transition-all duration-150",
isSelected
? "bg-[#F2465F] shadow-xs"
: "border-[2px] border-[#344054] bg-transparent group-hover/opt:border-[#181818]",
].join(" ")}
/>
<span className="text-[15px] font-bold text-[#181818]">
{option}
</span>
</button>
);
})
) : (
<span className="py-2 text-[13px] text-[#667085]">
No options found
</span>
)}
</div>
</div>
)}
</div>
{/* 2. City Text Input */}
<div className="w-full">
<input
type="text"
disabled={disabled}
value={cityInput}
onChange={handleCityChange}
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>
</>
)}
</div>
);
}
export default QuestionBirthplace;