diff --git a/docs/superpowers/plans/2026-04-16-popular-requests-prefill.md b/docs/superpowers/plans/2026-04-16-popular-requests-prefill.md new file mode 100644 index 00000000..19479971 --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-popular-requests-prefill.md @@ -0,0 +1,459 @@ +# Popular Requests Form Pre-fill Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make popular request clicks pre-fill the search form on the start page via query params, matching Angular behavior. + +**Architecture:** Each start page reads query params via `useSearchParams()` from `@modern-js/runtime/router`. The click handler builds a URL with query params and navigates to the same page. The form initializes state from those params. + +**Tech Stack:** React, Modern.js router (`useSearchParams`, `useNavigate`), existing `PopularRequest` types. + +--- + +## File Structure + +### Modified files +- `src/features/online-board/components/OnlineBoardStartPage.tsx` — Add click handler + query param reading + pass initial props to filter +- `src/features/schedule/components/ScheduleStartPage.tsx` — Add click handler + query param reading + initialize form state from params + +### No changes needed +- `src/features/online-board/components/OnlineBoardFilter.tsx` — Already accepts `initialDeparture`, `initialArrival`, `initialDate`, `initialTab`, `initialFlightNumber` props +- `src/features/popular-requests/` — No changes to any popular requests components + +--- + +### Task 1: OnlineBoardStartPage — Pre-fill on Popular Request Click + +**Files:** +- Modify: `src/features/online-board/components/OnlineBoardStartPage.tsx` + +- [ ] **Step 1: Write the failing test** + +Create `src/features/online-board/components/OnlineBoardStartPage.test.tsx`: + +```tsx +import { describe, it, expect, vi } from "vitest"; +import { buildPopularRequestQueryParams } from "./OnlineBoardStartPage.js"; + +describe("buildPopularRequestQueryParams", () => { + it("returns tab=flight with carrier and flight for FlightNumber mode", () => { + const params = buildPopularRequestQueryParams({ + mode: "FlightNumber", + carrier: "SU", + flightNumber: "0654", + type: "Onlineboard", + }); + expect(params.get("tab")).toBe("flight"); + expect(params.get("carrier")).toBe("SU"); + expect(params.get("flight")).toBe("0654"); + }); + + it("returns tab=route with departure for Departure mode", () => { + const params = buildPopularRequestQueryParams({ + mode: "Departure", + departure: "LED", + type: "Onlineboard", + }); + expect(params.get("tab")).toBe("route"); + expect(params.get("departure")).toBe("LED"); + expect(params.has("arrival")).toBe(false); + }); + + it("returns tab=route with arrival for Arrival mode", () => { + const params = buildPopularRequestQueryParams({ + mode: "Arrival", + arrival: "VKO", + type: "Onlineboard", + }); + expect(params.get("tab")).toBe("route"); + expect(params.get("arrival")).toBe("VKO"); + expect(params.has("departure")).toBe(false); + }); + + it("returns tab=route with both cities for Route mode", () => { + const params = buildPopularRequestQueryParams({ + mode: "Route", + departure: "LED", + arrival: "KRR", + type: "Onlineboard", + }); + expect(params.get("tab")).toBe("route"); + expect(params.get("departure")).toBe("LED"); + expect(params.get("arrival")).toBe("KRR"); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm vitest run src/features/online-board/components/OnlineBoardStartPage.test.tsx` + +Expected: FAIL — `buildPopularRequestQueryParams` is not exported. + +- [ ] **Step 3: Implement the query param builder and wire up the component** + +Edit `src/features/online-board/components/OnlineBoardStartPage.tsx`. Replace the entire file with: + +```tsx +/** + * Online Board start page matching the Angular `online-board-start-page` + * component DOM structure and CSS class names. + * + * Reads query params (set by popular request clicks) and passes them + * as initial props to OnlineBoardFilter for form pre-fill. + * + * @module + */ + +import { type FC, useCallback } from "react"; +import { useNavigate, useParams, useSearchParams } 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"; +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 "./OnlineBoardStartPage.scss"; + +/** Build query params from a popular request for form pre-fill. */ +export function buildPopularRequestQueryParams(request: PopularRequest): URLSearchParams { + const params = new URLSearchParams(); + + switch (request.mode) { + case "FlightNumber": + params.set("tab", "flight"); + params.set("carrier", request.carrier); + params.set("flight", request.flightNumber); + break; + case "Departure": + params.set("tab", "route"); + params.set("departure", request.departure); + break; + case "Arrival": + params.set("tab", "route"); + params.set("arrival", request.arrival); + break; + case "Route": + case "RouteWithBack": + params.set("tab", "route"); + params.set("departure", request.departure); + params.set("arrival", request.arrival); + break; + } + + return params; +} + +export const OnlineBoardStartPage: FC = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const routeParams = useParams<{ lang: string }>(); + const lang = routeParams.lang ?? "ru"; + const [searchParams] = useSearchParams(); + + const handlePopularRequestClick = useCallback((request: PopularRequest) => { + // Schedule-type requests navigate to the schedule page + if (request.type === "Schedule") { + 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"); + } + } + void navigate(`/${lang}/schedule?${params.toString()}`); + return; + } + + const qp = buildPopularRequestQueryParams(request); + void navigate(`/${lang}/onlineboard?${qp.toString()}`); + }, [navigate, lang]); + + // Read query params for filter pre-fill + const initialTab = searchParams.get("tab") === "flight" ? "flight" as const : searchParams.has("tab") ? "route" as const : undefined; + const initialFlightNumber = searchParams.get("carrier") && searchParams.get("flight") + ? `${searchParams.get("carrier")}${searchParams.get("flight")}` + : undefined; + const initialDeparture = searchParams.get("departure") ?? undefined; + const initialArrival = searchParams.get("arrival") ?? undefined; + + return ( +
+ + } + title={ +

+ {t("BOARD.TITLE")} +

+ } + breadcrumbs={[]} + contentLeft={ + <> + + + + } + > +
+

