Use CityAutocomplete for FlightsMapFilter with geolocate on departure

This commit is contained in:
2026-04-17 15:13:20 +03:00
parent b8d5de6ca7
commit 373f049e90
2 changed files with 66 additions and 77 deletions
@@ -16,13 +16,25 @@ vi.mock("primereact/calendar", () => ({
},
}));
// Stub PrimeReact AutoComplete so rendering is cheap.
vi.mock("primereact/autocomplete", () => ({
AutoComplete: (props: Record<string, unknown>) => (
<input data-testid={props["data-testid"] as string} />
// Stub the composite CityAutocomplete so rendering is cheap.
vi.mock("@/ui/city-autocomplete/index.js", () => ({
CityAutocomplete: (props: Record<string, unknown>) => (
<input
data-testid={`${(props["testIdPrefix"] as string) ?? "cac"}-input`}
placeholder={props["placeholder"] as string}
/>
),
}));
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> = {},
): IFlightsMapFilterState {
@@ -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<FlightsMapFilterProps> = ({
onChange,
}) => {
const { t } = useTranslation();
const [departure, setDeparture] = useState<CitySuggestion | string>(value.departure ?? "");
const [arrival, setArrival] = useState<CitySuggestion | string>(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<FlightsMapFilterProps> = ({
}
}, [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<FlightsMapFilterProps> = ({
return (
<div className="flights-map-filter" data-testid="flights-map-filter">
<div className="flights-map-filter__field">
<label htmlFor="fm-departure">{t("SHARED.DEPARTURE_CITY")}</label>
<AutoComplete
value={departure}
suggestions={departureSuggestions}
completeMethod={handleDepartureSearch}
field="name"
onChange={(e) => 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"
/>
</div>
<CityAutocomplete
label={t("SHARED.DEPARTURE_CITY")}
placeholder={t("FLIGHTS-MAP.FILTER_DEPARTURE_PLACEHOLDER")}
value={value.departure ?? ""}
onChange={(code) => {
onChange({
...value,
departure: code || undefined,
arrival: undefined,
});
}}
dictionaries={dictionaries}
onLocate={handleLocate}
testIdPrefix="fm-departure"
/>
<button
type="button"
@@ -185,22 +172,16 @@ export const FlightsMapFilter: FC<FlightsMapFilterProps> = ({
&#8646;
</button>
<div className="flights-map-filter__field">
<label htmlFor="fm-arrival">{t("SHARED.ARRIVAL_CITY")}</label>
<AutoComplete
value={arrival}
suggestions={arrivalSuggestions}
completeMethod={handleArrivalSearch}
field="name"
onChange={(e) => 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"
/>
</div>
<CityAutocomplete
label={t("SHARED.ARRIVAL_CITY")}
placeholder={t("FLIGHTS-MAP.FILTER_ARRIVAL_PLACEHOLDER")}
value={value.arrival ?? ""}
onChange={(code) => {
onChange({ ...value, arrival: code || undefined });
}}
dictionaries={dictionaries}
testIdPrefix="fm-arrival"
/>
<div className="flights-map-filter__field">
<label htmlFor="fm-date">{t("SHARED.FLIGHT_DATE")}</label>