Add FullRouteTimeline wrapper component
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
.full-route-timeline {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px 24px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
&__arrow {
|
||||
background: transparent;
|
||||
border: 1px solid #d0dae5;
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
color: #2060c0;
|
||||
font-size: 16px;
|
||||
|
||||
&:disabled { opacity: 0.3; cursor: not-allowed; }
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&--times { font-size: 18px; font-weight: 500; color: #1a3a5c; }
|
||||
&--stations { font-size: 14px; color: #666; }
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-section {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__separator {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #d0dae5;
|
||||
}
|
||||
|
||||
&__number {
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
background: #e3f0ff;
|
||||
color: #2060c0;
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__duration {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
padding: 0 8px;
|
||||
background: #fff;
|
||||
|
||||
&--specifying { color: #ff9000; }
|
||||
}
|
||||
}
|
||||
|
||||
.station {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
&--right { align-items: flex-end; text-align: right; }
|
||||
&--center { align-items: center; text-align: center; }
|
||||
|
||||
&__city { font-size: 14px; font-weight: 500; color: #1a3a5c; }
|
||||
&__code { font-size: 12px; color: #666; }
|
||||
&__terminal { font-size: 11px; color: #999; }
|
||||
|
||||
&--large { .station__city { font-size: 22px; } }
|
||||
&--small { .station__city { font-size: 12px; } }
|
||||
}
|
||||
|
||||
.station-change {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&__arrow { color: #2060c0; }
|
||||
&__city { font-size: 12px; color: #666; }
|
||||
&__code { font-size: 14px; font-weight: 500; color: #1a3a5c; }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// @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 }),
|
||||
}));
|
||||
|
||||
import { FullRouteTimeline } from "./FullRouteTimeline.js";
|
||||
|
||||
function makeLeg(i: number): IFlightLeg {
|
||||
return {
|
||||
arrival: {
|
||||
scheduled: {
|
||||
airport: "",
|
||||
airportCode: "AAA",
|
||||
city: `City${i}`,
|
||||
cityCode: `C${i}`,
|
||||
countryCode: "RU",
|
||||
},
|
||||
terminal: "",
|
||||
times: {
|
||||
scheduledArrival: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: `a${i}`,
|
||||
localTime: `a${i}`,
|
||||
tzOffset: 0,
|
||||
utc: `2026-04-17T0${i}:00:00Z`,
|
||||
},
|
||||
},
|
||||
},
|
||||
departure: {
|
||||
scheduled: {
|
||||
airport: "",
|
||||
airportCode: "BBB",
|
||||
city: `Dep${i}`,
|
||||
cityCode: `D${i}`,
|
||||
countryCode: "RU",
|
||||
},
|
||||
terminal: "",
|
||||
checkingStatus: "",
|
||||
times: {
|
||||
scheduledDeparture: {
|
||||
dayChange: { value: 0, title: "" },
|
||||
local: `d${i}`,
|
||||
localTime: `d${i}`,
|
||||
tzOffset: 0,
|
||||
utc: `2026-04-17T0${i}:00:00Z`,
|
||||
},
|
||||
},
|
||||
},
|
||||
dayChange: 0,
|
||||
equipment: {},
|
||||
flags: { checkinAvailable: false, returnToAirport: false, routeChanged: false },
|
||||
flyingTime: `${i}h`,
|
||||
index: i,
|
||||
operatingBy: {},
|
||||
status: "Scheduled",
|
||||
updated: "",
|
||||
} as IFlightLeg;
|
||||
}
|
||||
|
||||
describe("FullRouteTimeline", () => {
|
||||
it("returns null when fewer than 2 legs", () => {
|
||||
const { container } = render(<FullRouteTimeline legs={[makeLeg(0)]} viewType="Schedule" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("renders container with data-testid=full-route-timeline when 2+ legs", () => {
|
||||
render(<FullRouteTimeline legs={[makeLeg(0), makeLeg(1)]} viewType="Schedule" />);
|
||||
expect(screen.getByTestId("full-route-timeline")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders inner Timeline content (expect d0 visible when 2 legs)", () => {
|
||||
render(<FullRouteTimeline legs={[makeLeg(0), makeLeg(1)]} viewType="Schedule" />);
|
||||
expect(screen.getByText("d0")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { FC } from "react";
|
||||
import type { IFlightLeg } from "../../types.js";
|
||||
import { Timeline } from "./Timeline.js";
|
||||
import "./FullRouteTimeline.scss";
|
||||
|
||||
export interface FullRouteTimelineProps {
|
||||
legs: IFlightLeg[];
|
||||
viewType: "Onlineboard" | "Schedule";
|
||||
}
|
||||
|
||||
export const FullRouteTimeline: FC<FullRouteTimelineProps> = ({ legs, viewType }) => {
|
||||
if (legs.length < 2) return null;
|
||||
return (
|
||||
<section className="full-route-timeline" data-testid="full-route-timeline">
|
||||
<Timeline legs={legs} canChange={viewType === "Onlineboard"} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { FullRouteTimeline } from "./FullRouteTimeline.js";
|
||||
export type { FullRouteTimelineProps } from "./FullRouteTimeline.js";
|
||||
Reference in New Issue
Block a user