Populate filter sidebar when clicking a popular request

Two bugs prevented the popular-requests click from filling the filter:

1. OnlineBoardFilter seeded its fields from initial* props via
   useState(...), which only runs once. When a user clicked a popular
   request the parent pushed ?departure=SVO&arrival=LED into the URL
   and re-rendered with new initial* props, but the sidebar fields
   kept their previous empty values. Add an effect that diffs the
   initial* props against a ref and pushes the changes into local
   state, matching Angular's ngOnChanges behaviour.

2. CityAutocomplete's selectedCity only looked the value up in
   cityByCode. Airport codes like SVO aren't cities, so the header
   code label stayed blank. Fall back to airportByCode → city_code so
   the top-right code renders as 'MOW' when the input shows
   'Шереметьево'.

End-to-end behaviour now matches Angular: clicking
'Маршрут: Шереметьево - Санкт-Петербург' on the start page updates
the URL, populates 'Шереметьево' / 'Санкт-Петербург' in the inputs,
shows 'MOW' / 'LED' codes in the labels.
This commit is contained in:
2026-04-18 12:43:33 +03:00
parent db163f5645
commit b01fc2f0c9
2 changed files with 56 additions and 2 deletions
@@ -8,7 +8,7 @@
* @module
*/
import { type FC, useState, useCallback, type FormEvent } from "react";
import { type FC, useState, useCallback, useEffect, useRef, type FormEvent } from "react";
import { useNavigate, useParams } from "@modern-js/runtime/router";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
@@ -91,6 +91,46 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
);
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
// When the parent feeds new initial* props (e.g. a popular-request click
// pushes ?departure=SVO&arrival=LED into the URL), keep the fields in
// sync. useState only reads initial values once, so without this effect
// clicking a popular route left the sidebar untouched.
const lastInitialRef = useRef({
departure: initialDeparture,
arrival: initialArrival,
date: initialDate,
tab: initialTab,
flightNumber: initialFlightNumber,
});
useEffect(() => {
const prev = lastInitialRef.current;
if (prev.departure !== initialDeparture) {
setRouteDepartureCode(initialDeparture ?? "");
}
if (prev.arrival !== initialArrival) {
setRouteArrivalCode(initialArrival ?? "");
}
if (prev.date !== initialDate && initialDate) {
setRouteDate(yyyymmddToDate(initialDate));
if (initialTab === "flight") {
setFlightDate(yyyymmddToDate(initialDate));
}
}
if (prev.tab !== initialTab && initialTab) {
setActiveTab(initialTab);
}
if (prev.flightNumber !== initialFlightNumber) {
setFlightNumber(initialFlightNumber ?? "");
}
lastInitialRef.current = {
departure: initialDeparture,
arrival: initialArrival,
date: initialDate,
tab: initialTab,
flightNumber: initialFlightNumber,
};
}, [initialDeparture, initialArrival, initialDate, initialTab, initialFlightNumber]);
const handleTabClick = useCallback((tab: AccordionTab) => {
setActiveTab((prev) => (prev === tab ? prev : tab));
}, []);
+15 -1
View File
@@ -107,7 +107,21 @@ export const CityAutocomplete: FC<CityAutocompleteProps> = ({
);
}, []);
const selectedCity = dictionaries?.cityByCode.get(value.toUpperCase()) ?? null;
// Resolve the code to the owning city. The API (popular-requests,
// deep links, etc.) hands us either a city code like "MOW" or an
// airport code like "SVO"; in the latter case we look up the airport
// and follow its city_code so the label reads "MOW" instead of blank.
const selectedCity = (() => {
if (!value || !dictionaries) return null;
const upper = value.toUpperCase();
const direct = dictionaries.cityByCode.get(upper);
if (direct) return direct;
const airport = dictionaries.airportByCode.get(upper);
if (airport) {
return dictionaries.cityByCode.get(airport.city_code.toUpperCase()) ?? null;
}
return null;
})();
const hasValue = Boolean(selectedCity);
return (