Add flights mini-list sidebar (B.2) design spec

This commit is contained in:
2026-04-16 23:14:13 +03:00
parent 4a8ae20c47
commit 5759d165c4
@@ -0,0 +1,230 @@
# Flights Mini-List Sidebar (B.2)
## Goal
Add a left sidebar on the Online Board flight details page showing all flights with the same flight number on different dates, matching Angular's mini-list behavior. Clicking an item navigates to that flight's details; the current flight is highlighted and auto-scrolled into view.
## Scope
Sub-feature **B.2** of the Flight Details parity work. Online Board only — the Schedule details page mini-list (accordion grouped by date) is a separate concern and out of scope here.
## Current State
- React `OnlineBoardDetailsPage` uses a custom standalone layout (no sidebar).
- `useFlightDetails` fetches `/onlineboard/details` and returns `response.data.routes[0]`, discarding the rest of the array.
- The Angular equivalent (`online-board-flights-mini-list`) renders a left-column sidebar (285px desktop, hidden below 1050px) with the full `routes` array.
## Approach
Three changes, three focused commits:
1. **Extend `useFlightDetails`** to return both `flight` (for current display) and `allFlights` (for the list). The API already returns the array; we're just exposing it.
2. **Add `FlightsMiniList` + `FlightsMiniListItem`** components with auto-scroll-into-view and Link-based navigation.
3. **Refactor `OnlineBoardDetailsPage`** to use `PageLayout`, putting the mini-list in `contentLeft`.
## Architecture
### Hook: `useFlightDetails` (extended)
Before:
```typescript
interface UseFlightDetailsResult {
flight: ISimpleFlight | null;
loading: boolean;
error: ApiError | null;
}
```
After:
```typescript
interface UseFlightDetailsResult {
flight: ISimpleFlight | null;
allFlights: ISimpleFlight[];
loading: boolean;
error: ApiError | null;
}
```
Implementation: store `response.data.routes` as `allFlights`, set `flight = allFlights[0] ?? null`.
### Component: `FlightsMiniList`
```typescript
interface FlightsMiniListProps {
flights: ISimpleFlight[];
currentFlight: ISimpleFlight;
}
```
- Returns `null` when `flights.length <= 1` (nothing to navigate between).
- Maps to `FlightsMiniListItem` per flight.
- Uses a `Map<string, HTMLAnchorElement>` ref to track item nodes by `flight.id`.
- `useEffect([currentFlight.id])` calls `scrollIntoView({ block: "center", behavior: "smooth" })` on the selected item.
### Component: `FlightsMiniListItem`
```typescript
interface FlightsMiniListItemProps {
flight: ISimpleFlight;
isSelected: boolean;
}
```
- Renders a React Router `<Link>` to the flight's details URL.
- URL built via `buildFlightDetailsUrl(flight, lang)` — add helper to `src/features/online-board/url.ts` if absent.
- DOM structure:
- `.mini-list__flight-number` (e.g., "SU 0022")
- `.mini-list__content` 2x2 CSS Grid:
- Row 1: `.mini-list__dep-time` | `.mini-list__status-icon` | `.mini-list__arr-time`
- Row 2: `.mini-list__dep-station` | — | `.mini-list__arr-station`
- Times prefer latest (`actualBlockOff?.local ?? scheduledDeparture.local`).
- Multi-leg: use `legs[0].departure` and `legs[legs.length - 1].arrival`.
- Direct: use `leg.departure` and `leg.arrival`.
- Applies `--selected` modifier class when `isSelected`.
- Status icon: existing `FlightStatus` component.
### URL Helper
If `src/features/online-board/url.ts` doesn't already export `buildFlightDetailsUrl`, add:
```typescript
export function buildFlightDetailsUrl(flight: ISimpleFlight, lang: string): string {
const { carrier, flightNumber, suffix, date } = flight.flightId;
const suffixStr = suffix ?? "";
// date is already YYYYMMDD in IFlightId
return `/${lang}/onlineboard/${carrier}${flightNumber}${suffixStr}-${date}`;
}
```
### Integration: `OnlineBoardDetailsPage`
Wrap the current content in `PageLayout`:
```tsx
return (
<>
<SeoHead {...seoProps} />
<JsonLdRenderer data={jsonLdData} />
<PageLayout
headerLeft={<PageTabs viewType="onlineboard" />}
title={<h1 className="flight-details__title">{flightNumberFormatted}</h1>}
breadcrumbs={[
{ label: t("BREADCRUMBS.ONLINEBOARD"), href: `/${locale}/onlineboard` },
{ label: flightNumberFormatted },
]}
contentLeft={
<FlightsMiniList
flights={allFlights}
currentFlight={displayFlight}
/>
}
>
<div className="flight-details">
{/* existing content: connection status, summary card, legs, accordion, flying time */}
</div>
</PageLayout>
</>
);
```
Loading/error/not-found states render inside the `PageLayout` children so the sidebar still shows.
## Styling
`FlightsMiniList.scss`:
```scss
.mini-list {
display: flex;
flex-direction: column;
max-height: calc(100vh - 170px);
overflow-y: auto;
background: #fff;
border-radius: 8px;
&__item {
padding: 12px;
border-bottom: 1px solid #e0e0e0;
text-decoration: none;
color: inherit;
display: block;
&:last-child { border-bottom: none; }
&:hover { background: #f8f9fa; }
&--selected {
border: 2px solid #2060c0;
border-radius: 4px;
}
}
&__flight-number {
font-size: 12px;
color: #666;
margin-bottom: 4px;
}
&__content {
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-rows: auto auto;
gap: 8px 12px;
}
&__dep-time, &__arr-time {
font-size: 16px;
font-weight: 500;
color: #1a3a5c;
}
&__arr-time { text-align: right; }
&__status-icon { grid-column: 2; grid-row: 1; }
&__dep-station { grid-column: 1; grid-row: 2; }
&__arr-station { grid-column: 3; grid-row: 2; text-align: right; }
}
```
Responsive hiding (tablet/mobile) is handled by `PageLayout` — no extra CSS needed at the mini-list level.
## Testing
### Hook test (extend `useFlightDetails.test.ts`)
- Returns `allFlights` = `response.data.routes` array.
- Returns `flight === allFlights[0]`.
- Returns `allFlights: []` when response is empty or errors.
### `FlightsMiniList.test.tsx`
- Returns null when `flights.length <= 1`.
- Renders one item per flight.
- Highlights the item matching `currentFlight.id`.
- Calls `scrollIntoView` on the selected item's element (mocked).
- Works with both Direct and MultiLeg flights.
### `FlightsMiniListItem.test.tsx`
- Renders flight number, dep/arr times, station codes.
- Has `data-testid="mini-list-item-{id}"`.
- `<Link>` `href` points to the correct details URL.
- Applies `--selected` modifier when `isSelected=true`.
- Multi-leg flight: shows first leg's departure and last leg's arrival stations.
### `OnlineBoardDetailsPage.test.tsx` (update existing)
- Still passes the accordion integration tests from B.1.
- Renders `PageLayout` wrapper (check for page-layout class or testid).
- Mini-list visible when mock returns multiple flights.
- Mini-list not visible when mock returns single flight.
## i18n Keys
Reuse existing. Only one new key likely needed: `BREADCRUMBS.ONLINEBOARD` (check existing — the start page likely uses it already). No new keys specific to the mini-list contents (flight number, times, station codes are data, not labels).
## Out of Scope
- Schedule details page mini-list (separate accordion-grouped component).
- Day tabs (B.3).
- Status header action buttons (B.4).
- Transfer / multi-leg timeline (B.5).
- Back button / flight schedule timeline (B.6).
- Changes to SignalR integration.