{t("BOARD.BOARD-START")}

+ +
+
+ {t("BOARD.BOARD-START-TITLE1")} +
+ {t("BOARD.BOARD-START-TITLE1-DESCRIPTION")} +
+
+ +
+ {t("BOARD.BOARD-START-TITLE2")} +
+ {t("BOARD.BOARD-START-TITLE2-DESCRIPTION")} +
+
+ +
+ {t("BOARD.BOARD-START-TITLE3")} +
+ {t("BOARD.BOARD-START-TITLE3-DESCRIPTION")} +
+
+ +
+ {t("BOARD.BOARD-START-TITLE4")} +
+ {t("BOARD.BOARD-START-TITLE4-DESCRIPTION")} +
+
+
+ + +
+
+
+ ); +}; +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm vitest run src/features/online-board/components/OnlineBoardStartPage.test.tsx` + +Expected: PASS — all 4 tests pass. + +- [ ] **Step 5: Run existing tests to check for regressions** + +Run: `pnpm test` + +Expected: All existing tests pass. No regressions. + +- [ ] **Step 6: Commit** + +```bash +git add src/features/online-board/components/OnlineBoardStartPage.tsx src/features/online-board/components/OnlineBoardStartPage.test.tsx +git commit -m "Wire popular request clicks to pre-fill OnlineBoard filter via query params" +``` + +--- + +### Task 2: ScheduleStartPage — Pre-fill on Popular Request Click + +**Files:** +- Modify: `src/features/schedule/components/ScheduleStartPage.tsx` + +- [ ] **Step 1: Write the failing test** + +Create `src/features/schedule/components/ScheduleStartPage.test.tsx`: + +```tsx +import { describe, it, expect } from "vitest"; +import { buildSchedulePopularRequestQueryParams } from "./ScheduleStartPage.js"; + +describe("buildSchedulePopularRequestQueryParams", () => { + it("returns departure and arrival for Route mode", () => { + const params = buildSchedulePopularRequestQueryParams({ + mode: "Route", + departure: "LED", + arrival: "KRR", + type: "Schedule", + }); + expect(params.get("departure")).toBe("LED"); + expect(params.get("arrival")).toBe("KRR"); + expect(params.has("return")).toBe(false); + }); + + it("returns departure, arrival, and return=true for RouteWithBack mode", () => { + const params = buildSchedulePopularRequestQueryParams({ + mode: "RouteWithBack", + departure: "VKO", + arrival: "KUF", + type: "Schedule", + }); + expect(params.get("departure")).toBe("VKO"); + expect(params.get("arrival")).toBe("KUF"); + expect(params.get("return")).toBe("true"); + }); + + it("returns empty params for non-route modes", () => { + const params = buildSchedulePopularRequestQueryParams({ + mode: "Arrival", + arrival: "VKO", + type: "Onlineboard", + }); + expect(params.toString()).toBe(""); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `pnpm vitest run src/features/schedule/components/ScheduleStartPage.test.tsx` + +Expected: FAIL — `buildSchedulePopularRequestQueryParams` is not exported. + +- [ ] **Step 3: Implement the query param builder and wire up the component** + +Edit `src/features/schedule/components/ScheduleStartPage.tsx`. Make these changes: + +**Add imports** — add `useSearchParams` to the router import: + +```tsx +import { useNavigate, useParams, useSearchParams } from "@modern-js/runtime/router"; +``` + +**Add the exported builder function** — after the `addDays` helper, before the component: + +```tsx +/** Build query params from a popular request for schedule form pre-fill. */ +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; +} +``` + +**Read search params in the component** — after the `lang` line, add: + +```tsx +const [searchParams] = useSearchParams(); +``` + +**Initialize form state from query params** — change the `useState` calls: + +```tsx +const [departureAirport, setDepartureAirport] = useState(searchParams.get("departure") ?? ""); +const [arrivalAirport, setArrivalAirport] = useState(searchParams.get("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 [returnDateFrom, setReturnDateFrom] = useState(addDays(today, 7)); +const [returnDateTo, setReturnDateTo] = useState(addDays(today, 14)); +const [returnTimeRange, setReturnTimeRange] = useState<[number, number]>([0, 1440]); +``` + +**Implement the click handler** — replace the empty callback: + +```tsx +const handlePopularRequestClick = useCallback((request: PopularRequest) => { + // Onlineboard-type requests navigate to the onlineboard page + if (request.type === "Onlineboard") { + const params = new URLSearchParams(); + switch (request.mode) { + case "FlightNumber": + params.set("tab", "flight"); + params.set("carrier", request.carrier); + params.set("flight", request.flightNumber); + break; + case "Departure": + params.set("tab", "route"); + params.set("departure", request.departure); + break; + case "Arrival": + params.set("tab", "route"); + params.set("arrival", request.arrival); + break; + case "Route": + params.set("tab", "route"); + params.set("departure", request.departure); + params.set("arrival", request.arrival); + break; + } + void navigate(`/${lang}/onlineboard?${params.toString()}`); + return; + } + + const qp = buildSchedulePopularRequestQueryParams(request); + void navigate(`/${lang}/schedule?${qp.toString()}`); +}, [navigate, lang]); +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `pnpm vitest run src/features/schedule/components/ScheduleStartPage.test.tsx` + +Expected: PASS — all 3 tests pass. + +- [ ] **Step 5: Run all tests** + +Run: `pnpm test` + +Expected: All tests pass, no regressions. + +- [ ] **Step 6: Commit** + +```bash +git add src/features/schedule/components/ScheduleStartPage.tsx src/features/schedule/components/ScheduleStartPage.test.tsx +git commit -m "Wire popular request clicks to pre-fill Schedule form via query params" +``` + +--- + +### Task 3: Manual Verification + +- [ ] **Step 1: Start the React dev server** + +Run: `pnpm dev` + +- [ ] **Step 2: Navigate to Online Board start page and test popular requests** + +Open `http://localhost:8080/ru/onlineboard` in browser. + +Verify: +- Click each popular request item +- URL updates with query params (e.g., `?tab=flight&carrier=SU&flight=0654`) +- Filter form is pre-filled with the correct values +- Clicking "Search" from the pre-filled form navigates to the correct search results page + +- [ ] **Step 3: Navigate to Schedule start page and test popular requests** + +Open `http://localhost:8080/ru/schedule` in browser. + +Verify: +- Click a Route-type popular request +- URL updates with query params (e.g., `?departure=LED&arrival=KRR`) +- Departure and arrival fields are pre-filled +- For RouteWithBack: round-trip checkbox is checked +- Clicking "Search" navigates to schedule search results + +- [ ] **Step 4: Run comparison pipeline to verify improvement** + +Run: `pnpm compare:behavior` + +Expected: Popular requests tests (spec 19) that previously failed should now pass for the React project.