From dd43ea6905343e94a46d229aba22c21eb2bbb6a7 Mon Sep 17 00:00:00 2001 From: gnezim Date: Fri, 17 Apr 2026 02:31:42 +0300 Subject: [PATCH] Add TransferBar component for multi-leg transfer info --- .../components/TransferBar/TransferBar.scss | 45 ++++++ .../TransferBar/TransferBar.test.tsx | 130 ++++++++++++++++++ .../components/TransferBar/TransferBar.tsx | 62 +++++++++ .../components/TransferBar/index.ts | 2 + 4 files changed, 239 insertions(+) create mode 100644 src/features/online-board/components/TransferBar/TransferBar.scss create mode 100644 src/features/online-board/components/TransferBar/TransferBar.test.tsx create mode 100644 src/features/online-board/components/TransferBar/TransferBar.tsx create mode 100644 src/features/online-board/components/TransferBar/index.ts diff --git a/src/features/online-board/components/TransferBar/TransferBar.scss b/src/features/online-board/components/TransferBar/TransferBar.scss new file mode 100644 index 00000000..2e26b8d6 --- /dev/null +++ b/src/features/online-board/components/TransferBar/TransferBar.scss @@ -0,0 +1,45 @@ +.transfer-bar { + display: flex; + align-items: center; + gap: 16px; + background: #e3f0ff; + border: 1px solid #b8d4f0; + padding: 12px 16px; + min-height: 50px; + font-size: 13px; + + &--separated { + margin: 12px 0; + border-radius: 3px; + } + + &__icon svg { + fill: #ff9000; + width: 20px; + height: 20px; + } + + &__content { + flex: 1; + display: flex; + gap: 16px; + align-items: center; + flex-wrap: wrap; + } + + &__type { font-weight: 500; } + + &__times { color: #1a3a5c; } + + @media (max-width: 768px) { + &__content { + flex-direction: column; + align-items: flex-start; + } + } +} + +.transfer-time { + font-weight: 500; + color: #ff9000; +} diff --git a/src/features/online-board/components/TransferBar/TransferBar.test.tsx b/src/features/online-board/components/TransferBar/TransferBar.test.tsx new file mode 100644 index 00000000..71c8db93 --- /dev/null +++ b/src/features/online-board/components/TransferBar/TransferBar.test.tsx @@ -0,0 +1,130 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi } from "vitest"; +import { render, screen } from "@testing-library/react"; +import type { IFlightLeg } from "../../types.js"; + +vi.mock("@/i18n/provider.js", () => ({ + useTranslation: () => ({ t: (k: string) => k }), +})); + +vi.mock("../FullRouteTimeline/StationChange.js", () => ({ + StationChange: ({ from, to }: { from: { scheduled: { cityCode: string } }; to: { scheduled: { cityCode: string } } }) => ( +
{from.scheduled.cityCode}-{to.scheduled.cityCode}
+ ), +})); + +import { TransferBar } from "./TransferBar.js"; + +function makeLeg(overrides: { + arrCity?: string; + depCity?: string; + arrCityCode?: string; + depCityCode?: string; + arrAirportCode?: string; + depAirportCode?: string; + arrUtc?: string; + depUtc?: string; + arrLocal?: string; + depLocal?: string; +} = {}): IFlightLeg { + const arrCity = overrides.arrCity ?? "Moscow"; + const depCity = overrides.depCity ?? "Moscow"; + const arrCityCode = overrides.arrCityCode ?? "MOW"; + const depCityCode = overrides.depCityCode ?? "MOW"; + const arrAirportCode = overrides.arrAirportCode ?? "SVO"; + const depAirportCode = overrides.depAirportCode ?? "SVO"; + const arrUtc = overrides.arrUtc ?? "2026-04-17T10:00:00Z"; + const depUtc = overrides.depUtc ?? "2026-04-17T11:30:00Z"; + const arrLocal = overrides.arrLocal ?? "10:00"; + const depLocal = overrides.depLocal ?? "11:30"; + + return { + arrival: { + scheduled: { + airport: "", + airportCode: arrAirportCode, + city: arrCity, + cityCode: arrCityCode, + countryCode: "RU", + }, + terminal: "", + times: { + scheduledArrival: { + dayChange: { value: 0, title: "" }, + local: arrLocal, + localTime: arrLocal, + tzOffset: 0, + utc: arrUtc, + }, + }, + }, + departure: { + scheduled: { + airport: "", + airportCode: depAirportCode, + city: depCity, + cityCode: depCityCode, + countryCode: "RU", + }, + terminal: "", + checkingStatus: "", + times: { + scheduledDeparture: { + dayChange: { value: 0, title: "" }, + local: depLocal, + localTime: depLocal, + tzOffset: 0, + utc: depUtc, + }, + }, + }, + dayChange: 0, + equipment: {}, + flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false }, + flyingTime: "1h 30m", + index: 0, + operatingBy: {}, + status: "Scheduled", + updated: "", + } as IFlightLeg; +} + +describe("TransferBar", () => { + it("renders with data-testid=transfer-bar", () => { + const leg = makeLeg({ arrUtc: "2026-04-17T10:00:00Z", arrLocal: "10:00" }); + const nextLeg = makeLeg({ depUtc: "2026-04-17T11:30:00Z", depLocal: "11:30" }); + render(); + expect(screen.getByTestId("transfer-bar")).toBeTruthy(); + }); + + it("renders SHARED.INTERMEDIATE-LANDING label and both times", () => { + const leg = makeLeg({ arrLocal: "10:00", arrUtc: "2026-04-17T10:00:00Z" }); + const nextLeg = makeLeg({ depLocal: "11:30", depUtc: "2026-04-17T11:30:00Z" }); + render(); + expect(screen.getByText("SHARED.INTERMEDIATE-LANDING")).toBeTruthy(); + expect(screen.getByText("10:00")).toBeTruthy(); + expect(screen.getByText("11:30")).toBeTruthy(); + }); + + it("renders TransferTime component", () => { + const leg = makeLeg({ arrUtc: "2026-04-17T10:00:00Z" }); + const nextLeg = makeLeg({ depUtc: "2026-04-17T11:30:00Z" }); + render(); + expect(screen.getByTestId("transfer-time")).toBeTruthy(); + }); + + it("renders StationChange when arrival/departure cities differ", () => { + const leg = makeLeg({ arrCity: "Moscow", arrCityCode: "MOW" }); + const nextLeg = makeLeg({ depCity: "Saint Petersburg", depCityCode: "LED", depAirportCode: "LED" }); + render(); + expect(screen.getByTestId("station-change")).toBeTruthy(); + }); + + it("omits --separated class when arrival/departure cities match", () => { + const leg = makeLeg({ arrCity: "Moscow", arrCityCode: "MOW", arrAirportCode: "SVO" }); + const nextLeg = makeLeg({ depCity: "Moscow", depCityCode: "MOW", depAirportCode: "SVO" }); + const { container } = render(); + const bar = container.querySelector(".transfer-bar"); + expect(bar?.className).not.toContain("transfer-bar--separated"); + }); +}); diff --git a/src/features/online-board/components/TransferBar/TransferBar.tsx b/src/features/online-board/components/TransferBar/TransferBar.tsx new file mode 100644 index 00000000..6ce1e8c8 --- /dev/null +++ b/src/features/online-board/components/TransferBar/TransferBar.tsx @@ -0,0 +1,62 @@ +import type { FC } from "react"; +import { useTranslation } from "@/i18n/provider.js"; +import type { IFlightLeg } from "../../types.js"; +import { StationChange } from "../FullRouteTimeline/StationChange.js"; +import { detectStationChange } from "../FullRouteTimeline/detectStationChange.js"; +import { TransferTime } from "./TransferTime.js"; +import "./TransferBar.scss"; + +export interface TransferBarProps { + leg: IFlightLeg; + nextLeg: IFlightLeg; + viewType: "Onlineboard" | "Schedule"; +} + +function arrivalLocal(leg: IFlightLeg, viewType: "Onlineboard" | "Schedule"): string { + const t = leg.arrival.times; + if (viewType === "Schedule") return t.scheduledArrival.local; + return t.actualBlockOn?.local ?? t.scheduledArrival.local; +} + +function departureLocal(leg: IFlightLeg, viewType: "Onlineboard" | "Schedule"): string { + const t = leg.departure.times; + if (viewType === "Schedule") return t.scheduledDeparture.local; + return t.actualBlockOff?.local ?? t.scheduledDeparture.local; +} + +export const TransferBar: FC = ({ leg, nextLeg, viewType }) => { + const { t } = useTranslation(); + const stationChange = detectStationChange(leg.arrival, nextLeg.departure); + const separated = stationChange !== "noChange"; + + const className = `transfer-bar${separated ? " transfer-bar--separated" : ""}`; + + return ( +
+ +
+ {t("SHARED.INTERMEDIATE-LANDING")} +
+ +
+
+ {arrivalLocal(leg, viewType)} + + {departureLocal(nextLeg, viewType)} +
+ {separated && ( +
+ +
+ )} +
+
+ ); +}; diff --git a/src/features/online-board/components/TransferBar/index.ts b/src/features/online-board/components/TransferBar/index.ts new file mode 100644 index 00000000..9cf2dbb3 --- /dev/null +++ b/src/features/online-board/components/TransferBar/index.ts @@ -0,0 +1,2 @@ +export { TransferBar } from "./TransferBar.js"; +export type { TransferBarProps } from "./TransferBar.js";