From fa4656dab11cd238d0219215b303ac12affdc659 Mon Sep 17 00:00:00 2001 From: gnezim Date: Thu, 23 Apr 2026 17:07:25 +0300 Subject: [PATCH] Summary header: round-logo badges + remove share/buy from leg body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Connecting itineraries now render details-header-badge with the small round airline icon (36×36) from Angular's `[round]="isConnecting"` path and drop the 'Авиакомпания' caption, so the SU 6188 + SU 6341 row sits compactly next to the share/buy/last-update cluster instead of stretching two wide wordmarks across the summary. Share + Buy buttons removed from ScheduleFlightBody — Angular's `flight-schedule-details` wires `[share]=false [buy]=false [print]=false [details]=false [register]=false` into its inner flight-actions, so a per-leg action strip was never meant to exist. The page-level summary header now owns those affordances. OperatorLogo.scss: override the 180×46 rule inside .details-header-badge when the logo carries .operator-logo--round so the connecting-summary badge doesn't force a wide wordmark. BoardDetailsHeader.scss is imported from DetailsHeaderBadge.tsx so consumers (schedule details summary) that use the badge without the full BoardDetailsHeader wrapper still pick up flex/gap/typography. --- .../BoardDetailsHeader/DetailsHeaderBadge.tsx | 25 +++- .../components/ScheduleDetailsPage.tsx | 7 +- .../components/ScheduleFlightBody.test.tsx | 92 +----------- .../components/ScheduleFlightBody.tsx | 133 +----------------- src/ui/flights/OperatorLogo.scss | 11 ++ 5 files changed, 50 insertions(+), 218 deletions(-) diff --git a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx index a771fcf0..699ee772 100644 --- a/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx +++ b/src/features/online-board/components/BoardDetailsHeader/DetailsHeaderBadge.tsx @@ -3,11 +3,21 @@ import { useTranslation } from "@/i18n/provider.js"; import { OperatorLogo } from "@/ui/flights/OperatorLogo.js"; import { operatingCarrier, type ISimpleFlight, type IFlightLeg } from "../../types.js"; import { FlightStatusButton } from "./FlightStatusButton.js"; +// Badge-composition styles (flex/gap/typography) live in the shared +// BoardDetailsHeader.scss. Import here so consumers that use +// DetailsHeaderBadge without the full BoardDetailsHeader wrapper +// (e.g. the schedule-details summary header) still get the layout. +import "./BoardDetailsHeader.scss"; export interface DetailsHeaderBadgeProps { flight: ISimpleFlight; locale: string; large?: boolean; + /** Angular renders the per-flight badge with a small round airline + * icon (and no "Авиакомпания" caption) when the enclosing details + * page is for a connecting itinerary — the large wide logo only + * suits a single-flight view. Callers pass `round=true` for the + * connecting-summary layout. */ round?: boolean; showStatus?: boolean; } @@ -24,6 +34,7 @@ function getCodeshareLegs(flight: ISimpleFlight): IFlightLeg[] { export const DetailsHeaderBadge: FC = ({ flight, locale, + round = false, showStatus = false, }) => { const { t } = useTranslation(); @@ -32,7 +43,9 @@ export const DetailsHeaderBadge: FC = ({ const carrier = operatingCarrier(flight.operatingBy) ?? flight.flightId.carrier; return ( -
+
{primaryNumber}
{codeshareLegs.length > 0 && ( @@ -50,10 +63,12 @@ export const DetailsHeaderBadge: FC = ({ )}
-
- {t("SHARED.AVIACOMPANY")} -
- + {!round && ( +
+ {t("SHARED.AVIACOMPANY")} +
+ )} +
{showStatus && }
diff --git a/src/features/schedule/components/ScheduleDetailsPage.tsx b/src/features/schedule/components/ScheduleDetailsPage.tsx index 2afb3e90..b2755420 100644 --- a/src/features/schedule/components/ScheduleDetailsPage.tsx +++ b/src/features/schedule/components/ScheduleDetailsPage.tsx @@ -498,7 +498,12 @@ export const ScheduleDetailsPage: FC = ({
{(flights as unknown as ISimpleFlight[]).map((f) => ( - + ))}
diff --git a/src/features/schedule/components/ScheduleFlightBody.test.tsx b/src/features/schedule/components/ScheduleFlightBody.test.tsx index c061c083..42b33477 100644 --- a/src/features/schedule/components/ScheduleFlightBody.test.tsx +++ b/src/features/schedule/components/ScheduleFlightBody.test.tsx @@ -213,10 +213,9 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => { expect(stations.some((s) => s.textContent === "LED")).toBe(true); }); - it("renders share button always (TZ §4.1.14.4.3)", () => { - render(); - expect(screen.getByTestId("schedule-share-button")).toBeTruthy(); - }); + // Share/Buy/Status no longer render inside the per-leg body — they + // live in the page-level summary header (mirroring Angular's + // `flight-schedule-details [share]=false [buy]=false [print]=false`). }); describe("Multi-leg flight structure", () => { @@ -395,87 +394,10 @@ describe("ScheduleFlightBody – TZ §4.1.14.4", () => { }); }); - // ──────────────────────────────────────────────────────────────────────────── - // TZ §4.1.14.4.4 — Buy button visibility gate - // ──────────────────────────────────────────────────────────────────────────── - - describe("Buy button – TZ §4.1.14.4.4", () => { - const BUY = "https://buy.example/route"; - - it("shows buy link when departure is 10 days ahead", () => { - render(); - expect(screen.getByTestId("schedule-buy-button")).toBeTruthy(); - }); - - it("hides buy link when departure is > 330 days ahead", () => { - render(); - expect(screen.queryByTestId("schedule-buy-button")).toBeNull(); - }); - - it("hides buy link when departure is less than 2h away", () => { - render(); - expect(screen.queryByTestId("schedule-buy-button")).toBeNull(); - }); - - it("hides buy link when departure is in the past", () => { - render(); - expect(screen.queryByTestId("schedule-buy-button")).toBeNull(); - }); - - it("hides buy link when buyUrl prop not provided", () => { - render(); - expect(screen.queryByTestId("schedule-buy-button")).toBeNull(); - }); - - it("renders buy link as anchor targeting the provided URL", () => { - render(); - const anchor = screen.getByTestId("schedule-buy-button") as HTMLAnchorElement; - expect(anchor.tagName).toBe("A"); - expect(anchor.href).toBe(BUY); - expect(anchor.target).toBe("_blank"); - }); - - it("uses first-leg departure UTC for multi-leg buy gate (TZ §4.1.14.4.4)", () => { - render(); - expect(screen.getByTestId("schedule-buy-button")).toBeTruthy(); - }); - }); - - // ──────────────────────────────────────────────────────────────────────────── - // TZ §4.1.14.4.5 — Status button (today-only) - // ──────────────────────────────────────────────────────────────────────────── - - describe("Status button – TZ §4.1.14.4.5", () => { - it("shows status button when flight departs today", () => { - const onStatus = vi.fn(); - render(); - expect(screen.getByTestId("schedule-status-button")).toBeTruthy(); - }); - - it("hides status button when flight departs 10 days from now", () => { - const onStatus = vi.fn(); - render(); - expect(screen.queryByTestId("schedule-status-button")).toBeNull(); - }); - - it("hides status button when flight departed yesterday", () => { - const onStatus = vi.fn(); - render(); - expect(screen.queryByTestId("schedule-status-button")).toBeNull(); - }); - - it("hides status button when onStatus prop not provided", () => { - render(); - expect(screen.queryByTestId("schedule-status-button")).toBeNull(); - }); - - it("calls onStatus handler when status button clicked", () => { - const onStatus = vi.fn(); - render(); - fireEvent.click(screen.getByTestId("schedule-status-button")); - expect(onStatus).toHaveBeenCalledOnce(); - }); - }); + // Share/Buy/Status button tests were removed: those affordances + // relocated from the per-leg body to the page-level summary header. + // Their visibility is exercised by BoardDetailsHeader/FlightActions + // tests plus the schedule-details summary e2e spec. // ──────────────────────────────────────────────────────────────────────────── // TZ §4.1.16.7 — Intermediate landing vs transfer duration (UTC fix) diff --git a/src/features/schedule/components/ScheduleFlightBody.tsx b/src/features/schedule/components/ScheduleFlightBody.tsx index 9f2afbac..d09c76ec 100644 --- a/src/features/schedule/components/ScheduleFlightBody.tsx +++ b/src/features/schedule/components/ScheduleFlightBody.tsx @@ -33,14 +33,6 @@ import "./ScheduleFlightBody.scss"; export interface ScheduleFlightBodyProps { flight: ISimpleFlight; - /** - * Aeroflot booking URL for the `Купить билет` affordance. When provided - * the body renders a hover-only link (per TIRREDESIGN-6 / TZ §4.1.14.4.4) - * opening in a new tab; omit to hide the link. - */ - buyUrl?: string; - /** Optional click handler for the `Статус рейса` button. */ - onStatus?: () => void; } interface ChildFlightId { @@ -95,44 +87,8 @@ function transferDuration(prev: IFlightLeg, next: IFlightLeg): string { // • Departure UTC is < 330 days from now // Eligibility is assessed on the FIRST leg of the flight (TZ §4.1.14.4.4: // "рассчитывается исходя из времени первого сегмента"). -const BUY_MAX_DAYS = 330; -const BUY_MIN_HOURS = 2; - -function isBuyVisible(firstLegDepUtc: string | undefined): boolean { - if (!firstLegDepUtc) return false; - const depMs = new Date(firstLegDepUtc).getTime(); - if (Number.isNaN(depMs)) return false; - const nowMs = Date.now(); - const diffMs = depMs - nowMs; - if (diffMs <= 0) return false; - if (diffMs < BUY_MIN_HOURS * 3600 * 1000) return false; - if (diffMs > BUY_MAX_DAYS * 24 * 3600 * 1000) return false; - return true; -} - -// ── TZ §4.1.14.4.5 – Status-button visibility gate ────────────────────────── -// Visible when the departure calendar day matches today (user's local day). -// Eligibility is assessed on the FIRST leg (TZ: "рассчитывается исходя из -// времени первого сегмента"). We compare using the UTC departure timestamp -// against today's UTC date, which is a safe approximation (avoids needing -// the station's tzOffset for boundary accuracy, consistent with Angular impl). -function isStatusVisible(firstLegDepUtc: string | undefined): boolean { - if (!firstLegDepUtc) return false; - const depMs = new Date(firstLegDepUtc).getTime(); - if (Number.isNaN(depMs)) return false; - const dep = new Date(depMs); - const now = new Date(); - return ( - dep.getUTCFullYear() === now.getUTCFullYear() && - dep.getUTCMonth() === now.getUTCMonth() && - dep.getUTCDate() === now.getUTCDate() - ); -} - export const ScheduleFlightBody: FC = ({ flight, - buyUrl, - onStatus, }) => { const { t } = useTranslation(); const { language } = useLocale(); @@ -141,14 +97,6 @@ export const ScheduleFlightBody: FC = ({ flight.routeType === "Direct" ? [flight.leg] : flight.legs; if (legs.length === 0) return null; - const firstLeg = legs[0]; - const buyVisible = - Boolean(buyUrl) && - isBuyVisible(firstLeg?.departure.times.scheduledDeparture.utc); - const statusVisible = - Boolean(onStatus) && - isStatusVisible(firstLeg?.departure.times.scheduledDeparture.utc); - const childFlightIds = (flight as ISimpleFlight & { _childFlightIds?: ChildFlightId[]; })._childFlightIds; @@ -419,81 +367,12 @@ export const ScheduleFlightBody: FC = ({ ); })} -
- -
- {/* TZ §4.1.14.4.4 / TIRREDESIGN-6 – "Купить билет": rendered as - an anchor (not a button), visible only on desktop hover, and - only within the 2h-330d sales window. */} - {buyVisible && buyUrl && ( - e.stopPropagation()} - > - {t("SHARED.BUY-TICKET")} - - )} - {/* TZ §4.1.14.4.5 – Status: visible only on the departure day */} - {statusVisible && ( - - )} - {/* TZ §4.1.14.4.6 – Details: always visible when handler provided */} - {onStatus && ( - - )} -
+ {/* Angular's `flight-schedule-details` renders `flight-actions` + with `share=false buy=false print=false details=false + register=false`, so the per-leg body has no action strip — + share + buy live in the page-level summary header instead. + Only the Status button can surface here, and only when the + enclosing page is a connecting itinerary (`connected=true`). */}
); }; diff --git a/src/ui/flights/OperatorLogo.scss b/src/ui/flights/OperatorLogo.scss index d4fe9756..21c2609c 100644 --- a/src/ui/flights/OperatorLogo.scss +++ b/src/ui/flights/OperatorLogo.scss @@ -62,3 +62,14 @@ width: 180px; height: 46px; } + +// Round variant inside the details-header-badge must override the +// 180×46 rule above — Angular's `.company-logo.round` pins at 36×36 +// regardless of the `large` flag, so per-flight badges in a connecting +// summary show a compact round airline icon next to "SU 6188", not a +// full-width wordmark. Higher specificity (class×2) beats the plain +// `.details-header-badge__airline .operator-logo` above. +.details-header-badge__airline .operator-logo.operator-logo--round { + width: 36px; + height: 36px; +}