Use CityAutocomplete for FlightsMapFilter with geolocate on departure
This commit is contained in:
@@ -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> = ({
|
||||
⇆
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user