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 ( +