diff --git a/docs/superpowers/specs/2026-04-16-flights-mini-list-design.md b/docs/superpowers/specs/2026-04-16-flights-mini-list-design.md new file mode 100644 index 00000000..eb253b7b --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-flights-mini-list-design.md @@ -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` 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 `` 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 ( + <> + + + } + title={

{flightNumberFormatted}

} + breadcrumbs={[ + { label: t("BREADCRUMBS.ONLINEBOARD"), href: `/${locale}/onlineboard` }, + { label: flightNumberFormatted }, + ]} + contentLeft={ + + } + > +
+ {/* existing content: connection status, summary card, legs, accordion, flying time */} +
+
+ +); +``` + +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}"`. +- `` `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.