Wire cross-section filter hydration into Board/Schedule/Map per TZ 4.1.8 Table 10
This commit is contained in:
@@ -9,6 +9,15 @@ import type * as DictionariesModuleNS from "@/shared/dictionaries/index.js";
|
||||
import { transformDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import type { IDictionaries, IRawDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import type { FlightsMapSearchParams } from "../types.js";
|
||||
import {
|
||||
resetCrossSectionStore,
|
||||
setMapFilter,
|
||||
setBoardFilter,
|
||||
setScheduleFilter,
|
||||
type MapFilterSnapshot,
|
||||
type BoardFilterSnapshot,
|
||||
type ScheduleFilterSnapshot,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
|
||||
type DictionariesModule = typeof DictionariesModuleNS;
|
||||
|
||||
@@ -44,8 +53,12 @@ vi.mock("./MapCanvas.js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
let lastMapFilterProps: Record<string, unknown> | null = null;
|
||||
vi.mock("./FlightsMapFilter.js", () => ({
|
||||
FlightsMapFilter: () => <div data-testid="flights-map-filter" />,
|
||||
FlightsMapFilter: (props: Record<string, unknown>) => {
|
||||
lastMapFilterProps = props;
|
||||
return <div data-testid="flights-map-filter" />;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/env/index.js", () => ({
|
||||
@@ -491,3 +504,77 @@ describe("FlightsMapStartPage — C.4 integration", () => {
|
||||
expect(last?.connections).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TZ §4.1.8 / §4.1.1-R26: Map filter cross-section isolation tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.8 / 4.1.1-R26: Flight-Map filter cross-section isolation", () => {
|
||||
beforeEach(() => {
|
||||
lastMapFilterProps = null;
|
||||
lastMapCanvasProps = null;
|
||||
dictState.dictionaries = buildDictionaries();
|
||||
dictState.loading = false;
|
||||
dictState.error = null;
|
||||
searchState.routes = [];
|
||||
searchState.loading = false;
|
||||
searchState.error = null;
|
||||
resetCrossSectionStore();
|
||||
});
|
||||
|
||||
it("4.1.8: hydrates map filter from map snapshot on mount", () => {
|
||||
const mapSnap: MapFilterSnapshot = {
|
||||
departure: "MOW", arrival: "LED", date: "20260515",
|
||||
showInternal: true, showInternational: false, showTransfers: true,
|
||||
};
|
||||
setMapFilter(mapSnap);
|
||||
render(<FlightsMapStartPage />);
|
||||
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
|
||||
expect(filterValue["departure"]).toBe("MOW");
|
||||
expect(filterValue["arrival"]).toBe("LED");
|
||||
expect(filterValue["domestic"]).toBe(true);
|
||||
expect(filterValue["international"]).toBe(false);
|
||||
expect(filterValue["connections"]).toBe(true);
|
||||
});
|
||||
|
||||
it("4.1.1-R26: Board filter does NOT hydrate flight map", () => {
|
||||
const boardSnap: BoardFilterSnapshot = {
|
||||
mode: "route", departure: "SVO", arrival: "VVO",
|
||||
date: "20260515", timeFrom: "0000", timeTo: "2400",
|
||||
searchExecuted: true,
|
||||
};
|
||||
setBoardFilter(boardSnap);
|
||||
// No map snapshot set
|
||||
render(<FlightsMapStartPage />);
|
||||
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
|
||||
// Map filter should be at defaults, not seeded from board
|
||||
expect(filterValue["departure"]).toBeUndefined();
|
||||
expect(filterValue["arrival"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("4.1.1-R26: Schedule filter does NOT hydrate flight map", () => {
|
||||
const schedSnap: ScheduleFilterSnapshot = {
|
||||
mode: "route", departure: "KZN", arrival: "AER",
|
||||
dateFrom: "20260511", dateTo: "20260517",
|
||||
timeFrom: "0000", timeTo: "2400",
|
||||
onlyDirect: false, showReturn: false, searchExecuted: true,
|
||||
};
|
||||
setScheduleFilter(schedSnap);
|
||||
// No map snapshot set
|
||||
render(<FlightsMapStartPage />);
|
||||
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
|
||||
// Map filter should be at defaults, not seeded from schedule
|
||||
expect(filterValue["departure"]).toBeUndefined();
|
||||
expect(filterValue["arrival"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("4.1.8: renders empty filter when no map snapshot is in store", () => {
|
||||
// Store is empty (resetCrossSectionStore called in beforeEach)
|
||||
render(<FlightsMapStartPage />);
|
||||
const filterValue = lastMapFilterProps!["value"] as Record<string, unknown>;
|
||||
expect(filterValue["departure"]).toBeUndefined();
|
||||
expect(filterValue["arrival"]).toBeUndefined();
|
||||
expect(filterValue["domestic"]).toBe(false);
|
||||
expect(filterValue["international"]).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,10 @@ import { filterRoutes } from "../filterRoutes.js";
|
||||
import { buildBuyTicketUrl, escapeHtml } from "../buyTicketUrl.js";
|
||||
import { useDictionaries, getCityCodeByAirportCode } from "@/shared/dictionaries/index.js";
|
||||
import { getCityZoomLevel } from "../cityCategory.js";
|
||||
import {
|
||||
getMapFilter,
|
||||
setMapFilter,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
import type {
|
||||
IFlightsMapFilterState,
|
||||
FlightsMapSearchParams,
|
||||
@@ -89,10 +93,25 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
||||
error: dictionariesError,
|
||||
} = useDictionaries(language);
|
||||
|
||||
const [filterState, setFilterState] = useState<IFlightsMapFilterState>({
|
||||
connections: false,
|
||||
domestic: false,
|
||||
international: false,
|
||||
const [filterState, setFilterState] = useState<IFlightsMapFilterState>(() => {
|
||||
// TZ §4.1.8 / 4.1.1-R26: Map filter is stored independently and NEVER
|
||||
// projected from/to Board or Schedule.
|
||||
const mapSnap = getMapFilter();
|
||||
if (mapSnap) {
|
||||
return {
|
||||
departure: mapSnap.departure ?? undefined,
|
||||
arrival: mapSnap.arrival ?? undefined,
|
||||
date: mapSnap.date ?? undefined,
|
||||
connections: mapSnap.showTransfers,
|
||||
domestic: mapSnap.showInternal,
|
||||
international: mapSnap.showInternational,
|
||||
};
|
||||
}
|
||||
return {
|
||||
connections: false,
|
||||
domestic: false,
|
||||
international: false,
|
||||
};
|
||||
});
|
||||
|
||||
useGeolocationDefault(dictionaries, filterState, setFilterState);
|
||||
@@ -161,6 +180,15 @@ export const FlightsMapStartPage: FC<FlightsMapStartPageProps> = ({
|
||||
|
||||
const handleFilterChange = useCallback((newState: IFlightsMapFilterState) => {
|
||||
setFilterState(newState);
|
||||
// TZ §4.1.8 / 4.1.1-R26: persist map filter independently from Board/Schedule.
|
||||
setMapFilter({
|
||||
departure: newState.departure ?? null,
|
||||
arrival: newState.arrival ?? null,
|
||||
date: newState.date ?? null,
|
||||
showInternal: newState.domestic,
|
||||
showInternational: newState.international,
|
||||
showTransfers: newState.connections,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleMarkerClick = useCallback(
|
||||
|
||||
@@ -18,6 +18,7 @@ import { CityAutocomplete } from "@/ui/city-autocomplete/index.js";
|
||||
import { DayQuickPick } from "@/ui/calendar/DayQuickPick.js";
|
||||
import { useDictionaries } from "@/shared/dictionaries/index.js";
|
||||
import { buildOnlineBoardUrl } from "../url.js";
|
||||
import { setBoardFilter } from "@/shared/state/crossSectionNavigation.js";
|
||||
import "./OnlineBoardFilter.scss";
|
||||
|
||||
function minutesToTime(minutes: number): string {
|
||||
@@ -233,6 +234,17 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
||||
const carrier = "SU";
|
||||
const num = cleaned;
|
||||
if (!num) return;
|
||||
|
||||
// TZ §4.1.8: persist filter snapshot for cross-section hydration.
|
||||
setBoardFilter({
|
||||
mode: "flight-number",
|
||||
flightNumber: `${carrier}${num}`,
|
||||
date: dateParam,
|
||||
timeFrom: "0000",
|
||||
timeTo: "2400",
|
||||
searchExecuted: true,
|
||||
});
|
||||
|
||||
const url = buildOnlineBoardUrl({ type: "flight", carrier, flightNumber: num, date: dateParam });
|
||||
void navigate(`/${locale}/${url}`);
|
||||
},
|
||||
@@ -273,10 +285,38 @@ export const OnlineBoardFilter: FC<OnlineBoardFilterProps> = ({
|
||||
|
||||
let url: string;
|
||||
if (depCode && !arrCode) {
|
||||
// TZ §4.1.8: persist filter snapshot for cross-section hydration.
|
||||
setBoardFilter({
|
||||
mode: "departure",
|
||||
departure: depCode,
|
||||
date: dateParam,
|
||||
timeFrom: timeExtras.timeFrom ?? "0000",
|
||||
timeTo: timeExtras.timeTo ?? "2400",
|
||||
searchExecuted: true,
|
||||
});
|
||||
url = buildOnlineBoardUrl({ type: "departure", station: depCode, date: dateParam, ...timeExtras });
|
||||
} else if (!depCode && arrCode) {
|
||||
// TZ §4.1.8: persist filter snapshot for cross-section hydration.
|
||||
setBoardFilter({
|
||||
mode: "arrival",
|
||||
arrival: arrCode,
|
||||
date: dateParam,
|
||||
timeFrom: timeExtras.timeFrom ?? "0000",
|
||||
timeTo: timeExtras.timeTo ?? "2400",
|
||||
searchExecuted: true,
|
||||
});
|
||||
url = buildOnlineBoardUrl({ type: "arrival", station: arrCode, date: dateParam, ...timeExtras });
|
||||
} else {
|
||||
// TZ §4.1.8: persist filter snapshot for cross-section hydration.
|
||||
setBoardFilter({
|
||||
mode: "route",
|
||||
departure: depCode,
|
||||
arrival: arrCode,
|
||||
date: dateParam,
|
||||
timeFrom: timeExtras.timeFrom ?? "0000",
|
||||
timeTo: timeExtras.timeTo ?? "2400",
|
||||
searchExecuted: true,
|
||||
});
|
||||
url = buildOnlineBoardUrl({ type: "route", departure: depCode, arrival: arrCode, date: dateParam, ...timeExtras });
|
||||
}
|
||||
void navigate(`/${locale}/${url}`);
|
||||
|
||||
@@ -11,6 +11,13 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { OnlineBoardStartPage, buildOnlineBoardPrefillState } from "./OnlineBoardStartPage.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import {
|
||||
resetCrossSectionStore,
|
||||
setScheduleFilter,
|
||||
setBoardFilter,
|
||||
type ScheduleFilterSnapshot,
|
||||
type BoardFilterSnapshot,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
|
||||
@@ -58,6 +65,7 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
<input
|
||||
data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
|
||||
placeholder={props["placeholder"] as string}
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
@@ -181,3 +189,56 @@ describe("OnlineBoardStartPage", () => {
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TZ §4.1.8: cross-section hydration tests (Board ↔ Schedule)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.8: Online-Board hydrates from cross-section store on mount", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resetCrossSectionStore();
|
||||
});
|
||||
|
||||
it("4.1.8: hydrates departure + arrival from Schedule filter (projected) when no own state", () => {
|
||||
const schedSnap: ScheduleFilterSnapshot = {
|
||||
mode: "route", departure: "MOW", arrival: "LED",
|
||||
dateFrom: "20260511", dateTo: "20260517",
|
||||
timeFrom: "0000", timeTo: "2400",
|
||||
onlyDirect: false, showReturn: false, searchExecuted: true,
|
||||
};
|
||||
setScheduleFilter(schedSnap);
|
||||
render(<OnlineBoardStartPage />);
|
||||
// departure input should be seeded with "MOW"
|
||||
const depInput = screen.getByTestId("route-departure-input") as HTMLInputElement;
|
||||
expect(depInput.defaultValue).toBe("MOW");
|
||||
const arrInput = screen.getByTestId("route-arrival-input") as HTMLInputElement;
|
||||
expect(arrInput.defaultValue).toBe("LED");
|
||||
});
|
||||
|
||||
it("4.1.8: uses own board snapshot (not schedule) when board state is set", () => {
|
||||
// Set board first (own state)
|
||||
setBoardFilter({
|
||||
mode: "route", departure: "KZN", arrival: "AER",
|
||||
date: "20260515", timeFrom: "0000", timeTo: "2400",
|
||||
searchExecuted: false,
|
||||
});
|
||||
// Also set schedule — it should be ignored since board is present
|
||||
setScheduleFilter({
|
||||
mode: "route", departure: "SVO", arrival: "VVO",
|
||||
dateFrom: "20260511", dateTo: "20260517",
|
||||
timeFrom: "0000", timeTo: "2400",
|
||||
onlyDirect: false, showReturn: false, searchExecuted: true,
|
||||
});
|
||||
render(<OnlineBoardStartPage />);
|
||||
const depInput = screen.getByTestId("route-departure-input") as HTMLInputElement;
|
||||
expect(depInput.defaultValue).toBe("KZN");
|
||||
});
|
||||
|
||||
it("4.1.8: renders empty filter when both board and schedule store are empty", () => {
|
||||
// No cross-section state set
|
||||
render(<OnlineBoardStartPage />);
|
||||
const depInput = screen.getByTestId("route-departure-input") as HTMLInputElement;
|
||||
expect(depInput.defaultValue).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { type FC, useCallback, useState } from "react";
|
||||
import { type FC, useCallback, useState, useEffect } from "react";
|
||||
import { useNavigate } from "@modern-js/runtime/router";
|
||||
import { useLocale } from "@/i18n/useLocale.js";
|
||||
import { useTranslation } from "@/i18n/provider.js";
|
||||
@@ -26,6 +26,12 @@ import {
|
||||
readAndClearTransientPrefill,
|
||||
writeTransientPrefill,
|
||||
} from "@/shared/state/transientPrefill.js";
|
||||
import {
|
||||
getBoardFilter,
|
||||
getScheduleFilter,
|
||||
setBoardFilter,
|
||||
projectScheduleToBoard,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
import {
|
||||
useDictionaries,
|
||||
getCityCodeByAirportCode,
|
||||
@@ -97,12 +103,34 @@ export const OnlineBoardStartPage: FC = () => {
|
||||
// Read-and-clear any prefill the previous page wrote. Stored in
|
||||
// useState (with a one-shot initializer) so React strict mode's
|
||||
// double-render doesn't lose the value on the second pass.
|
||||
const [prefill, setPrefill] = useState<OnlineBoardPrefillState>(
|
||||
() =>
|
||||
readAndClearTransientPrefill<OnlineBoardPrefillState>(
|
||||
ONLINE_BOARD_PREFILL_SLOT,
|
||||
) ?? {},
|
||||
);
|
||||
// Falls back to cross-section store projection (Schedule → Board) per
|
||||
// TZ §4.1.8 Table 10 when no transient prefill is present.
|
||||
const [prefill, setPrefill] = useState<OnlineBoardPrefillState>(() => {
|
||||
const transient = readAndClearTransientPrefill<OnlineBoardPrefillState>(
|
||||
ONLINE_BOARD_PREFILL_SLOT,
|
||||
);
|
||||
if (transient) return transient;
|
||||
|
||||
// TZ §4.1.8: try own snapshot first, then project from Schedule.
|
||||
const boardSnap = getBoardFilter();
|
||||
if (boardSnap) {
|
||||
return {
|
||||
tab: "route",
|
||||
departure: boardSnap.departure,
|
||||
arrival: boardSnap.arrival,
|
||||
};
|
||||
}
|
||||
const schedSnap = getScheduleFilter();
|
||||
if (schedSnap) {
|
||||
const projected = projectScheduleToBoard(schedSnap);
|
||||
return {
|
||||
tab: "route",
|
||||
departure: projected.departure,
|
||||
arrival: projected.arrival,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
// Same-page popular clicks need to re-mount the filter so its
|
||||
// useState initial values pick up the new prefill. Key bump does it.
|
||||
const [filterKey, setFilterKey] = useState(0);
|
||||
|
||||
@@ -12,6 +12,13 @@ import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { ScheduleStartPage } from "./ScheduleStartPage.js";
|
||||
import { sessionStore } from "@/shared/storage.js";
|
||||
import type { PopularRequest } from "@/features/popular-requests/types.js";
|
||||
import {
|
||||
resetCrossSectionStore,
|
||||
setBoardFilter,
|
||||
setScheduleFilter,
|
||||
type BoardFilterSnapshot,
|
||||
type ScheduleFilterSnapshot,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
|
||||
const mockNavigate = vi.fn();
|
||||
|
||||
@@ -63,6 +70,15 @@ vi.mock("@/shared/hooks/useCitySearch.js", () => ({
|
||||
useCitySearch: () => ({ suggestions: [], search: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock("@/ui/city-autocomplete/index.js", () => ({
|
||||
CityAutocomplete: (props: Record<string, unknown>) => (
|
||||
<input
|
||||
data-testid={`${(props["testIdPrefix"] as string) ?? "city-autocomplete"}-input`}
|
||||
defaultValue={(props["value"] as string) ?? ""}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/ui/layout/SearchHistory.js", () => ({
|
||||
SearchHistory: () => <div data-testid="search-history" />,
|
||||
}));
|
||||
@@ -114,3 +130,53 @@ describe("ScheduleStartPage", () => {
|
||||
expect((roundTripCheckbox as HTMLInputElement).checked).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TZ §4.1.8: cross-section hydration tests (Board → Schedule)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("4.1.8: Schedule hydrates from cross-section store on mount", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
sessionStore.clear();
|
||||
resetCrossSectionStore();
|
||||
});
|
||||
|
||||
it("4.1.8: hydrates departure + arrival from Board filter (projected) when no own state", () => {
|
||||
const boardSnap: BoardFilterSnapshot = {
|
||||
mode: "route", departure: "MOW", arrival: "LED",
|
||||
date: "20260515", timeFrom: "0600", timeTo: "2200",
|
||||
searchExecuted: true,
|
||||
};
|
||||
setBoardFilter(boardSnap);
|
||||
render(<ScheduleStartPage />);
|
||||
const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
|
||||
expect(depInput.defaultValue).toBe("MOW");
|
||||
const arrInput = screen.getByTestId("schedule-arrival-input") as HTMLInputElement;
|
||||
expect(arrInput.defaultValue).toBe("LED");
|
||||
});
|
||||
|
||||
it("4.1.8: uses own schedule snapshot (not board) when schedule state is set", () => {
|
||||
setScheduleFilter({
|
||||
mode: "route", departure: "KZN", arrival: "AER",
|
||||
dateFrom: "20260511", dateTo: "20260517",
|
||||
timeFrom: "0000", timeTo: "2400",
|
||||
onlyDirect: false, showReturn: false, searchExecuted: false,
|
||||
});
|
||||
// Also set board — it should be ignored since schedule snapshot is present
|
||||
setBoardFilter({
|
||||
mode: "route", departure: "SVO", arrival: "VVO",
|
||||
date: "20260515", timeFrom: "0000", timeTo: "2400",
|
||||
searchExecuted: true,
|
||||
});
|
||||
render(<ScheduleStartPage />);
|
||||
const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
|
||||
expect(depInput.defaultValue).toBe("KZN");
|
||||
});
|
||||
|
||||
it("4.1.8: renders empty filter when both schedule and board store are empty", () => {
|
||||
render(<ScheduleStartPage />);
|
||||
const depInput = screen.getByTestId("schedule-departure-input") as HTMLInputElement;
|
||||
expect(depInput.defaultValue).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,12 @@ import {
|
||||
readAndClearTransientPrefill,
|
||||
writeTransientPrefill,
|
||||
} from "@/shared/state/transientPrefill.js";
|
||||
import {
|
||||
getScheduleFilter,
|
||||
getBoardFilter,
|
||||
setScheduleFilter,
|
||||
projectBoardToSchedule,
|
||||
} from "@/shared/state/crossSectionNavigation.js";
|
||||
import {
|
||||
useDictionaries,
|
||||
getCityCodeByAirportCode,
|
||||
@@ -92,12 +98,34 @@ export const ScheduleStartPage: FC = () => {
|
||||
const { dictionaries } = useDictionaries(language);
|
||||
|
||||
// One-shot read of any prefill the previous page wrote.
|
||||
const [prefill] = useState<SchedulePrefillState>(
|
||||
() =>
|
||||
readAndClearTransientPrefill<SchedulePrefillState>(
|
||||
SCHEDULE_PREFILL_SLOT,
|
||||
) ?? {},
|
||||
);
|
||||
// Falls back to cross-section store projection (Board → Schedule) per
|
||||
// TZ §4.1.8 Table 10 when no transient prefill is present.
|
||||
const [prefill] = useState<SchedulePrefillState>(() => {
|
||||
const transient = readAndClearTransientPrefill<SchedulePrefillState>(
|
||||
SCHEDULE_PREFILL_SLOT,
|
||||
);
|
||||
if (transient) return transient;
|
||||
|
||||
// TZ §4.1.8: try own snapshot first, then project from Board.
|
||||
const schedSnap = getScheduleFilter();
|
||||
if (schedSnap) {
|
||||
return {
|
||||
departure: schedSnap.departure,
|
||||
arrival: schedSnap.arrival,
|
||||
withReturn: schedSnap.showReturn,
|
||||
};
|
||||
}
|
||||
const boardSnap = getBoardFilter();
|
||||
if (boardSnap) {
|
||||
const projected = projectBoardToSchedule(boardSnap);
|
||||
return {
|
||||
departure: projected.departure,
|
||||
arrival: projected.arrival,
|
||||
withReturn: false,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const today = new Date();
|
||||
|
||||
@@ -184,6 +212,20 @@ export const ScheduleStartPage: FC = () => {
|
||||
});
|
||||
}
|
||||
|
||||
// TZ §4.1.8: persist filter snapshot for cross-section hydration.
|
||||
setScheduleFilter({
|
||||
mode: "route",
|
||||
departure: dep,
|
||||
arrival: arr,
|
||||
dateFrom: dateFromParam,
|
||||
dateTo: dateToParam,
|
||||
timeFrom: outbound.timeFrom ?? "0000",
|
||||
timeTo: outbound.timeTo ?? "2400",
|
||||
onlyDirect: directOnly,
|
||||
showReturn: isRoundTrip,
|
||||
searchExecuted: true,
|
||||
});
|
||||
|
||||
void navigate(`/${locale}/${url}`);
|
||||
},
|
||||
[departureCode, arrivalCode, dateFrom, dateTo, timeRange, directOnly, isRoundTrip, returnDateFrom, returnDateTo, returnTimeRange, navigate, locale],
|
||||
|
||||
Reference in New Issue
Block a user