From 373f049e9024bb14f4f5af19d6fb2ba4ec9129fa Mon Sep 17 00:00:00 2001 From: gnezim Date: Fri, 17 Apr 2026 15:13:20 +0300 Subject: [PATCH] Use CityAutocomplete for FlightsMapFilter with geolocate on departure --- .../components/FlightsMapFilter.test.tsx | 24 ++-- .../components/FlightsMapFilter.tsx | 119 ++++++++---------- 2 files changed, 66 insertions(+), 77 deletions(-) diff --git a/src/features/flights-map/components/FlightsMapFilter.test.tsx b/src/features/flights-map/components/FlightsMapFilter.test.tsx index c95127ed..719cba45 100644 --- a/src/features/flights-map/components/FlightsMapFilter.test.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.test.tsx @@ -16,13 +16,25 @@ vi.mock("primereact/calendar", () => ({ }, })); -// Stub PrimeReact AutoComplete so rendering is cheap. -vi.mock("primereact/autocomplete", () => ({ - AutoComplete: (props: Record) => ( - +// Stub the composite CityAutocomplete so rendering is cheap. +vi.mock("@/ui/city-autocomplete/index.js", () => ({ + CityAutocomplete: (props: Record) => ( + ), })); +vi.mock("@/shared/dictionaries/index.js", () => ({ + useDictionaries: () => ({ dictionaries: null, loading: false, error: null }), + findCityByCoord: () => null, +})); + +vi.mock("@modern-js/runtime/router", () => ({ + useParams: () => ({ lang: "ru" }), +})); + vi.mock("@/i18n/provider.js", () => ({ useTranslation: () => ({ t: (key: string) => key, @@ -30,10 +42,6 @@ vi.mock("@/i18n/provider.js", () => ({ }), })); -vi.mock("@/shared/hooks/useCitySearch.js", () => ({ - useCitySearch: () => ({ suggestions: [], search: vi.fn() }), -})); - function filter( overrides: Partial = {}, ): IFlightsMapFilterState { diff --git a/src/features/flights-map/components/FlightsMapFilter.tsx b/src/features/flights-map/components/FlightsMapFilter.tsx index 799532aa..cf960fd8 100644 --- a/src/features/flights-map/components/FlightsMapFilter.tsx +++ b/src/features/flights-map/components/FlightsMapFilter.tsx @@ -7,11 +7,12 @@ * @module */ -import { type FC, useState, useCallback, useEffect, useMemo, type FormEvent } from "react"; -import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete"; +import { type FC, useCallback, useEffect, useMemo, type FormEvent } from "react"; import { Calendar } from "primereact/calendar"; +import { useParams } from "@modern-js/runtime/router"; import { useTranslation } from "@/i18n/provider.js"; -import { useCitySearch, type CitySuggestion } from "@/shared/hooks/useCitySearch.js"; +import { CityAutocomplete } from "@/ui/city-autocomplete/index.js"; +import { useDictionaries, findCityByCoord } from "@/shared/dictionaries/index.js"; import { getMinDate, getMaxDate, @@ -51,12 +52,28 @@ export const FlightsMapFilter: FC = ({ onChange, }) => { const { t } = useTranslation(); - const [departure, setDeparture] = useState(value.departure ?? ""); - const [arrival, setArrival] = useState(value.arrival ?? ""); + const { lang } = useParams<{ lang: string }>(); + const { dictionaries } = useDictionaries(lang ?? "ru"); - // City autocomplete search - const { suggestions: departureSuggestions, search: searchDeparture } = useCitySearch(); - const { suggestions: arrivalSuggestions, search: searchArrival } = useCitySearch(); + const handleLocate = useCallback(async () => { + if (!dictionaries || typeof navigator === "undefined" || !navigator.geolocation) return; + navigator.geolocation.getCurrentPosition( + (pos) => { + const city = findCityByCoord( + dictionaries, + pos.coords.latitude, + pos.coords.longitude, + ); + if (city) { + onChange({ ...value, departure: city.code }); + } + }, + () => { + // silent + }, + { enableHighAccuracy: false, timeout: 5000 }, + ); + }, [dictionaries, onChange, value]); const minDate = useMemo(() => getMinDate(), []); const maxDate = useMemo(() => getMaxDate(), []); @@ -79,38 +96,9 @@ export const FlightsMapFilter: FC = ({ } }, [disabledDates, minDate, maxDate, value, onChange]); - const handleDepartureSearch = useCallback((event: AutoCompleteCompleteEvent) => { - void searchDeparture(event.query); - }, [searchDeparture]); - - const handleArrivalSearch = useCallback((event: AutoCompleteCompleteEvent) => { - void searchArrival(event.query); - }, [searchArrival]); - - const handleDepartureBlur = useCallback(() => { - const code = typeof departure === "string" - ? departure.trim().toUpperCase() - : departure.code; - if (code !== value.departure) { - onChange({ ...value, departure: code || undefined, arrival: undefined }); - setArrival(""); - } - }, [departure, value, onChange]); - - const handleArrivalBlur = useCallback(() => { - const code = typeof arrival === "string" - ? arrival.trim().toUpperCase() - : arrival.code; - if (code !== value.arrival) { - onChange({ ...value, arrival: code || undefined }); - } - }, [arrival, value, onChange]); - const handleExchange = useCallback(() => { const newDep = value.arrival ?? ""; const newArr = value.departure ?? ""; - setDeparture(newDep); - setArrival(newArr); onChange({ ...value, departure: newDep || undefined, @@ -158,22 +146,21 @@ export const FlightsMapFilter: FC = ({ return (
-
- - setDeparture(e.value as CitySuggestion | string)} - onBlur={handleDepartureBlur} - placeholder={t("FLIGHTS-MAP.FILTER_DEPARTURE_PLACEHOLDER")} - className="input--filter" - inputClassName="input--filter" - inputId="fm-departure" - data-testid="fm-departure-input" - /> -
+ { + onChange({ + ...value, + departure: code || undefined, + arrival: undefined, + }); + }} + dictionaries={dictionaries} + onLocate={handleLocate} + testIdPrefix="fm-departure" + /> -
- - setArrival(e.value as CitySuggestion | string)} - onBlur={handleArrivalBlur} - placeholder={t("FLIGHTS-MAP.FILTER_ARRIVAL_PLACEHOLDER")} - className="input--filter" - inputClassName="input--filter" - inputId="fm-arrival" - data-testid="fm-arrival-input" - /> -
+ { + onChange({ ...value, arrival: code || undefined }); + }} + dictionaries={dictionaries} + testIdPrefix="fm-arrival" + />