From e05ef1ca202bf395ede6b8cdd24bad782ac2ca5a Mon Sep 17 00:00:00 2001 From: gnezim Date: Mon, 20 Apr 2026 00:30:39 +0300 Subject: [PATCH] Render rich Schedule details page + fix broken SEO key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule details page used to show only a one-line FlightCard and stop. Reuse ScheduleFlightBody so each flight in the chain renders the same per-leg layout the schedule results page uses (route summary, leg cards, transit pill, share/Купить/Детали рейса actions). Add a `Вернуться к Расписанию` back link to the header. While here, fix the SEO title key — buildScheduleDetailsSeo was calling SEO.SCHEDULE.DETAILS.TITLE with `flights={...}`, but the i18n bundle only defines SEO.SCHEDULE.FLIGHT-DETAILS.TITLE with `flightNumber={...}`. The unresolved key was leaking into the document title as "SEO.SCHEDULE.DETAILS.TITLE". --- .../components/ScheduleDetailsPage.scss | 71 +++++++++++++++++++ .../components/ScheduleDetailsPage.tsx | 70 ++++++++++-------- src/features/schedule/seo.test.ts | 6 +- src/features/schedule/seo.ts | 8 +-- 4 files changed, 119 insertions(+), 36 deletions(-) create mode 100644 src/features/schedule/components/ScheduleDetailsPage.scss diff --git a/src/features/schedule/components/ScheduleDetailsPage.scss b/src/features/schedule/components/ScheduleDetailsPage.scss new file mode 100644 index 00000000..f5b3bfbd --- /dev/null +++ b/src/features/schedule/components/ScheduleDetailsPage.scss @@ -0,0 +1,71 @@ +@use "../../../styles/colors" as colors; +@use "../../../styles/variables" as vars; +@use "../../../styles/fonts" as fonts; + +.schedule-details { + display: flex; + flex-direction: column; + gap: vars.$space-l; + + &__header-left { + display: flex; + flex-direction: column; + gap: vars.$space-m; + } + + // `Вернуться к Расписанию` back link mirrors Angular's + // `details-back` button. White-translucent pill on the dark + // header strip. + &__back { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: rgba(255, 255, 255, 0.92); + color: colors.$blue; + border-radius: 4px; + font-size: 14px; + font-weight: fonts.$font-medium; + text-decoration: none; + transition: background-color 120ms ease; + + &:hover { background: #fff; } + } + + &__back-arrow { + font-size: 16px; + line-height: 1; + } + + &__flight { + display: flex; + flex-direction: column; + background: #fff; + } + + &__header { + display: flex; + align-items: baseline; + gap: vars.$space-m; + padding: vars.$space-l vars.$space-xl 0; + } + + &__flight-number { + font-size: 22px; + font-weight: fonts.$font-medium; + color: #1c2330; + margin: 0; + } + + &__status { + color: #6b7280; + font-size: 14px; + } + + &--error, + &--not-found { + padding: vars.$space-xl; + color: colors.$red; + text-align: center; + } +} diff --git a/src/features/schedule/components/ScheduleDetailsPage.tsx b/src/features/schedule/components/ScheduleDetailsPage.tsx index 259b8c2b..62dd9021 100644 --- a/src/features/schedule/components/ScheduleDetailsPage.tsx +++ b/src/features/schedule/components/ScheduleDetailsPage.tsx @@ -9,6 +9,7 @@ */ import type { FC } from "react"; +import { Link } from "@modern-js/runtime/router"; import { useTranslation } from "@/i18n/provider.js"; import { FlightCard } from "@/ui/flights/FlightCard.js"; import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js"; @@ -19,7 +20,9 @@ import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; import { useScheduleDetails } from "../hooks/useScheduleDetails.js"; import { buildScheduleDetailsSeo } from "../seo.js"; import { buildScheduleFlightJsonLd } from "../json-ld.js"; -import type { IScheduleFlightId, IFlightLeg } from "../types.js"; +import { ScheduleFlightBody } from "./ScheduleFlightBody.js"; +import type { IScheduleFlightId, IFlightLeg, ISimpleFlight } from "../types.js"; +import "./ScheduleDetailsPage.scss"; export interface ScheduleDetailsPageProps { /** Parsed flight identifiers from the catch-all URL */ @@ -74,6 +77,18 @@ export const ScheduleDetailsPage: FC = ({ const pageTabs = ; const scheduleHref = `/${locale}/schedule`; + const backLink = ( + + + {t("SHARED.BACK-SCHEDULE") || t("SHARED.BACK")} + + ); const title = flightIds[0] ? `${t("BOARD.FLIGHT-INFO")}: ${flightIds[0].carrier} ${flightIds[0].flightNumber}` : t("SCHEDULE.TITLE"); @@ -127,9 +142,22 @@ export const ScheduleDetailsPage: FC = ({ // SEO const seoProps = buildScheduleDetailsSeo(t, flights, locale, canonicalOrigin, flightIds); + // Single-flight chains render one schedule body. Multi-flight chains + // (connecting itineraries assembled from the URL) render the rich + // body for each, separated by a transit caption — close to Angular's + // `schedule-flight-details-view` for the connecting case. + const renderBody = (flight: typeof flights[number]) => ( + + ); + return ( + {backLink} + {pageTabs} + + } title={

{title}

} breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]} > @@ -138,44 +166,28 @@ export const ScheduleDetailsPage: FC = ({
{flights.map((flight) => { const jsonLd = buildScheduleFlightJsonLd(flight); - const legs = getLegs(flight); const flightNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`; return ( -
+
-

{flightNumber}

+

+ {flightNumber} +

{t(`FLIGHT-STATUSES.${flight.status}`)}
- - - {/* Per-leg aircraft info (multi-leg routes only — for a - direct flight the FlightCard summary already shows the - route and times, duplicating them here is noise). */} - {legs.length > 1 && - legs.map((leg, i) => ( -
-
- {leg.departure.scheduled.airportCode} - - {leg.arrival.scheduled.airportCode} -
- {leg.equipment.name && ( -
- {leg.equipment.name} -
- )} -
- ))} + {/* Collapsed summary row, then the rich per-leg body. */} + + {renderBody(flight)}
); })} diff --git a/src/features/schedule/seo.test.ts b/src/features/schedule/seo.test.ts index 6922f51d..e55d5fa0 100644 --- a/src/features/schedule/seo.test.ts +++ b/src/features/schedule/seo.test.ts @@ -196,11 +196,11 @@ describe("buildScheduleDetailsSeo", () => { }, }; - it("uses DETAILS translation keys", () => { + it("uses FLIGHT-DETAILS translation keys", () => { const result = buildScheduleDetailsSeo(stubT, [flight], "ru", CANONICAL, flightIds); - expect(result.title).toContain("SEO.SCHEDULE.DETAILS.TITLE"); - expect(result.title).toContain("flights=SU 0012"); + expect(result.title).toContain("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE"); + expect(result.title).toContain("flightNumber=SU 0012"); expect(result.title).toContain("date=27.05.2022"); }); diff --git a/src/features/schedule/seo.ts b/src/features/schedule/seo.ts index bbc7871a..54ac14f0 100644 --- a/src/features/schedule/seo.ts +++ b/src/features/schedule/seo.ts @@ -188,12 +188,12 @@ export function buildScheduleDetailsSeo( ? formatDateForSeo(flightIds[0].date) : ""; - const title = t("SEO.SCHEDULE.DETAILS.TITLE", { - flights: flightDisplay, + const title = t("SEO.SCHEDULE.FLIGHT-DETAILS.TITLE", { + flightNumber: flightDisplay, date: dateDisplay, }); - const description = t("SEO.SCHEDULE.DETAILS.DESCRIPTION", { - flights: flightDisplay, + const description = t("SEO.SCHEDULE.FLIGHT-DETAILS.DESCRIPTION", { + flightNumber: flightDisplay, date: dateDisplay, });