ScheduleStartPage: use shared CityAutocomplete (adds clear button + regional picker parity)
This commit is contained in:
@@ -7,17 +7,16 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { type FC, useState, useCallback, useEffect, type FormEvent } from "react";
|
||||
import { type FC, useState, useCallback, type FormEvent } from "react";
|
||||
import { useNavigate } from "@modern-js/runtime/router";
|
||||
import { useLocale } from "@/i18n/useLocale.js";
|
||||
import { Calendar } from "primereact/calendar";
|
||||
import { Slider, type SliderChangeEvent } from "primereact/slider";
|
||||
import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
import { useCitySearch, type CitySuggestion } from "@/shared/hooks/useCitySearch.js";
|
||||
import { PageLayout } from "@/ui/layout/PageLayout.js";
|
||||
import { PageTabs } from "@/ui/layout/PageTabs.js";
|
||||
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
|
||||
import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
|
||||
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import {
|
||||
@@ -90,38 +89,13 @@ export const ScheduleStartPage: FC = () => {
|
||||
|
||||
const today = new Date();
|
||||
|
||||
const [departureAirport, setDepartureAirport] = useState<CitySuggestion | string>(prefill.departure ?? "");
|
||||
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>(prefill.arrival ?? "");
|
||||
// State is the IATA city code (string). The shared CityAutocomplete
|
||||
// resolves the code to a localized display name internally — same
|
||||
// component used on OnlineBoard and FlightsMap, so the clear (×)
|
||||
// button, regional picker, and airport→city resolution come for free.
|
||||
const [departureCode, setDepartureCode] = useState<string>(prefill.departure ?? "");
|
||||
const [arrivalCode, setArrivalCode] = useState<string>(prefill.arrival ?? "");
|
||||
|
||||
// Prefill arrives as a bare IATA code ("MOW"). Once dictionaries are
|
||||
// loaded, upgrade each state slot to a `{code, name}` CitySuggestion
|
||||
// so the PrimeReact AutoComplete renders "Москва" rather than the raw
|
||||
// code. Mirrors Angular's CityAutocomplete.writeValue + getCityOrAirport.
|
||||
useEffect(() => {
|
||||
if (!dictionaries) return;
|
||||
const resolve = (code: string): CitySuggestion | null => {
|
||||
const upper = code.toUpperCase();
|
||||
const city = dictionaries.cityByCode.get(upper);
|
||||
if (city) return { code: city.code, name: city.name };
|
||||
const airport = dictionaries.airportByCode.get(upper);
|
||||
if (airport) {
|
||||
const parentCode = airport.city_code?.toUpperCase();
|
||||
const parent = parentCode
|
||||
? dictionaries.cityByCode.get(parentCode)
|
||||
: null;
|
||||
if (parent) return { code: parent.code, name: parent.name };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
setDepartureAirport((current) => {
|
||||
if (typeof current !== "string" || !current) return current;
|
||||
return resolve(current) ?? current;
|
||||
});
|
||||
setArrivalAirport((current) => {
|
||||
if (typeof current !== "string" || !current) return current;
|
||||
return resolve(current) ?? current;
|
||||
});
|
||||
}, [dictionaries]);
|
||||
// Start blank to match Angular's `ДД.ММ.ГГГГ - ДД.ММ.ГГГГ` placeholder
|
||||
// (the "current week" pre-fill was a React-only convenience that
|
||||
// pulled the date input out of parity). Submit handler defaults to
|
||||
@@ -135,28 +109,12 @@ export const ScheduleStartPage: FC = () => {
|
||||
const [returnDateTo, setReturnDateTo] = useState<Date | null>(null);
|
||||
const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]);
|
||||
|
||||
// City autocomplete search
|
||||
const { suggestions: departureSuggestions, search: searchDeparture } = useCitySearch();
|
||||
const { suggestions: arrivalSuggestions, search: searchArrival } = useCitySearch();
|
||||
|
||||
const handleDepartureSearch = useCallback((event: AutoCompleteCompleteEvent) => {
|
||||
void searchDeparture(event.query);
|
||||
}, [searchDeparture]);
|
||||
|
||||
const handleArrivalSearch = useCallback((event: AutoCompleteCompleteEvent) => {
|
||||
void searchArrival(event.query);
|
||||
}, [searchArrival]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
const dep = (typeof departureAirport === "string"
|
||||
? departureAirport.trim().toUpperCase()
|
||||
: departureAirport.code);
|
||||
const arr = (typeof arrivalAirport === "string"
|
||||
? arrivalAirport.trim().toUpperCase()
|
||||
: arrivalAirport.code);
|
||||
const dep = departureCode.trim().toUpperCase();
|
||||
const arr = arrivalCode.trim().toUpperCase();
|
||||
if (!dep || !arr) return;
|
||||
|
||||
// Empty dates default to the current week (today → today + 7) so
|
||||
@@ -202,7 +160,7 @@ export const ScheduleStartPage: FC = () => {
|
||||
|
||||
void navigate(`/${locale}/${url}`);
|
||||
},
|
||||
[departureAirport, arrivalAirport, dateFrom, dateTo, timeRange, directOnly, isRoundTrip, returnDateFrom, returnDateTo, returnTimeRange, navigate, locale],
|
||||
[departureCode, arrivalCode, dateFrom, dateTo, timeRange, directOnly, isRoundTrip, returnDateFrom, returnDateTo, returnTimeRange, navigate, locale],
|
||||
);
|
||||
|
||||
const handlePopularRequestClick = useCallback(
|
||||
@@ -240,31 +198,23 @@ export const ScheduleStartPage: FC = () => {
|
||||
data-testid="schedule-search-form"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="schedule-start__field">
|
||||
<label htmlFor="schedule-departure">{t("SHARED.DEPARTURE_CITY")}</label>
|
||||
<AutoComplete
|
||||
value={departureAirport}
|
||||
suggestions={departureSuggestions}
|
||||
completeMethod={handleDepartureSearch}
|
||||
field="name"
|
||||
dropdown
|
||||
onChange={(e) => setDepartureAirport(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
inputId="schedule-departure"
|
||||
data-testid="departure-input"
|
||||
/>
|
||||
</div>
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.DEPARTURE_CITY")}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
value={departureCode}
|
||||
onChange={setDepartureCode}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="schedule-departure"
|
||||
/>
|
||||
|
||||
<div className="change-container">
|
||||
<button
|
||||
className="button-change"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const tmp = departureAirport;
|
||||
setDepartureAirport(arrivalAirport);
|
||||
setArrivalAirport(tmp);
|
||||
const tmp = departureCode;
|
||||
setDepartureCode(arrivalCode);
|
||||
setArrivalCode(tmp);
|
||||
}}
|
||||
data-testid="swap-cities-button"
|
||||
>
|
||||
@@ -274,22 +224,14 @@ export const ScheduleStartPage: FC = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="schedule-start__field">
|
||||
<label htmlFor="schedule-arrival">{t("SHARED.ARRIVAL_CITY")}</label>
|
||||
<AutoComplete
|
||||
value={arrivalAirport}
|
||||
suggestions={arrivalSuggestions}
|
||||
completeMethod={handleArrivalSearch}
|
||||
field="name"
|
||||
dropdown
|
||||
onChange={(e) => setArrivalAirport(e.value as CitySuggestion | string)}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
className="input--filter"
|
||||
inputClassName="input--filter"
|
||||
inputId="schedule-arrival"
|
||||
data-testid="arrival-input"
|
||||
/>
|
||||
</div>
|
||||
<CityAutocomplete
|
||||
label={t("SHARED.ARRIVAL_CITY")}
|
||||
placeholder={t("SHARED.CITY_PLACEHOLDER")}
|
||||
value={arrivalCode}
|
||||
onChange={setArrivalCode}
|
||||
dictionaries={dictionaries}
|
||||
testIdPrefix="schedule-arrival"
|
||||
/>
|
||||
|
||||
<div className="schedule-start__field">
|
||||
<label htmlFor="schedule-date-from">{t("SHARED.SCHEDULES_DATE")}</label>
|
||||
|
||||
Reference in New Issue
Block a user