From 84e6d265fcf9e995c66941b612ab50c127e3520a Mon Sep 17 00:00:00 2001 From: gnezim Date: Fri, 17 Apr 2026 23:14:59 +0300 Subject: [PATCH] Align board search + details with Angular visual parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both pages were rendering content directly on the dark-blue page background, which made the flight list and details card effectively invisible. Angular wraps the same content in a white card (section.frame) with drop shadow. Changes: - Wrap FlightList in
on the search page and wrap the details body the same way. - Replace the inline numbered .calendar-day strip on the search page with the existing component — the same one the details page already uses (weekday + day + month labels, ‹/› paging). - Pass onFlightClick through FlightList into FlightCard so whole rows are keyboard-accessible buttons, matching Angular's row-level click. The off-screen data-testid='flight-link-*' buttons stay for e2e. - Fix 'Leg NaN' header + the React key warning in FlightLegs when the API returns a Direct leg without an index field. - Update the existing flight-search integration test to target the DayTabs testid instead of the old ad-hoc calendar-strip one. A docs/parity-report-2026-04-17.md file captures the diffs I applied and a punch list of the remaining parity gaps (operator logo on rows, delay/day-change glyphs, Share button visibility on board details, the aircraft panel as a table). Those need per-component work against the Angular templates and will follow in a separate pass. --- docs/parity-report-2026-04-17.md | 52 +++++++++++ .../components/OnlineBoardDetailsPage.tsx | 86 ++++++++++--------- .../components/OnlineBoardSearchPage.tsx | 48 +++++------ src/ui/flights/FlightCard.scss | 15 ++++ src/ui/flights/FlightCard.tsx | 24 +++++- src/ui/flights/FlightList.tsx | 9 +- .../online-board/flight-search.test.tsx | 14 +-- 7 files changed, 168 insertions(+), 80 deletions(-) create mode 100644 docs/parity-report-2026-04-17.md diff --git a/docs/parity-report-2026-04-17.md b/docs/parity-report-2026-04-17.md new file mode 100644 index 00000000..4fe580e9 --- /dev/null +++ b/docs/parity-report-2026-04-17.md @@ -0,0 +1,52 @@ +# Visual Parity Report — Angular vs React + +_Captured 2026-04-17, SVO→LED route, SU 6805 details page, viewport 1440×900._ + +Screenshots (checked in under `/` root): +- `parity-angular-search.png`, `parity-react-search.png` +- `parity-angular-details.png`, `parity-react-details.png` + +--- + +## Search results (`/ru/onlineboard/route/SVO-LED-20260417`) + +| Angular | React (pre-fix) | Notes | +| --- | --- | --- | +| Flight list sits on a white card with drop shadow (`section.frame`) | Flight rows rendered on the dark-blue page background → text barely readable | **fixed**: wrap `` in `
` | +| Calendar strip uses paged weekday tabs: `16 апр.` / `17 апреля` (active, bold) / `18 апр.` … with `‹ ›` arrows | Thin strip of naked numbered buttons wrapping across two rows (`16 17 18 … 1 2 3 …`) | **fixed**: replace inline `.calendar-day` with existing `` component | +| Each row is clickable; hovering highlights the row | Rows are not interactive; navigation to details only via off-screen test-only buttons | **fixed**: `FlightCard` accepts `onClick` and renders with `role="button"` + hover highlight | +| Row shows: flight # · operator logo + name · scheduled time (big) · actual time struck-through in orange on delay · city + terminal link · status with plane icon · arrival mirror · expand arrow | Row shows: flight # · scheduled time · city name · status text. Missing operator logo, terminal link, actual-time strike-through, delay arrow glyph, expanded panel | open — operator logo, terminal link, expanded-state layout still pending | +| Filter sidebar has collapsible Route accordion + time slider inside; "Sheremetyevo" populated in departure input | Filter sidebar shows `SVO` / `LED` codes and sliders; labels don't render the city name | open — filter should pre-populate with the airport display name | +| Footer note `* Время в системе - МЕСТНОЕ.` | Not rendered | open | + +## Details page (`/ru/onlineboard/SU6805-20260417`) + +| Angular | React (pre-fix) | Notes | +| --- | --- | --- | +| All right-hand content on a single white card | Content directly on dark blue — text invisible | **fixed**: wrap in `
` | +| `DayTabs` used at the top of the card (already present in React) | Same component, but on dark bg | **fixed** by moving into frame | +| React console warning: `Each child in a list should have a unique "key" prop. Check the render method of FlightLegs` | — | **fixed**: key now falls back to the array index when `leg.index` is absent | +| Header shows: flight # · airline name · big `РОССИЯ` logo · share button · last-update timestamp | React header currently shows only flight # + share + last-update | open — needs operator name/logo treatment in `BoardDetailsHeader` | +| Route row: departure city + plane + status word (`Запланирован`) + arrival city; "+1" day indicator sits above the arrival time | React leg row renders raw station codes, city names, times stacked vertically | open — `FlightLegs` block needs to be rewritten as a horizontal route strip like Angular's `flight-board-details` | +| Accordion first section (`Детали рейса`) open by default with Регистрация / Посадка rows visible; others (Борт, Питание, Услуги, Деплайнинг) below | 5 accordions all rendered, all collapsed by default; no `Детали рейса` wrapper | open — open primary section by default, hide `Share` button (Angular hides it on board view) | +| `Борт` (aircraft) panel has a table with columns: Название / Количество мест / Эконом / Бизнес / Предыдущий рейс | React's `AircraftPanel` renders the same fields but as a list, not a table | open | + +--- + +## Priority fixes shipped in this commit + +1. **White card** (`section.frame`) around flight list + details body — the single biggest readability issue. +2. **`DayTabs` calendar strip** replaces the inline number-only strip on the search page. +3. **Clickable `FlightCard`** with keyboard support and hover highlight, so users don't have to use the off-screen test button. +4. **Fixed React key warning** in the `FlightLegs` renderer. + +## Deferred (not shipped) + +- Operator logo + airline name on every row and in the details header. +- Delay arrow + `+1` day-change indicators in Angular's layout. +- Inline per-row expansion with Registration/Boarding/Deboarding mini-grid. +- `Борт` aircraft panel as a table. +- Pre-populate the filter with the selected city's display name. +- Hide `Share` button on the board details page (Angular only shows it on schedule details). + +These need targeted, per-component changes against the Angular HTML templates referenced in `ClientApp/src/app/modules/pages/board/` and `ClientApp/src/app/modules/pages/details/`. They don't affect readability, so they're listed for a follow-up pass. diff --git a/src/features/online-board/components/OnlineBoardDetailsPage.tsx b/src/features/online-board/components/OnlineBoardDetailsPage.tsx index 1ed7215a..73b95dbc 100644 --- a/src/features/online-board/components/OnlineBoardDetailsPage.tsx +++ b/src/features/online-board/components/OnlineBoardDetailsPage.tsx @@ -55,10 +55,10 @@ function FlightLegs({ return (
{legs.map((leg, i) => ( - -
+ +
- Leg {leg.index + 1} + Leg {(leg.index ?? i) + 1} {leg.status}
@@ -279,49 +279,51 @@ export const OnlineBoardDetailsPage: FC = ({ /> } > -
- {/* Connection status */} -
- {connectionStatus === "live" && ( - Live - )} - {connectionStatus === "reconnecting" && ( - Reconnecting... - )} - {connectionStatus === "offline" && ( - Offline - )} -
- - - - {displayFlight.routeType === "MultiLeg" && ( - - )} - - {/* Summary card */} - - - {/* Operating carrier */} - {displayFlight.operatingBy.carrier && ( -
- Operated by: {displayFlight.operatingBy.carrier} - {displayFlight.operatingBy.flightNumber - ? ` ${displayFlight.operatingBy.flightNumber}` - : ""} +
+
+ {/* Connection status */} +
+ {connectionStatus === "live" && ( + Live + )} + {connectionStatus === "reconnecting" && ( + Reconnecting... + )} + {connectionStatus === "offline" && ( + Offline + )}
- )} - {/* Detailed leg information */} - + - {/* Flying time */} -
- Total flying time: {displayFlight.flyingTime} + {displayFlight.routeType === "MultiLeg" && ( + + )} + + {/* Summary card */} + + + {/* Operating carrier */} + {displayFlight.operatingBy.carrier && ( +
+ Operated by: {displayFlight.operatingBy.carrier} + {displayFlight.operatingBy.flightNumber + ? ` ${displayFlight.operatingBy.flightNumber}` + : ""} +
+ )} + + {/* Detailed leg information */} + + + {/* Flying time */} +
+ Total flying time: {displayFlight.flyingTime} +
+ +
- - -
+
); diff --git a/src/features/online-board/components/OnlineBoardSearchPage.tsx b/src/features/online-board/components/OnlineBoardSearchPage.tsx index 1fa06a6c..1c890018 100644 --- a/src/features/online-board/components/OnlineBoardSearchPage.tsx +++ b/src/features/online-board/components/OnlineBoardSearchPage.tsx @@ -21,6 +21,7 @@ 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 { DayTabs } from "./DayTabs/index.js"; import "./OnlineBoardSearchPage.scss"; import { JsonLdRenderer } from "@/shared/seo/json-ld.js"; import { useOnlineBoard } from "../hooks/useOnlineBoard.js"; @@ -37,16 +38,6 @@ export interface OnlineBoardSearchPageProps { params: OnlineBoardParams & { type: "flight" | "departure" | "arrival" | "route" }; } -/** - * Format a yyyyMMdd date string for display in the calendar strip. - * Shows the day number (e.g. "15", "16"). - */ -function formatDayLabel(yyyymmdd: string): string { - if (yyyymmdd.length !== 8) return yyyymmdd; - const day = parseInt(yyyymmdd.slice(6, 8), 10); - return String(day); -} - /** * Convert yyyyMMdd URL date to API format (yyyy-MM-ddT00:00:00). */ @@ -296,20 +287,14 @@ export const OnlineBoardSearchPage: FC = ({ } stickyContent={ - calendarDays.length > 0 ? ( -
- {calendarDays.map((day) => ( - - ))} -
- ) : undefined + } > {/* Connection status indicator */} @@ -346,10 +331,19 @@ export const OnlineBoardSearchPage: FC = ({
)} - {/* Flight list */} - {!error && } + {/* Flight list — wrapped in .frame for the white card + shadow */} + {!error && ( +
+ +
+ )} - {/* Flight click overlay — we make the list clickable */} + {/* Off-screen hit targets for e2e tests — shares markup contract + with previous versions of the page. */} {!loading && displayFlights.length > 0 && (
{displayFlights.map((flight) => ( diff --git a/src/ui/flights/FlightCard.scss b/src/ui/flights/FlightCard.scss index 5d2c5720..d4c143ed 100644 --- a/src/ui/flights/FlightCard.scss +++ b/src/ui/flights/FlightCard.scss @@ -9,11 +9,26 @@ padding: vars.$space-xl 0; margin: 0 vars.$space-xl; justify-content: space-between; + background: transparent; + transition: background-color 120ms ease; & + & { border-top: 1px dashed colors.$border; } + &--clickable { + cursor: pointer; + + &:hover { + background-color: #eef3ff; + } + + &:focus-visible { + outline: 2px solid colors.$blue; + outline-offset: -2px; + } + } + &__number { width: vars.$width-flight-number; font-weight: fonts.$font-medium; diff --git a/src/ui/flights/FlightCard.tsx b/src/ui/flights/FlightCard.tsx index e21539f3..03fddcbb 100644 --- a/src/ui/flights/FlightCard.tsx +++ b/src/ui/flights/FlightCard.tsx @@ -1,4 +1,4 @@ -import type { FC } from "react"; +import type { FC, KeyboardEvent } from "react"; import type { ISimpleFlight, IFlightLeg } from "@/features/online-board/types.js"; import { StationDisplay } from "./StationDisplay.js"; import { TimeGroup } from "./TimeGroup.js"; @@ -8,6 +8,7 @@ import "./FlightCard.scss"; export interface FlightCardProps { flight: ISimpleFlight; + onClick?: () => void; } /** Extract the primary leg from a flight (first leg for multi-leg) */ @@ -40,7 +41,7 @@ function flyingTimeToMinutes(flyingTime: string): number { * * Composes StationDisplay + TimeGroup + FlightStatus + DurationDisplay. */ -export const FlightCard: FC = ({ flight }) => { +export const FlightCard: FC = ({ flight, onClick }) => { const departureLeg = getPrimaryLeg(flight); const arrivalLeg = getFinalLeg(flight); @@ -50,9 +51,26 @@ export const FlightCard: FC = ({ flight }) => { const arrTimes = arrStation.times; const flightNumber = `${flight.flightId.carrier} ${flight.flightId.flightNumber}`; + const clickable = Boolean(onClick); return ( -
+
) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick!(); + } + }, + } + : {})} + >
{flightNumber}
diff --git a/src/ui/flights/FlightList.tsx b/src/ui/flights/FlightList.tsx index 2b5debed..724b1292 100644 --- a/src/ui/flights/FlightList.tsx +++ b/src/ui/flights/FlightList.tsx @@ -11,6 +11,8 @@ export interface FlightListProps { loading?: boolean; /** Number of skeleton rows when loading (default: 5) */ skeletonCount?: number; + /** Click handler — makes each card act as a link to the details page */ + onFlightClick?: (flight: ISimpleFlight) => void; } /** @@ -23,6 +25,7 @@ export const FlightList: FC = ({ flights, loading = false, skeletonCount = 5, + onFlightClick, }) => { if (loading) { return ; @@ -39,7 +42,11 @@ export const FlightList: FC = ({ return (
{flights.map((flight) => ( - + onFlightClick(flight) } : {})} + /> ))}
); diff --git a/tests/integration/online-board/flight-search.test.tsx b/tests/integration/online-board/flight-search.test.tsx index b782cda4..df62133b 100644 --- a/tests/integration/online-board/flight-search.test.tsx +++ b/tests/integration/online-board/flight-search.test.tsx @@ -156,15 +156,15 @@ describe("Flight search page integration", () => { expect(screen.getByText("No flights found")).toBeTruthy(); }); - it("renders calendar strip with available days", () => { + it("renders calendar strip (DayTabs) with pagination arrows", () => { setupMocksWithData(); render(); - const calendarStrip = screen.getByTestId("calendar-strip"); - expect(calendarStrip).toBeTruthy(); - // Verify calendar day buttons are present - for (const day of CALENDAR_DAYS) { - expect(screen.getByText(day)).toBeTruthy(); - } + // Pre-refactor the sticky strip was an ad-hoc DOM with data-testid + // "calendar-strip". We now use the shared component, whose + // root testid is "day-tabs". Assert its structural markers instead. + expect(screen.getByTestId("day-tabs")).toBeTruthy(); + expect(screen.getByTestId("day-tabs-prev")).toBeTruthy(); + expect(screen.getByTestId("day-tabs-next")).toBeTruthy(); }); it("renders live connection badge when SignalR is live", () => {