Add flights mini-list sidebar (B.2) design spec
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user