diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx index 70c3b314..200a9060 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.test.tsx @@ -77,6 +77,7 @@ const mockFlight: IDirectFlight = { let mockState = { flight: mockFlight as IDirectFlight | null, allFlights: [mockFlight] as IDirectFlight[], + daysOfFlight: ["20250115"] as string[], loading: false, error: null as Error | null, }; @@ -85,6 +86,17 @@ vi.mock("../hooks/useFlightDetails.js", () => ({ useFlightDetails: () => mockState, })); +vi.mock("@/shared/hooks/useAppSettings.js", () => ({ + useAppSettings: () => ({ + onlineboardSearchFrom: 2, + onlineboardSearchTo: 14, + scheduleSearchFrom: 30, + scheduleSearchTo: 30, + loading: false, + error: null, + }), +})); + vi.mock("../hooks/useLiveFlightDetails.js", () => ({ useLiveFlightDetails: (_id: unknown, initialFlight: unknown) => ({ flight: initialFlight, @@ -120,6 +132,7 @@ describe("OnlineBoardDetailsPage", () => { mockState = { flight: mockFlight, allFlights: [mockFlight], + daysOfFlight: ["20250115"], loading: false, error: null, }; @@ -133,19 +146,19 @@ describe("OnlineBoardDetailsPage", () => { }); it("renders loading skeleton", () => { - mockState = { flight: null, allFlights: [], loading: true, error: null }; + mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: true, error: null }; render(); expect(screen.queryByTestId("flight-details")).toBeNull(); }); it("renders error state", () => { - mockState = { flight: null, allFlights: [], loading: false, error: new Error("fail") }; + mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: false, error: new Error("fail") }; render(); expect(screen.getByTestId("flight-details-error")).toBeTruthy(); }); it("renders not-found state", () => { - mockState = { flight: null, allFlights: [], loading: false, error: null }; + mockState = { flight: null, allFlights: [], daysOfFlight: [], loading: false, error: null }; render(); expect(screen.getByTestId("flight-details-not-found")).toBeTruthy(); }); @@ -208,7 +221,7 @@ describe("OnlineBoardDetailsPage", () => { }, }, }; - mockState = { flight: flightWithTransition, allFlights: [flightWithTransition], loading: false, error: null }; + mockState = { flight: flightWithTransition, allFlights: [flightWithTransition], daysOfFlight: ["20250115"], loading: false, error: null }; render(); expect(screen.getByTestId("flight-details-accordion")).toBeTruthy(); }); @@ -218,21 +231,29 @@ describe("OnlineBoardDetailsPage", () => { it("renders mini-list when multiple flights are returned", () => { const first = mockFlight; const second = { ...mockFlight, id: "SU0022-20260417", flightId: { ...mockFlight.flightId, date: "20260417" } }; - mockState = { flight: first, allFlights: [first, second], loading: false, error: null }; + mockState = { flight: first, allFlights: [first, second], daysOfFlight: ["20250115", "20260417"], loading: false, error: null }; render(); expect(screen.getByTestId("flights-mini-list")).toBeTruthy(); }); it("does not render mini-list when only one flight is returned", () => { - mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null }; + mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20250115"], loading: false, error: null }; render(); expect(screen.queryByTestId("flights-mini-list")).toBeNull(); }); it("renders inside PageLayout (has page-layout class)", () => { - mockState = { flight: mockFlight, allFlights: [mockFlight], loading: false, error: null }; + mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20250115"], loading: false, error: null }; const { container } = render(); expect(container.querySelector(".page-layout")).toBeTruthy(); }); }); + + describe("day tabs integration", () => { + it("renders DayTabs as sticky content", () => { + mockState = { flight: mockFlight, allFlights: [mockFlight], daysOfFlight: ["20260416"], loading: false, error: null }; + render(); + expect(screen.getByTestId("day-tabs")).toBeTruthy(); + }); + }); }); diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index 0e98b55f..2c8692a4 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -7,7 +7,8 @@ * @module */ -import type { FC } from "react"; +import { useCallback, type FC } from "react"; +import { useNavigate } from "@modern-js/runtime/router"; import { useTranslation } from "@/i18n/provider.js"; import "./OnlineBoardDetailsPage.scss"; import { FlightCard } from "@/ui/flights/FlightCard.js"; @@ -16,12 +17,15 @@ import { SeoHead } from "@/ui/seo/SeoHead.js"; import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; import { PageLayout } from "@/ui/layout/PageLayout.js"; import { PageTabs } from "@/ui/layout/PageTabs.js"; +import { useAppSettings } from "@/shared/hooks/useAppSettings.js"; import { useFlightDetails } from "../hooks/useFlightDetails.js"; import { useLiveFlightDetails } from "../hooks/useLiveFlightDetails.js"; import { buildFlightDetailsSeo } from "../seo.js"; import { buildFlightJsonLd } from "../json-ld.js"; +import { buildOnlineBoardUrl } from "../url.js"; import { FlightDetailsAccordion } from "./details-panels/FlightDetailsAccordion.js"; import { FlightsMiniList } from "./FlightsMiniList/index.js"; +import { DayTabs } from "./DayTabs/index.js"; import type { IParsedFlightId, IFlightLeg } from "../types.js"; export interface OnlineBoardDetailsPageProps { @@ -152,7 +156,7 @@ export const OnlineBoardDetailsPage: FC = ({ flights: `${flightId.carrier}${flightId.flightNumber}${flightId.suffix ?? ""}`, dates: `${flightId.date.slice(0, 4)}-${flightId.date.slice(4, 6)}-${flightId.date.slice(6, 8)}`, }; - const { flight: firstFlight, allFlights, loading, error } = useFlightDetails(detailsParams); + const { flight: firstFlight, allFlights, daysOfFlight, loading, error } = useFlightDetails(detailsParams); // Pick the flight matching the URL's flightId (date-based match). The API // response may contain multiple flights with the same flight number on @@ -168,6 +172,23 @@ export const OnlineBoardDetailsPage: FC = ({ const displayFlight = connectionStatus === "live" && liveFlight ? liveFlight : flight; + const { onlineboardSearchFrom, onlineboardSearchTo } = useAppSettings(); + const navigate = useNavigate(); + + const handleNavigateDate = useCallback( + (newDate: string) => { + const url = buildOnlineBoardUrl({ + type: "details", + carrier: flightId.carrier, + flightNumber: flightId.flightNumber, + ...(flightId.suffix ? { suffix: flightId.suffix } : {}), + date: newDate, + }); + void navigate(`/${locale}/${url}`); + }, + [flightId.carrier, flightId.flightNumber, flightId.suffix, locale, navigate], + ); + const onlineboardHref = `/${locale}/onlineboard`; const commonLayoutProps = { headerLeft: , @@ -228,6 +249,16 @@ export const OnlineBoardDetailsPage: FC = ({ lang={locale} /> } + stickyContent={ + + } >
{/* Connection status */} diff --git a/tests/integration/online-board/error-handling.test.tsx b/tests/integration/online-board/error-handling.test.tsx index bacc13aa..bd2aab63 100644 --- a/tests/integration/online-board/error-handling.test.tsx +++ b/tests/integration/online-board/error-handling.test.tsx @@ -76,6 +76,17 @@ vi.mock("@/features/online-board/hooks/useLiveFlightDetails.js", () => ({ }), })); +vi.mock("@/shared/hooks/useAppSettings.js", () => ({ + useAppSettings: () => ({ + onlineboardSearchFrom: 2, + onlineboardSearchTo: 14, + scheduleSearchFrom: 30, + scheduleSearchTo: 30, + loading: false, + error: null, + }), +})); + // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- @@ -178,6 +189,8 @@ describe("Details page error handling", () => { it("renders error state for details API failure", () => { mockUseFlightDetails.mockReturnValue({ flight: null, + allFlights: [], + daysOfFlight: [], loading: false, error: new ApiHttpError("HTTP 500", 500), }); @@ -195,6 +208,8 @@ describe("Details page error handling", () => { it("renders not-found when details returns null", () => { mockUseFlightDetails.mockReturnValue({ flight: null, + allFlights: [], + daysOfFlight: [], loading: false, error: null, }); diff --git a/tests/integration/online-board/flight-details.test.tsx b/tests/integration/online-board/flight-details.test.tsx index e3095b7e..fcd7f904 100644 --- a/tests/integration/online-board/flight-details.test.tsx +++ b/tests/integration/online-board/flight-details.test.tsx @@ -50,6 +50,17 @@ vi.mock("@/features/online-board/hooks/useLiveFlightDetails.js", () => ({ useLiveFlightDetails: (...args: unknown[]) => mockUseLiveFlightDetails(...args), })); +vi.mock("@/shared/hooks/useAppSettings.js", () => ({ + useAppSettings: () => ({ + onlineboardSearchFrom: 2, + onlineboardSearchTo: 14, + scheduleSearchFrom: 30, + scheduleSearchTo: 30, + loading: false, + error: null, + }), +})); + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -64,6 +75,7 @@ function setupWithFlight(flight: ISimpleFlight = DIRECT_FLIGHT) { mockUseFlightDetails.mockReturnValue({ flight, allFlights: [flight], + daysOfFlight: ["20250115"], loading: false, error: null, }); @@ -179,6 +191,7 @@ describe("Flight details page integration", () => { mockUseFlightDetails.mockReturnValue({ flight: null, allFlights: [], + daysOfFlight: [], loading: false, error: new Error("API error"), }); @@ -201,6 +214,7 @@ describe("Flight details page integration", () => { mockUseFlightDetails.mockReturnValue({ flight: null, allFlights: [], + daysOfFlight: [], loading: false, error: null, });