116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { COUNTRIES } from "@/lib/countries";
|
|
|
|
interface CountrySelectProps {
|
|
value: string;
|
|
onChange: (code: string) => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function CountrySelect({ value, onChange, className }: CountrySelectProps) {
|
|
const [open, setOpen] = useState(false);
|
|
const [search, setSearch] = useState("");
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const selected = COUNTRIES.find((c) => c.code === value);
|
|
|
|
const filtered = search
|
|
? COUNTRIES.filter((c) =>
|
|
c.label.toLowerCase().includes(search.toLowerCase()),
|
|
)
|
|
: COUNTRIES;
|
|
|
|
// Close on outside click
|
|
useEffect(() => {
|
|
function handle(e: MouseEvent) {
|
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
setOpen(false);
|
|
setSearch("");
|
|
}
|
|
}
|
|
document.addEventListener("mousedown", handle);
|
|
return () => document.removeEventListener("mousedown", handle);
|
|
}, []);
|
|
|
|
// Focus search input when opened
|
|
useEffect(() => {
|
|
if (open) inputRef.current?.focus();
|
|
}, [open]);
|
|
|
|
return (
|
|
<div ref={ref} className="relative">
|
|
{/* Trigger button */}
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setOpen((o) => !o);
|
|
setSearch("");
|
|
}}
|
|
className={`${className} flex items-center justify-between gap-2 text-left`}
|
|
>
|
|
<span className={selected ? "" : "text-lightsec dark:text-darksec"}>
|
|
{selected?.label ?? "Select country"}
|
|
</span>
|
|
<svg
|
|
className="h-4 w-4 shrink-0 text-lightsec dark:text-darksec"
|
|
viewBox="0 0 20 20"
|
|
fill="currentColor"
|
|
>
|
|
<path
|
|
fillRule="evenodd"
|
|
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
|
clipRule="evenodd"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Dropdown */}
|
|
{open && (
|
|
<div className="absolute left-0 z-50 mt-1 w-full overflow-hidden rounded-xl border border-lightline bg-white shadow-lg dark:border-darkline dark:bg-darkbg">
|
|
{/* Search */}
|
|
<div className="border-b border-lightline p-2 dark:border-darkline">
|
|
<input
|
|
ref={inputRef}
|
|
type="text"
|
|
placeholder="Search countries…"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full rounded-lg bg-transparent px-3 py-2 text-sm outline-none placeholder:text-lightsec dark:placeholder:text-darksec"
|
|
/>
|
|
</div>
|
|
|
|
{/* Options */}
|
|
<ul className="max-h-48 overflow-y-auto">
|
|
{filtered.length === 0 && (
|
|
<li className="px-4 py-3 text-sm text-lightsec dark:text-darksec">
|
|
No results
|
|
</li>
|
|
)}
|
|
{filtered.map((c) => (
|
|
<li key={c.code}>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
onChange(c.code);
|
|
setOpen(false);
|
|
setSearch("");
|
|
}}
|
|
className={`w-full px-4 py-2.5 text-left text-sm transition-colors hover:bg-gray-50 dark:hover:bg-white/5 ${
|
|
c.code === value
|
|
? "font-silkasb text-trptkblue dark:text-white"
|
|
: ""
|
|
}`}
|
|
>
|
|
{c.label}
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|