diff --git a/src/features/online-board/components/OnlineBoardStartPage.test.tsx b/src/features/online-board/components/OnlineBoardStartPage.test.tsx
index 726b5a53..5007006e 100644
--- a/src/features/online-board/components/OnlineBoardStartPage.test.tsx
+++ b/src/features/online-board/components/OnlineBoardStartPage.test.tsx
@@ -9,16 +9,14 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
-import { OnlineBoardStartPage, buildPopularRequestQueryParams } from "./OnlineBoardStartPage.js";
+import { OnlineBoardStartPage, buildOnlineBoardPrefillState } from "./OnlineBoardStartPage.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
const mockNavigate = vi.fn();
-const mockSearchParams = new URLSearchParams();
vi.mock("@modern-js/runtime/router", () => ({
useNavigate: () => mockNavigate,
useParams: () => ({ lang: "ru" }),
- useSearchParams: () => [mockSearchParams],
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
{children}
),
@@ -65,88 +63,52 @@ vi.mock("@/ui/city-autocomplete/index.js", () => ({
}));
// ---------------------------------------------------------------------------
-// Pure function: buildPopularRequestQueryParams
+// Pure function: buildOnlineBoardPrefillState
// ---------------------------------------------------------------------------
-describe("buildPopularRequestQueryParams", () => {
- it("builds params for FlightNumber mode", () => {
+describe("buildOnlineBoardPrefillState", () => {
+ it("builds state for FlightNumber mode", () => {
const request: PopularRequest = {
mode: "FlightNumber",
carrier: "SU",
flightNumber: "0654",
type: "Onlineboard",
};
- const params = buildPopularRequestQueryParams(request);
- expect(params.get("tab")).toBe("flight");
- expect(params.get("carrier")).toBe("SU");
- expect(params.get("flight")).toBe("0654");
- expect(params.has("departure")).toBe(false);
- expect(params.has("arrival")).toBe(false);
+ expect(buildOnlineBoardPrefillState(request)).toEqual({
+ tab: "flight",
+ flightNumber: "SU0654",
+ });
});
- it("builds params for Departure mode", () => {
- const request: PopularRequest = {
- mode: "Departure",
- departure: "LED",
- type: "Onlineboard",
- };
- const params = buildPopularRequestQueryParams(request);
- expect(params.get("tab")).toBe("route");
- expect(params.get("departure")).toBe("LED");
- expect(params.has("arrival")).toBe(false);
- expect(params.has("carrier")).toBe(false);
+ it("builds state for Departure mode", () => {
+ expect(
+ buildOnlineBoardPrefillState({
+ mode: "Departure",
+ departure: "LED",
+ type: "Onlineboard",
+ }),
+ ).toEqual({ tab: "route", departure: "LED" });
});
- it("builds params for Arrival mode", () => {
- const request: PopularRequest = {
- mode: "Arrival",
- arrival: "VKO",
- type: "Onlineboard",
- };
- const params = buildPopularRequestQueryParams(request);
- expect(params.get("tab")).toBe("route");
- expect(params.get("arrival")).toBe("VKO");
- expect(params.has("departure")).toBe(false);
+ it("builds state for Arrival mode", () => {
+ expect(
+ buildOnlineBoardPrefillState({
+ mode: "Arrival",
+ arrival: "VKO",
+ type: "Onlineboard",
+ }),
+ ).toEqual({ tab: "route", arrival: "VKO" });
});
- it("builds params for Route mode (Onlineboard)", () => {
- const request: PopularRequest = {
- mode: "Route",
- departure: "LED",
- arrival: "KRR",
- type: "Onlineboard",
- };
- const params = buildPopularRequestQueryParams(request);
- expect(params.get("tab")).toBe("route");
- expect(params.get("departure")).toBe("LED");
- expect(params.get("arrival")).toBe("KRR");
- expect(params.has("return")).toBe(false);
- });
-
- it("builds params for Route mode (Schedule)", () => {
- const request: PopularRequest = {
- mode: "Route",
- departure: "SVO",
- arrival: "LED",
- type: "Schedule",
- };
- const params = buildPopularRequestQueryParams(request);
- expect(params.get("departure")).toBe("SVO");
- expect(params.get("arrival")).toBe("LED");
- expect(params.has("return")).toBe(false);
- });
-
- it("builds params for RouteWithBack mode (Schedule) with return flag", () => {
- const request: PopularRequest = {
- mode: "RouteWithBack",
- departure: "SVO",
- arrival: "LED",
- type: "Schedule",
- };
- const params = buildPopularRequestQueryParams(request);
- expect(params.get("departure")).toBe("SVO");
- expect(params.get("arrival")).toBe("LED");
- expect(params.get("return")).toBe("true");
+ it("builds state for Route mode", () => {
+ expect(
+ buildOnlineBoardPrefillState({
+ mode: "Route",
+ departure: "LED",
+ arrival: "KRR",
+ type: "Onlineboard",
+ }),
+ ).toEqual({ tab: "route", departure: "LED", arrival: "KRR" });
});
});
@@ -211,14 +173,11 @@ describe("OnlineBoardStartPage", () => {
expect(screen.getByTestId("feedback-button")).toBeTruthy();
});
- it("navigates on popular request click (Onlineboard type)", () => {
+ it("does not navigate on same-page popular click (Onlineboard type)", () => {
render();
const btn = screen.getByTestId("popular-click");
fireEvent.click(btn);
- expect(mockNavigate).toHaveBeenCalledWith(
- expect.objectContaining({
- search: expect.stringContaining("tab=flight"),
- }),
- );
+ // Same-page click — the filter remounts via key bump, no nav.
+ expect(mockNavigate).not.toHaveBeenCalled();
});
});
diff --git a/src/features/online-board/components/OnlineBoardStartPage.tsx b/src/features/online-board/components/OnlineBoardStartPage.tsx
index 6c678c2a..529cfba0 100644
--- a/src/features/online-board/components/OnlineBoardStartPage.tsx
+++ b/src/features/online-board/components/OnlineBoardStartPage.tsx
@@ -12,8 +12,8 @@
* @module
*/
-import { type FC, useCallback, useMemo } from "react";
-import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router";
+import { type FC, useCallback, useState } from "react";
+import { useNavigate, useParams } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
@@ -21,47 +21,52 @@ import { SearchHistory } from "@/ui/layout/SearchHistory.js";
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
+import {
+ readAndClearTransientPrefill,
+ writeTransientPrefill,
+} from "@/shared/state/transientPrefill.js";
import "./OnlineBoardStartPage.scss";
-/**
- * Build URL search params for a given popular request.
- *
- * Schedule-type requests produce params for the schedule page;
- * Onlineboard-type requests produce params for the online board filter.
- */
-export function buildPopularRequestQueryParams(
- request: PopularRequest,
-): URLSearchParams {
- const params = new URLSearchParams();
+export const ONLINE_BOARD_PREFILL_SLOT = "online-board";
+export const SCHEDULE_PREFILL_SLOT = "schedule";
+/**
+ * Transient prefill state handed from a popular-request click to the
+ * start page form. Mirrors Angular's `OnlineBoardFiltersStateService`
+ * cross-page singleton: URLs stay clean of query strings while the
+ * start-page form still seeds itself from the click. Held in
+ * sessionStorage and read-then-cleared on mount, so a fresh tab or
+ * page reload renders an empty form (matching Angular's behavior of a
+ * pristine service after navigation).
+ */
+export interface OnlineBoardPrefillState {
+ tab?: "flight" | "route";
+ departure?: string;
+ arrival?: string;
+ flightNumber?: string;
+}
+
+export function buildOnlineBoardPrefillState(
+ request: PopularRequest,
+): OnlineBoardPrefillState {
switch (request.mode) {
case "FlightNumber":
- params.set("tab", "flight");
- params.set("carrier", request.carrier);
- params.set("flight", request.flightNumber);
- break;
+ return {
+ tab: "flight",
+ flightNumber: `${request.carrier}${request.flightNumber}`,
+ };
case "Departure":
- params.set("tab", "route");
- params.set("departure", request.departure);
- break;
+ return { tab: "route", departure: request.departure };
case "Arrival":
- params.set("tab", "route");
- params.set("arrival", request.arrival);
- break;
+ return { tab: "route", arrival: request.arrival };
case "Route":
case "RouteWithBack":
- if (request.type === "Onlineboard") {
- params.set("tab", "route");
- }
- params.set("departure", request.departure);
- params.set("arrival", request.arrival);
- if (request.mode === "RouteWithBack" && request.type === "Schedule") {
- params.set("return", "true");
- }
- break;
+ return {
+ tab: "route",
+ departure: request.departure,
+ arrival: request.arrival,
+ };
}
-
- return params;
}
export const OnlineBoardStartPage: FC = () => {
@@ -69,53 +74,49 @@ export const OnlineBoardStartPage: FC = () => {
const navigate = useNavigate();
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
- const [searchParams] = useSearchParams();
- /** Derive initial filter props from current URL query params */
- const filterInitialProps = useMemo(() => {
- const tab = searchParams.get("tab");
- const departure = searchParams.get("departure");
- const arrival = searchParams.get("arrival");
- const carrier = searchParams.get("carrier");
- const flight = searchParams.get("flight");
- const date = searchParams.get("date");
+ // 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(
+ () =>
+ readAndClearTransientPrefill(
+ ONLINE_BOARD_PREFILL_SLOT,
+ ) ?? {},
+ );
+ // 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);
- // No query params — use defaults
- if (!tab && !departure && !arrival && !carrier && !flight) {
- return {};
- }
-
- return {
- ...(tab === "flight" || tab === "route"
- ? { initialTab: tab as "flight" | "route" }
- : {}),
- ...(departure ? { initialDeparture: departure } : {}),
- ...(arrival ? { initialArrival: arrival } : {}),
- ...(date ? { initialDate: date } : {}),
- ...(carrier && flight
- ? { initialFlightNumber: `${carrier}${flight}` }
- : {}),
- };
- }, [searchParams]);
+ const filterInitialProps = {
+ ...(prefill.tab ? { initialTab: prefill.tab } : {}),
+ ...(prefill.departure ? { initialDeparture: prefill.departure } : {}),
+ ...(prefill.arrival ? { initialArrival: prefill.arrival } : {}),
+ ...(prefill.flightNumber ? { initialFlightNumber: prefill.flightNumber } : {}),
+ };
const handlePopularRequestClick = useCallback(
(request: PopularRequest) => {
- const params = buildPopularRequestQueryParams(request);
-
// Schedule-type requests navigate to the schedule feature
if (request.type === "Schedule") {
- navigate({
- pathname: `/${lang}/schedule`,
- search: params.toString(),
- });
+ const state =
+ request.mode === "Route" || request.mode === "RouteWithBack"
+ ? {
+ departure: request.departure,
+ arrival: request.arrival,
+ withReturn: request.mode === "RouteWithBack",
+ }
+ : {};
+ writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state);
+ navigate(`/${lang}/schedule`);
return;
}
- // Onlineboard requests stay on this page with query params
- navigate({
- pathname: `/${lang}/onlineboard`,
- search: params.toString(),
- });
+ // Onlineboard request — same page. Update local prefill +
+ // remount the filter via key bump so its useState initializers
+ // see the new initial* props.
+ setPrefill(buildOnlineBoardPrefillState(request));
+ setFilterKey((n) => n + 1);
},
[navigate, lang],
);
@@ -134,7 +135,7 @@ export const OnlineBoardStartPage: FC = () => {
breadcrumbs={[]}
contentLeft={
<>
-
+
>
}
diff --git a/src/features/schedule/components/ScheduleStartPage.test.tsx b/src/features/schedule/components/ScheduleStartPage.test.tsx
index 1fa67031..ae08f620 100644
--- a/src/features/schedule/components/ScheduleStartPage.test.tsx
+++ b/src/features/schedule/components/ScheduleStartPage.test.tsx
@@ -9,16 +9,14 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
-import { ScheduleStartPage, buildSchedulePopularRequestQueryParams } from "./ScheduleStartPage.js";
+import { ScheduleStartPage } from "./ScheduleStartPage.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
const mockNavigate = vi.fn();
-let mockSearchParams = new URLSearchParams();
vi.mock("@modern-js/runtime/router", () => ({
useNavigate: () => mockNavigate,
useParams: () => ({ lang: "ru" }),
- useSearchParams: () => [mockSearchParams],
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; className?: string; [k: string]: unknown }) => (
{children}
),
@@ -50,14 +48,14 @@ vi.mock("@/features/popular-requests/components/PopularRequestsPanel.js", () =>
}));
vi.mock("@/features/online-board/components/OnlineBoardStartPage.js", () => ({
- buildPopularRequestQueryParams: (request: PopularRequest) => {
- const params = new URLSearchParams();
+ buildOnlineBoardPrefillState: (request: PopularRequest) => {
if (request.mode === "Departure") {
- params.set("tab", "route");
- params.set("departure", (request as { departure: string }).departure);
+ return { tab: "route", departure: request.departure };
}
- return params;
+ return {};
},
+ ONLINE_BOARD_PREFILL_SLOT: "online-board",
+ SCHEDULE_PREFILL_SLOT: "schedule",
}));
vi.mock("@/shared/hooks/useCitySearch.js", () => ({
@@ -68,56 +66,10 @@ vi.mock("@/ui/layout/SearchHistory.js", () => ({
SearchHistory: () => ,
}));
-// ---------------------------------------------------------------------------
-// Pure function: buildSchedulePopularRequestQueryParams
-// ---------------------------------------------------------------------------
-
-describe("buildSchedulePopularRequestQueryParams", () => {
- it("builds params for Route mode with departure and arrival", () => {
- const request: PopularRequest = {
- mode: "Route",
- departure: "SVO",
- arrival: "LED",
- type: "Schedule",
- };
- const params = buildSchedulePopularRequestQueryParams(request);
- expect(params.get("departure")).toBe("SVO");
- expect(params.get("arrival")).toBe("LED");
- expect(params.has("return")).toBe(false);
- });
-
- it("builds params for RouteWithBack mode with return flag", () => {
- const request: PopularRequest = {
- mode: "RouteWithBack",
- departure: "SVO",
- arrival: "LED",
- type: "Schedule",
- };
- const params = buildSchedulePopularRequestQueryParams(request);
- expect(params.get("departure")).toBe("SVO");
- expect(params.get("arrival")).toBe("LED");
- expect(params.get("return")).toBe("true");
- });
-
- it("returns empty params for non-route modes", () => {
- const request: PopularRequest = {
- mode: "Arrival",
- arrival: "VKO",
- type: "Onlineboard",
- };
- const params = buildSchedulePopularRequestQueryParams(request);
- expect(params.toString()).toBe("");
- });
-});
-
-// ---------------------------------------------------------------------------
-// Component tests
-// ---------------------------------------------------------------------------
-
describe("ScheduleStartPage", () => {
beforeEach(() => {
vi.clearAllMocks();
- mockSearchParams = new URLSearchParams();
+ sessionStorage.clear();
});
it("renders the start page", () => {
@@ -125,29 +77,29 @@ describe("ScheduleStartPage", () => {
expect(screen.getByTestId("schedule-start")).toBeTruthy();
});
- it("navigates to schedule page on Schedule-type popular request click", () => {
+ it("writes prefill + navigates to schedule on Schedule-type popular click", () => {
render();
fireEvent.click(screen.getByTestId("popular-click-route"));
- expect(mockNavigate).toHaveBeenCalledWith(
- expect.objectContaining({
- pathname: "/ru/schedule",
- search: expect.stringContaining("departure=SVO"),
- }),
+ expect(sessionStorage.getItem("afl-prefill:schedule")).toBe(
+ JSON.stringify({ departure: "SVO", arrival: "LED", withReturn: false }),
);
+ expect(mockNavigate).toHaveBeenCalledWith("/ru/schedule");
});
- it("navigates to onlineboard on Onlineboard-type popular request click", () => {
+ it("writes prefill + navigates to onlineboard on Onlineboard-type popular click", () => {
render();
fireEvent.click(screen.getByTestId("popular-click-onlineboard"));
- expect(mockNavigate).toHaveBeenCalledWith(
- expect.objectContaining({
- pathname: "/ru/onlineboard",
- }),
+ expect(sessionStorage.getItem("afl-prefill:online-board")).toBe(
+ JSON.stringify({ tab: "route", departure: "LED" }),
);
+ expect(mockNavigate).toHaveBeenCalledWith("/ru/onlineboard");
});
- it("initializes form from search params", () => {
- mockSearchParams = new URLSearchParams("departure=SVO&arrival=LED&return=true");
+ it("initializes form from sessionStorage prefill", () => {
+ sessionStorage.setItem(
+ "afl-prefill:schedule",
+ JSON.stringify({ departure: "SVO", arrival: "LED", withReturn: true }),
+ );
render();
const roundTripCheckbox = screen.getByTestId("round-trip-toggle");
expect((roundTripCheckbox as HTMLInputElement).checked).toBe(true);
diff --git a/src/features/schedule/components/ScheduleStartPage.tsx b/src/features/schedule/components/ScheduleStartPage.tsx
index af453c7e..32fecb06 100644
--- a/src/features/schedule/components/ScheduleStartPage.tsx
+++ b/src/features/schedule/components/ScheduleStartPage.tsx
@@ -8,7 +8,7 @@
*/
import { type FC, useState, useCallback, type FormEvent } from "react";
-import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router";
+import { useNavigate, useParams } from "@modern-js/runtime/router";
import { Calendar } from "primereact/calendar";
import { Slider, type SliderChangeEvent } from "primereact/slider";
import { AutoComplete, type AutoCompleteCompleteEvent } from "primereact/autocomplete";
@@ -19,7 +19,15 @@ import { PageTabs } from "@/ui/layout/PageTabs.js";
import { SearchHistory } from "@/ui/layout/SearchHistory.js";
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
-import { buildPopularRequestQueryParams } from "@/features/online-board/components/OnlineBoardStartPage.js";
+import {
+ buildOnlineBoardPrefillState,
+ ONLINE_BOARD_PREFILL_SLOT,
+ SCHEDULE_PREFILL_SLOT,
+} from "@/features/online-board/components/OnlineBoardStartPage.js";
+import {
+ readAndClearTransientPrefill,
+ writeTransientPrefill,
+} from "@/shared/state/transientPrefill.js";
import { buildScheduleUrl } from "../url.js";
import "./ScheduleStartPage.scss";
@@ -43,23 +51,16 @@ function addDays(base: Date, days: number): Date {
}
/**
- * Build URL search params for a schedule-type popular request.
- *
- * Only Route and RouteWithBack modes produce params; other modes
- * return empty params (they belong to the online board).
+ * Transient prefill state handed from a popular-request click via
+ * sessionStorage. Mirrors Angular's `ScheduleFiltersStateService`
+ * cross-page singleton: URL stays clean of query strings while the
+ * form seeds from the click. Read-and-cleared on mount, so reload
+ * gives a pristine form (matching Angular).
*/
-export function buildSchedulePopularRequestQueryParams(
- request: PopularRequest,
-): URLSearchParams {
- const params = new URLSearchParams();
- if (request.mode === "Route" || request.mode === "RouteWithBack") {
- params.set("departure", request.departure);
- params.set("arrival", request.arrival);
- if (request.mode === "RouteWithBack") {
- params.set("return", "true");
- }
- }
- return params;
+export interface SchedulePrefillState {
+ departure?: string;
+ arrival?: string;
+ withReturn?: boolean;
}
export const ScheduleStartPage: FC = () => {
@@ -67,17 +68,24 @@ export const ScheduleStartPage: FC = () => {
const { t } = useTranslation();
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
- const [searchParams] = useSearchParams();
+
+ // One-shot read of any prefill the previous page wrote.
+ const [prefill] = useState(
+ () =>
+ readAndClearTransientPrefill(
+ SCHEDULE_PREFILL_SLOT,
+ ) ?? {},
+ );
const today = new Date();
- const [departureAirport, setDepartureAirport] = useState(searchParams.get("departure") ?? "");
- const [arrivalAirport, setArrivalAirport] = useState(searchParams.get("arrival") ?? "");
+ const [departureAirport, setDepartureAirport] = useState(prefill.departure ?? "");
+ const [arrivalAirport, setArrivalAirport] = useState(prefill.arrival ?? "");
const [dateFrom, setDateFrom] = useState(today);
const [dateTo, setDateTo] = useState(addDays(today, 7));
const [timeRange, setTimeRange] = useState<[number, number]>([0, 1440]);
const [directOnly, setDirectOnly] = useState(false);
- const [isRoundTrip, setIsRoundTrip] = useState(searchParams.get("return") === "true");
+ const [isRoundTrip, setIsRoundTrip] = useState(prefill.withReturn === true);
const [returnDateFrom, setReturnDateFrom] = useState(addDays(today, 7));
const [returnDateTo, setReturnDateTo] = useState(addDays(today, 14));
const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]);
@@ -149,20 +157,25 @@ export const ScheduleStartPage: FC = () => {
const handlePopularRequestClick = useCallback(
(request: PopularRequest) => {
if (request.type === "Onlineboard") {
- const params = buildPopularRequestQueryParams(request);
- navigate({
- pathname: `/${lang}/onlineboard`,
- search: params.toString(),
- });
+ writeTransientPrefill(
+ ONLINE_BOARD_PREFILL_SLOT,
+ buildOnlineBoardPrefillState(request),
+ );
+ navigate(`/${lang}/onlineboard`);
return;
}
- // Schedule-type: build params and navigate to schedule with pre-fill
- const params = buildSchedulePopularRequestQueryParams(request);
- navigate({
- pathname: `/${lang}/schedule`,
- search: params.toString(),
- });
+ // Schedule-type: only Route / RouteWithBack carry city info.
+ const state: SchedulePrefillState =
+ request.mode === "Route" || request.mode === "RouteWithBack"
+ ? {
+ departure: request.departure,
+ arrival: request.arrival,
+ withReturn: request.mode === "RouteWithBack",
+ }
+ : {};
+ writeTransientPrefill(SCHEDULE_PREFILL_SLOT, state);
+ navigate(`/${lang}/schedule`);
},
[navigate, lang],
);
diff --git a/src/routes/[lang]/popular/page.tsx b/src/routes/[lang]/popular/page.tsx
deleted file mode 100644
index 277e265e..00000000
--- a/src/routes/[lang]/popular/page.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * Popular Requests standalone page route.
- *
- * Provides a standalone view of the PopularRequestsPanel. In Angular,
- * popular requests were embedded in OnlineBoard and Schedule start pages.
- * This route provides a direct-access path for the MF remote and allows
- * independent rendering/testing.
- *
- * URL: /{lang}/popular
- */
-
-import { lazy, Suspense, useCallback } from "react";
-import { useParams, useNavigate } from "@modern-js/runtime/router";
-import { useTranslation } from "@/i18n/provider.js";
-import type { PopularRequest } from "@/features/popular-requests/types.js";
-
-const PopularRequestsPanel = lazy(() =>
- import("@/features/popular-requests/components/PopularRequestsPanel.js").then(
- (m) => ({ default: m.PopularRequestsPanel }),
- ),
-);
-
-export default function PopularPage(): JSX.Element {
- const { t } = useTranslation();
- const routeParams = useParams<{ lang: string }>();
- const lang = routeParams.lang ?? "ru";
- const navigate = useNavigate();
-
- const handleRequestClick = useCallback(
- (request: PopularRequest) => {
- switch (request.mode) {
- case "FlightNumber":
- case "Arrival":
- case "Departure":
- void navigate(`/${lang}/onlineboard`);
- return;
- case "Route":
- if (request.type === "Onlineboard") {
- void navigate(`/${lang}/onlineboard`);
- } else {
- void navigate(`/${lang}/schedule`);
- }
- return;
- case "RouteWithBack":
- void navigate(`/${lang}/schedule`);
- return;
- }
- },
- [lang, navigate],
- );
-
- return (
- {t("SHARED.LOADING")}}>
-
-
- );
-}
diff --git a/src/shared/state/transientPrefill.ts b/src/shared/state/transientPrefill.ts
new file mode 100644
index 00000000..c5257816
--- /dev/null
+++ b/src/shared/state/transientPrefill.ts
@@ -0,0 +1,36 @@
+/**
+ * Cross-page transient prefill store.
+ *
+ * Mirrors Angular's `OnlineBoardFiltersStateService` /
+ * `ScheduleFiltersStateService` — singleton state used to hand
+ * popular-request data from one page to another without polluting the
+ * URL with query strings. Reload clears it (matches Angular's behavior:
+ * a fresh navigation to the page bypasses the service-held state).
+ *
+ * sessionStorage is the host: same-tab persistence, gone on tab close,
+ * works around react-router v6's same-path navigation collapsing
+ * `state` updates without re-rendering useLocation consumers.
+ */
+
+const KEY_PREFIX = "afl-prefill:";
+
+export function writeTransientPrefill(slot: string, value: T): void {
+ if (typeof sessionStorage === "undefined") return;
+ try {
+ sessionStorage.setItem(KEY_PREFIX + slot, JSON.stringify(value));
+ } catch {
+ // ignore quota/disabled
+ }
+}
+
+export function readAndClearTransientPrefill(slot: string): T | null {
+ if (typeof sessionStorage === "undefined") return null;
+ try {
+ const raw = sessionStorage.getItem(KEY_PREFIX + slot);
+ if (!raw) return null;
+ sessionStorage.removeItem(KEY_PREFIX + slot);
+ return JSON.parse(raw) as T;
+ } catch {
+ return null;
+ }
+}
diff --git a/tests/e2e/popular.spec.ts b/tests/e2e/popular.spec.ts
deleted file mode 100644
index 70876e66..00000000
--- a/tests/e2e/popular.spec.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { test, expect } from "@playwright/test";
-
-test.describe("Popular Requests", () => {
- test("/ru/popular renders", async ({ page }) => {
- await page.goto("/ru/popular");
- await page.waitForLoadState("domcontentloaded");
-
- // The page should render without crashing. The PopularRequestsPanel
- // is lazy-loaded, so we wait for either the panel or the loading fallback.
- await expect(
- page.locator("main, [aria-busy='true'], body"),
- ).not.toBeEmpty();
-
- // URL should be correct
- expect(page.url()).toContain("/ru/popular");
- });
-});
diff --git a/tests/integration/online-board/start-page.test.tsx b/tests/integration/online-board/start-page.test.tsx
index 9ef6c61f..d96335c9 100644
--- a/tests/integration/online-board/start-page.test.tsx
+++ b/tests/integration/online-board/start-page.test.tsx
@@ -20,7 +20,7 @@ const navigateSpy = vi.fn();
vi.mock("@modern-js/runtime/router", () => ({
useNavigate: () => navigateSpy,
useParams: () => ({ lang: "ru" }),
- useSearchParams: () => [new URLSearchParams()],
+ useLocation: () => ({ state: null, pathname: "/ru/onlineboard" }),
Link: ({ children, to, ...props }: { children: React.ReactNode; to: string; [k: string]: unknown }) => (
{children}
),