Add popular requests pre-fill implementation plan

This commit is contained in:
2026-04-16 18:17:34 +03:00
parent e8cf655abc
commit c1c65faef3
@@ -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 (
<div className="online-board-start-page" data-testid="online-board-start">
<PageLayout
headerLeft={
<PageTabs viewType="onlineboard" />
}
title={
<h1 className="text--white page-title">
{t("BOARD.TITLE")}
</h1>
}
breadcrumbs={[]}
contentLeft={
<>
<OnlineBoardFilter
initialTab={initialTab}
initialFlightNumber={initialFlightNumber}
initialDeparture={initialDeparture}
initialArrival={initialArrival}
/>
<SearchHistory />
</>
}
>
<section className="frame">
<h2>{t("BOARD.BOARD-START")}</h2>
<div className="titles-container">
<div className="title title1">
<a>{t("BOARD.BOARD-START-TITLE1")}</a>
<div>
{t("BOARD.BOARD-START-TITLE1-DESCRIPTION")}
</div>
</div>
<div className="title title2">
<a>{t("BOARD.BOARD-START-TITLE2")}</a>
<div>
{t("BOARD.BOARD-START-TITLE2-DESCRIPTION")}
</div>
</div>
<div className="title title3">
<a>{t("BOARD.BOARD-START-TITLE3")}</a>
<div>
{t("BOARD.BOARD-START-TITLE3-DESCRIPTION")}
</div>
</div>
<div className="title title4">
<a>{t("BOARD.BOARD-START-TITLE4")}</a>
<div>
{t("BOARD.BOARD-START-TITLE4-DESCRIPTION")}
</div>
</div>
</div>
<PopularRequestsPanel onRequestClick={handlePopularRequestClick} />
</section>
</PageLayout>
</div>
);
};
```
- [ ] **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<CitySuggestion | string>(searchParams.get("departure") ?? "");
const [arrivalAirport, setArrivalAirport] = useState<CitySuggestion | string>(searchParams.get("arrival") ?? "");
const [dateFrom, setDateFrom] = useState<Date | null>(today);
const [dateTo, setDateTo] = useState<Date | null>(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<Date | null>(addDays(today, 7));
const [returnDateTo, setReturnDateTo] = useState<Date | null>(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.