Add TransferBar component for multi-leg transfer info
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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 } } }) => (
|
||||
<div data-testid="station-change">{from.scheduled.cityCode}-{to.scheduled.cityCode}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
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(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
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(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
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(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
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(<TransferBar leg={leg} nextLeg={nextLeg} viewType="Schedule" />);
|
||||
const bar = container.querySelector(".transfer-bar");
|
||||
expect(bar?.className).not.toContain("transfer-bar--separated");
|
||||
});
|
||||
});
|
||||
@@ -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<TransferBarProps> = ({ 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 (
|
||||
<div className={className} data-testid="transfer-bar">
|
||||
<div className="transfer-bar__icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20">
|
||||
<path d="M3 12h15l-4-4m4 4-4 4" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="transfer-bar__content">
|
||||
<span className="transfer-bar__type">{t("SHARED.INTERMEDIATE-LANDING")}</span>
|
||||
<div className="transfer-bar__duration">
|
||||
<TransferTime
|
||||
arrivalUtc={leg.arrival.times.scheduledArrival.utc}
|
||||
departureUtc={nextLeg.departure.times.scheduledDeparture.utc}
|
||||
/>
|
||||
</div>
|
||||
<div className="transfer-bar__times">
|
||||
<span>{arrivalLocal(leg, viewType)}</span>
|
||||
<span> — </span>
|
||||
<span>{departureLocal(nextLeg, viewType)}</span>
|
||||
</div>
|
||||
{separated && (
|
||||
<div className="transfer-bar__station-change">
|
||||
<StationChange from={leg.arrival} to={nextLeg.departure} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { TransferBar } from "./TransferBar.js";
|
||||
export type { TransferBarProps } from "./TransferBar.js";
|
||||
Reference in New Issue
Block a user