Schedule search → details links emit ?request= + details page shows leaf breadcrumb (TZ §4.1.2 row 11, §4.1.4 rows 11-13)

This commit is contained in:
2026-04-21 17:45:36 +03:00
parent 12807cf085
commit 5728861c5c
12 changed files with 126 additions and 18 deletions
@@ -423,7 +423,7 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
// city (the station). We show only that city. See AGENTS.md Conflicts register.
const detailsCrumbs = useMemo(() => {
const baseCrumbs = [{ label: t("BOARD.TITLE"), url: `/${locale}/onlineboard` }];
if (!parentRequest) return baseCrumbs;
if (!parentRequest || parentRequest.area !== "onlineboard") return baseCrumbs;
const backUrl = (() => {
switch (parentRequest.kind) {
@@ -484,7 +484,7 @@ export const OnlineBoardDetailsPage: FC<OnlineBoardDetailsPageProps> = ({
}, [parentRequest, locale, t]);
const parentParams = useMemo(() => {
if (!parentRequest) return null;
if (!parentRequest || parentRequest.area !== "onlineboard") return null;
const d = parentRequest.date;
const isoDate = `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
const dateFrom = `${isoDate}T00:00:00`;
@@ -9,8 +9,8 @@
*/
import type { FC } from "react";
import { useCallback } from "react";
import { Link } from "@modern-js/runtime/router";
import { useCallback, useMemo } from "react";
import { Link, useSearchParams } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import { localeToLanguage, normalizeLocaleParam, DEFAULT_LANGUAGE } from "@/i18n/resolver.js";
import { FlightCard } from "@/ui/flights/FlightCard.js";
@@ -20,6 +20,8 @@ import { SeoHead } from "@/ui/seo/SeoHead.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
import { parseDetailsRequestParam } from "@/shared/detailsRequestParam.js";
import { buildScheduleUrl } from "../url.js";
import { useScheduleDetails } from "../hooks/useScheduleDetails.js";
import { buildScheduleDetailsSeo } from "../seo.js";
import { buildScheduleFlightJsonLd } from "../json-ld.js";
@@ -83,6 +85,72 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
const pageTabs = <PageTabs viewType="schedule" />;
const scheduleHref = `/${locale}/schedule`;
// Parse ?request= to build the leaf breadcrumb (TZ §4.1.4 Table 7 rows 11-13)
const [searchParams] = useSearchParams();
const parentRequest = useMemo(() => {
const raw = searchParams.get("request");
return raw ? parseDetailsRequestParam(raw) : null;
}, [searchParams]);
const breadcrumbs = useMemo(() => {
const baseCrumbs = [{ label: t("SCHEDULE.TITLE"), url: scheduleHref }];
if (!parentRequest || parentRequest.area !== "schedule") return baseCrumbs;
// Build the back URL from the parsed request context
const backUrl = `/${locale}/${buildScheduleUrl(
parentRequest.returnTrip
? {
type: "roundtrip",
outbound: {
departure: parentRequest.departure,
arrival: parentRequest.arrival,
dateFrom: parentRequest.dateFrom,
dateTo: parentRequest.dateTo,
...(parentRequest.timeFrom && parentRequest.timeTo
? { timeFrom: parentRequest.timeFrom, timeTo: parentRequest.timeTo }
: {}),
...(parentRequest.connections !== undefined
? { connections: parentRequest.connections }
: {}),
},
inbound: {
departure: parentRequest.returnTrip.departure,
arrival: parentRequest.returnTrip.arrival,
dateFrom: parentRequest.returnTrip.dateFrom,
dateTo: parentRequest.returnTrip.dateTo,
...(parentRequest.returnTrip.timeFrom && parentRequest.returnTrip.timeTo
? { timeFrom: parentRequest.returnTrip.timeFrom, timeTo: parentRequest.returnTrip.timeTo }
: {}),
...(parentRequest.returnTrip.connections !== undefined
? { connections: parentRequest.returnTrip.connections }
: {}),
},
}
: {
type: "route",
outbound: {
departure: parentRequest.departure,
arrival: parentRequest.arrival,
dateFrom: parentRequest.dateFrom,
dateTo: parentRequest.dateTo,
...(parentRequest.timeFrom && parentRequest.timeTo
? { timeFrom: parentRequest.timeFrom, timeTo: parentRequest.timeTo }
: {}),
...(parentRequest.connections !== undefined
? { connections: parentRequest.connections }
: {}),
},
},
)}`;
const leafLabel = t("BREADCRUMBS.SCHEDULE-ROUTE", {
departureCity: parentRequest.departure,
arrivalCity: parentRequest.arrival,
});
return [...baseCrumbs, { label: leafLabel, url: backUrl }];
}, [parentRequest, locale, scheduleHref, t]);
// `Купить` button — opens Aeroflot's booking flow in a new tab.
// Mirrors BoardDetailsHeader's BuyTicketButton / Schedule search page.
const language =
@@ -128,7 +196,7 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
<PageLayout
headerLeft={pageTabs}
title={<h1 className="text--white page-title">{title}</h1>}
breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]}
breadcrumbs={breadcrumbs}
>
<section className="frame">
<FlightListSkeleton count={flightIds.length} />
@@ -142,7 +210,7 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
<PageLayout
headerLeft={pageTabs}
title={<h1 className="text--white page-title">{title}</h1>}
breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]}
breadcrumbs={breadcrumbs}
>
<section className="frame">
<div
@@ -162,7 +230,7 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
<PageLayout
headerLeft={pageTabs}
title={<h1 className="text--white page-title">{title}</h1>}
breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]}
breadcrumbs={breadcrumbs}
>
<section className="frame">
<div
@@ -26,6 +26,7 @@ import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
import { useScheduleSearch } from "../hooks/useScheduleSearch.js";
import { buildScheduleUrl } from "../url.js";
import { buildFlightUrlParams } from "../../online-board/url.js";
import { buildDetailsRequestParam } from "@/shared/detailsRequestParam.js";
import { buildScheduleFlightListJsonLd } from "../json-ld.js";
import type { ScheduleParams } from "../url.js";
import type { IScheduleSearchRequest, ISimpleFlight } from "../types.js";
@@ -137,9 +138,39 @@ export const ScheduleSearchPage: FC<ScheduleSearchPageProps> = ({ params }) => {
...(flight.flightId.suffix ? { suffix: flight.flightId.suffix } : {}),
date: flight.flightId.date,
});
void navigate(`/${locale}/schedule/${segment}`);
const requestParam = buildDetailsRequestParam({
area: "schedule",
kind: "route",
departure: outbound.departure,
arrival: outbound.arrival,
dateFrom: outbound.dateFrom,
dateTo: outbound.dateTo,
...(outbound.timeFrom && outbound.timeTo
? { timeFrom: outbound.timeFrom, timeTo: outbound.timeTo }
: {}),
...(outbound.connections !== undefined
? { connections: outbound.connections }
: {}),
...(inbound
? {
returnTrip: {
departure: inbound.departure,
arrival: inbound.arrival,
dateFrom: inbound.dateFrom,
dateTo: inbound.dateTo,
...(inbound.timeFrom && inbound.timeTo
? { timeFrom: inbound.timeFrom, timeTo: inbound.timeTo }
: {}),
...(inbound.connections !== undefined
? { connections: inbound.connections }
: {}),
},
}
: {}),
});
void navigate(`/${locale}/schedule/${segment}?request=${encodeURIComponent(requestParam)}`);
},
[locale, navigate],
[locale, navigate, outbound, inbound],
);
// `Купить` button — opens Aeroflot's booking flow in a new tab, same
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}
+2 -1
View File
@@ -53,7 +53,8 @@
"FLIGHT-NUMBER": "Flight: {flightNumber}",
"DEPARTURE": "Departure: {city}",
"ARRIVAL": "Arrival: {city}",
"ROUTE": "Route: {departureCity}-{arrivalCity}"
"ROUTE": "Route: {departureCity}-{arrivalCity}",
"SCHEDULE-ROUTE": "{departureCity}-{arrivalCity}"
},
"DETAILS": {
"REGISTRATION": "Check-in",
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}
+2 -1
View File
@@ -53,7 +53,8 @@
"FLIGHT-NUMBER": "Номер рейса: {flightNumber}",
"DEPARTURE": "Вылет: {city}",
"ARRIVAL": "Прилет: {city}",
"ROUTE": "Маршрут: {departureCity}-{arrivalCity}"
"ROUTE": "Маршрут: {departureCity}-{arrivalCity}",
"SCHEDULE-ROUTE": "{departureCity}-{arrivalCity}"
},
"DETAILS": {
"REGISTRATION": "Регистрация",
+2 -1
View File
@@ -418,6 +418,7 @@
"FLIGHT-NUMBER": "",
"DEPARTURE": "",
"ARRIVAL": "",
"ROUTE": ""
"ROUTE": "",
"SCHEDULE-ROUTE": ""
}
}