Render rich Schedule details page + fix broken SEO key
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".
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ScheduleDetailsPageProps> = ({
|
||||
|
||||
const pageTabs = <PageTabs viewType="schedule" />;
|
||||
const scheduleHref = `/${locale}/schedule`;
|
||||
const backLink = (
|
||||
<Link
|
||||
to={scheduleHref}
|
||||
className="schedule-details__back"
|
||||
data-testid="schedule-details-back"
|
||||
>
|
||||
<span aria-hidden="true" className="schedule-details__back-arrow">
|
||||
{"\u2190"}
|
||||
</span>
|
||||
{t("SHARED.BACK-SCHEDULE") || t("SHARED.BACK")}
|
||||
</Link>
|
||||
);
|
||||
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<ScheduleDetailsPageProps> = ({
|
||||
// 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]) => (
|
||||
<ScheduleFlightBody flight={flight as unknown as ISimpleFlight} />
|
||||
);
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
headerLeft={pageTabs}
|
||||
headerLeft={
|
||||
<div className="schedule-details__header-left">
|
||||
{backLink}
|
||||
{pageTabs}
|
||||
</div>
|
||||
}
|
||||
title={<h1 className="text--white page-title">{title}</h1>}
|
||||
breadcrumbs={[{ label: t("SCHEDULE.TITLE"), url: scheduleHref }]}
|
||||
>
|
||||
@@ -138,44 +166,28 @@ export const ScheduleDetailsPage: FC<ScheduleDetailsPageProps> = ({
|
||||
<div className="schedule-details" data-testid="schedule-details">
|
||||
{flights.map((flight) => {
|
||||
const jsonLd = buildScheduleFlightJsonLd(flight);
|
||||
const legs = getLegs(flight);
|
||||
const flightNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`;
|
||||
|
||||
return (
|
||||
<div key={flight.id} className="schedule-details__flight" data-testid={`flight-${flight.id}`}>
|
||||
<div
|
||||
key={flight.id}
|
||||
className="schedule-details__flight"
|
||||
data-testid={`flight-${flight.id}`}
|
||||
>
|
||||
<JsonLdRenderer data={jsonLd} />
|
||||
|
||||
<div className="schedule-details__header">
|
||||
<h2 className="schedule-details__flight-number">{flightNumber}</h2>
|
||||
<h2 className="schedule-details__flight-number">
|
||||
{flightNumber}
|
||||
</h2>
|
||||
<span className="schedule-details__status">
|
||||
{t(`FLIGHT-STATUSES.${flight.status}`)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<FlightCard flight={flight} />
|
||||
|
||||
{/* 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) => (
|
||||
<div
|
||||
key={`leg-${leg.index ?? i}`}
|
||||
className="schedule-details__leg"
|
||||
data-testid={`leg-${leg.index ?? i}`}
|
||||
>
|
||||
<div className="schedule-details__leg-route">
|
||||
<span>{leg.departure.scheduled.airportCode}</span>
|
||||
<span>→</span>
|
||||
<span>{leg.arrival.scheduled.airportCode}</span>
|
||||
</div>
|
||||
{leg.equipment.name && (
|
||||
<div className="schedule-details__aircraft">
|
||||
{leg.equipment.name}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* Collapsed summary row, then the rich per-leg body. */}
|
||||
<FlightCard flight={flight} direction="schedule" />
|
||||
{renderBody(flight)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user