diff --git a/src/features/online-board/components/OnlineBoardStartPage.tsx b/src/features/online-board/components/OnlineBoardStartPage.tsx index f6c80e73..9d4c5142 100644 --- a/src/features/online-board/components/OnlineBoardStartPage.tsx +++ b/src/features/online-board/components/OnlineBoardStartPage.tsx @@ -26,8 +26,25 @@ import { readAndClearTransientPrefill, writeTransientPrefill, } from "@/shared/state/transientPrefill.js"; +import { + useDictionaries, + getCityCodeByAirportCode, +} from "@/shared/dictionaries/index.js"; +import type { IDictionaries } from "@/shared/dictionaries/index.js"; import "./OnlineBoardStartPage.scss"; +/** + * Normalize a popular-request IATA code to its owning city code so that + * clicking "Шереметьево - Санкт-Петербург" (SVO → LED) lands on the same + * Moscow → St Petersburg prefill as any MOW-coded entry. Falls back to + * the original code when the airport has no parent city or when the + * dictionaries aren't loaded yet. + */ +function toCityCode(code: string, dictionaries: IDictionaries | null): string { + if (!dictionaries) return code; + return getCityCodeByAirportCode(dictionaries, code) ?? code; +} + export const ONLINE_BOARD_PREFILL_SLOT = "online-board"; export const SCHEDULE_PREFILL_SLOT = "schedule"; @@ -49,6 +66,7 @@ export interface OnlineBoardPrefillState { export function buildOnlineBoardPrefillState( request: PopularRequest, + dictionaries: IDictionaries | null = null, ): OnlineBoardPrefillState { switch (request.mode) { case "FlightNumber": @@ -57,15 +75,15 @@ export function buildOnlineBoardPrefillState( flightNumber: `${request.carrier}${request.flightNumber}`, }; case "Departure": - return { tab: "route", departure: request.departure }; + return { tab: "route", departure: toCityCode(request.departure, dictionaries) }; case "Arrival": - return { tab: "route", arrival: request.arrival }; + return { tab: "route", arrival: toCityCode(request.arrival, dictionaries) }; case "Route": case "RouteWithBack": return { tab: "route", - departure: request.departure, - arrival: request.arrival, + departure: toCityCode(request.departure, dictionaries), + arrival: toCityCode(request.arrival, dictionaries), }; } } @@ -73,7 +91,8 @@ export function buildOnlineBoardPrefillState( export const OnlineBoardStartPage: FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { locale } = useLocale(); + const { locale, language } = useLocale(); + const { dictionaries } = useDictionaries(language); // Read-and-clear any prefill the previous page wrote. Stored in // useState (with a one-shot initializer) so React strict mode's @@ -97,13 +116,17 @@ export const OnlineBoardStartPage: FC = () => { const handlePopularRequestClick = useCallback( (request: PopularRequest) => { - // Schedule-type requests navigate to the schedule feature + // Schedule-type requests navigate to the schedule feature. City + // codes come off the popular-request API as airport codes in some + // cases (SVO/LED instead of MOW/LED); resolve them to owning city + // so the destination form pre-fills with a city rather than an + // airport pin. if (request.type === "Schedule") { const state = request.mode === "Route" || request.mode === "RouteWithBack" ? { - departure: request.departure, - arrival: request.arrival, + departure: toCityCode(request.departure, dictionaries), + arrival: toCityCode(request.arrival, dictionaries), withReturn: request.mode === "RouteWithBack", } : {}; @@ -115,10 +138,10 @@ export const OnlineBoardStartPage: FC = () => { // Onlineboard request — same page. Update local prefill + // remount the filter via key bump so its useState initializers // see the new initial* props. - setPrefill(buildOnlineBoardPrefillState(request)); + setPrefill(buildOnlineBoardPrefillState(request, dictionaries)); setFilterKey((n) => n + 1); }, - [navigate, locale], + [navigate, locale, dictionaries], ); return ( diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx index ec00219f..bc92fbc8 100644 --- a/src/features/schedule/components/ScheduleStartPage.test.tsx +++ b/src/features/schedule/components/ScheduleStartPage.test.tsx @@ -67,6 +67,14 @@ vi.mock("@/ui/layout/SearchHistory.js", () => ({ SearchHistory: () =>
, })); +// Dictionaries aren't needed for the popular-request behavior the test +// exercises — return nulls so toCityCode() falls back to the raw code +// (test assertions expect the unresolved SVO/LED codes). +vi.mock("@/shared/dictionaries/index.js", () => ({ + useDictionaries: () => ({ dictionaries: null, loading: false, error: null }), + getCityCodeByAirportCode: () => undefined, +})); + describe("ScheduleStartPage", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx index fbc3e119..b311088c 100644 --- a/src/features/schedule/components/ScheduleStartPage.tsx +++ b/src/features/schedule/components/ScheduleStartPage.tsx @@ -29,9 +29,19 @@ import { readAndClearTransientPrefill, writeTransientPrefill, } from "@/shared/state/transientPrefill.js"; +import { + useDictionaries, + getCityCodeByAirportCode, +} from "@/shared/dictionaries/index.js"; +import type { IDictionaries } from "@/shared/dictionaries/index.js"; import { buildScheduleUrl } from "../url.js"; import "./ScheduleStartPage.scss"; +function toCityCode(code: string, dictionaries: IDictionaries | null): string { + if (!dictionaries) return code; + return getCityCodeByAirportCode(dictionaries, code) ?? code; +} + function minutesToTime(minutes: number): string { const h = Math.floor(minutes / 60); const m = minutes % 60; @@ -67,7 +77,8 @@ export interface SchedulePrefillState { export const ScheduleStartPage: FC = () => { const navigate = useNavigate(); const { t } = useTranslation(); - const { locale } = useLocale(); + const { locale, language } = useLocale(); + const { dictionaries } = useDictionaries(language); // One-shot read of any prefill the previous page wrote. const [prefill] = useState( @@ -166,10 +177,13 @@ export const ScheduleStartPage: FC = () => { const handlePopularRequestClick = useCallback( (request: PopularRequest) => { + // Popular-request entries sometimes carry airport codes (SVO, LED) + // instead of city codes (MOW, LED). Normalize to the owning city + // so clicks land on the city filter, not on a specific airport. if (request.type === "Onlineboard") { writeTransientPrefill( ONLINE_BOARD_PREFILL_SLOT, - buildOnlineBoardPrefillState(request), + buildOnlineBoardPrefillState(request, dictionaries), ); navigate(`/${locale}/onlineboard`); return; @@ -179,15 +193,15 @@ export const ScheduleStartPage: FC = () => { const state: SchedulePrefillState = request.mode === "Route" || request.mode === "RouteWithBack" ? { - departure: request.departure, - arrival: request.arrival, + departure: toCityCode(request.departure, dictionaries), + arrival: toCityCode(request.arrival, dictionaries), withReturn: request.mode === "RouteWithBack", } : {}; writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state); navigate(`/${locale}/schedule`); }, - [navigate, locale], + [navigate, locale, dictionaries], ); const scheduleFilter = (