import { useState, useRef, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; /** * CityFilter — мультиселект городов с поиском и свободным вводом. * * @param {string[]} cities — доступные города (из /posts/cities) * @param {string[]} value — выбранные города * @param {(next: string[]) => void} onChange — изменение выбора */ export default function CityFilter({ cities, value, onChange }) { const { t } = useTranslation(); const [query, setQuery] = useState(""); const [open, setOpen] = useState(false); const rootRef = useRef(null); const inputRef = useRef(null); // Закрываем дропдаун при клике вне компонента useEffect(() => { if (!open) return; const onDocMouseDown = (e) => { if (rootRef.current && !rootRef.current.contains(e.target)) { setOpen(false); } }; document.addEventListener("mousedown", onDocMouseDown); return () => document.removeEventListener("mousedown", onDocMouseDown); }, [open]); const normalizedQuery = query.trim().toLowerCase(); // Города, совпадающие с введённым текстом (выбранные помечаются галочкой) const options = useMemo(() => { const q = normalizedQuery; return cities.filter(c => c && (!q || c.toLowerCase().includes(q))); }, [cities, normalizedQuery]); // Свободный ввод доступен, если введён текст, которого нет ни в списке, ни в выбранных const canAddCustom = normalizedQuery.length > 0 && !value.some(c => c.toLowerCase() === normalizedQuery) && !cities.some(c => c.toLowerCase() === normalizedQuery); const toggle = (city) => { if (value.includes(city)) { onChange(value.filter(c => c !== city)); } else { onChange([...value, city]); } setQuery(""); inputRef.current?.focus(); }; const addCustom = () => { const city = query.trim(); if (city && !value.includes(city)) { onChange([...value, city]); } setQuery(""); inputRef.current?.focus(); }; const handleKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); if (canAddCustom) { addCustom(); } else if (options.length) { toggle(options[0]); } } else if (e.key === "Backspace" && query === "" && value.length) { onChange(value.slice(0, -1)); } else if (e.key === "Escape") { setOpen(false); } }; return (
setOpen(v => !v)}>
{value.map(city => ( {city} ))} { setQuery(e.target.value); setOpen(true); }} onClick={e => e.stopPropagation()} onKeyDown={handleKeyDown} />
{value.length > 0 && ( )}
{open && (
{options.length === 0 && !canAddCustom && (
{t('feed.filterCitiesEmpty')}
)} {canAddCustom && ( )} {options.map(city => { const selected = value.includes(city); return ( ); })}
)}
); }