Add board details header + action buttons (B.4) design spec
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
# Board Details Header + Action Buttons (B.4)
|
||||
|
||||
## Goal
|
||||
|
||||
Add the flight status header to the React Online Board details page: airline logo, flight number, codesharing, action buttons (Buy Ticket, Register, Flight Status, Share, Print), flight event indicators (route change, reroute), and a last-update timestamp. Match Angular parity including time-based button visibility logic.
|
||||
|
||||
## Scope
|
||||
|
||||
Sub-feature **B.4** of the Flight Details parity work. Online Board details page only.
|
||||
|
||||
## Current State
|
||||
|
||||
The React details page has a minimal header:
|
||||
```tsx
|
||||
<div className="flight-details__header">
|
||||
<h1>{flightNumber}</h1> // Now in PageLayout title from B.2
|
||||
<span>{displayFlight.status}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
Angular has a rich `board-details-header` component with 10+ sub-components, visibility logic, and airline-specific behavior.
|
||||
|
||||
## Approach
|
||||
|
||||
Full Angular parity port. New components live under `src/features/online-board/components/BoardDetailsHeader/`. Pure visibility logic lives in `visibility/*.ts` as side-effect-free functions, tested in isolation. Airline configuration (URLs, capabilities) goes in `airlines.ts` as a constant map. Time math uses `date-fns` (new dependency).
|
||||
|
||||
## New Dependency
|
||||
|
||||
- `date-fns` (~25kb tree-shakable) — `parseISO`, `subHours`, `isAfter`, `isBefore`, `isSameDay`
|
||||
|
||||
## Architecture
|
||||
|
||||
### File structure
|
||||
|
||||
```
|
||||
src/features/online-board/components/BoardDetailsHeader/
|
||||
├── BoardDetailsHeader.tsx # Orchestrator
|
||||
├── BoardDetailsHeader.scss
|
||||
├── BoardDetailsHeader.test.tsx
|
||||
├── DetailsHeaderBadge.tsx # Flight number + codesharing + operator logo + small status button
|
||||
├── DetailsHeaderBadge.test.tsx
|
||||
├── OperatorLogo.tsx # CSS-class-driven airline logo
|
||||
├── OperatorLogo.scss # Ported from Angular's _logos.scss
|
||||
├── OperatorLogo.test.tsx
|
||||
├── FlightActions.tsx # Container for 5 action buttons
|
||||
├── FlightActions.test.tsx
|
||||
├── BuyTicketButton.tsx
|
||||
├── BuyTicketButton.test.tsx
|
||||
├── RegistrationButton.tsx
|
||||
├── RegistrationButton.test.tsx
|
||||
├── FlightStatusButton.tsx
|
||||
├── FlightStatusButton.test.tsx
|
||||
├── ShareButton.tsx # Button + panel together
|
||||
├── SharePanel.tsx
|
||||
├── ShareButton.test.tsx
|
||||
├── SharePanel.test.tsx
|
||||
├── PrintButton.tsx # Stub (empty URL, hidden)
|
||||
├── FlightEvents.tsx # Route change + reroute icons
|
||||
├── FlightEvents.test.tsx
|
||||
├── LastUpdate.tsx # Timestamp + mobile share
|
||||
├── LastUpdate.test.tsx
|
||||
├── airlines.ts # Airline URL/capability config
|
||||
├── actions.scss # Shared button styles
|
||||
├── icons/
|
||||
│ ├── change.svg
|
||||
│ ├── return.svg
|
||||
│ ├── share.svg
|
||||
│ └── print.svg
|
||||
├── airlines-logo/ # 35 airline logo folders copied from Angular
|
||||
│ └── aeroflot/, rossiya/, aurora/, ...
|
||||
├── visibility/
|
||||
│ ├── buyTicketVisibility.ts
|
||||
│ ├── buyTicketVisibility.test.ts
|
||||
│ ├── registrationVisibility.ts
|
||||
│ ├── registrationVisibility.test.ts
|
||||
│ ├── flightStatusVisibility.ts
|
||||
│ └── flightStatusVisibility.test.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### `useAppSettings` hook extension
|
||||
|
||||
```typescript
|
||||
export interface UseAppSettingsResult {
|
||||
// Existing
|
||||
onlineboardSearchFrom: number;
|
||||
onlineboardSearchTo: number;
|
||||
scheduleSearchFrom: number;
|
||||
scheduleSearchTo: number;
|
||||
// New (B.4)
|
||||
flightStatusAvailableFromHours: number; // default 24
|
||||
buyTicketMinHours: number; // default 2
|
||||
buyTicketMaxHours: number; // default 72
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
```
|
||||
|
||||
Parses `uiOptions.buttons.flightStatus.availableFrom` (e.g., `"24h"`) and `uiOptions.buttons.buyTicket.period.min/max`. Pattern: `/^(\d+)h$/`. Falls back to defaults if parsing fails.
|
||||
|
||||
### Visibility logic (pure functions in `visibility/`)
|
||||
|
||||
**`canBuyTicket(flight, now, minHours, maxHours)`**
|
||||
- Returns false if status is "Cancelled" or "InFlight"
|
||||
- Extracts first leg's scheduled departure UTC
|
||||
- Returns true only when `now` is between `departure - maxHours` and `departure - minHours`
|
||||
|
||||
**`canRegister(flight, airlineConfig)`**
|
||||
- Gets `flight.operatingBy.carrier`
|
||||
- Returns false if carrier has no `registrationUrl`
|
||||
- Returns true only when `firstLeg.transition.registration.status === "InProgress"`
|
||||
|
||||
**`canViewFlightStatus(flight, now, availableFromHours, airlinesWithStatus)`**
|
||||
- Returns false if carrier not in set `{"SU", "HZ", "FV", "DP"}`
|
||||
- Returns false if `now` is not same-day as departure
|
||||
- Returns true when `now > departure - availableFromHours`
|
||||
|
||||
### Airline config (`airlines.ts`)
|
||||
|
||||
```typescript
|
||||
export interface AirlineConfig {
|
||||
name: string;
|
||||
registrationUrl?: string;
|
||||
hasNativeStatus: boolean;
|
||||
statusUrl?: string; // only when hasNativeStatus is false
|
||||
}
|
||||
|
||||
export const AIRLINES: Record<string, AirlineConfig> = {
|
||||
SU: { name: "Aeroflot", registrationUrl: "https://www.aeroflot.ru/sb/ckin/app/ru-ru", hasNativeStatus: true },
|
||||
FV: { name: "Rossiya", registrationUrl: "https://www.rossiya-airlines.com/flight-with-us/before_flight/the_ways_of_check-in/", hasNativeStatus: true },
|
||||
HZ: { name: "Aurora", registrationUrl: "https://www.flyaurora.ru", hasNativeStatus: false, statusUrl: "https://www.flyaurora.ru" },
|
||||
DP: { name: "Pobeda", registrationUrl: "https://www.pobeda.aero", hasNativeStatus: false, statusUrl: "https://www.pobeda.aero" },
|
||||
AF: { name: "AirFrance", hasNativeStatus: false },
|
||||
};
|
||||
|
||||
export const AIRLINES_WITH_STATUS = new Set(["SU", "HZ", "FV", "DP"]);
|
||||
```
|
||||
|
||||
### Component contracts
|
||||
|
||||
**`BoardDetailsHeader`**
|
||||
```typescript
|
||||
interface Props {
|
||||
flight: ISimpleFlight;
|
||||
locale: string;
|
||||
}
|
||||
```
|
||||
Orchestrates `DetailsHeaderBadge`, `FlightActions`, `FlightEvents`, `LastUpdate` in a 2-column grid.
|
||||
|
||||
**`DetailsHeaderBadge`**
|
||||
```typescript
|
||||
interface Props {
|
||||
flight: ISimpleFlight;
|
||||
large?: boolean;
|
||||
round?: boolean;
|
||||
showStatus?: boolean; // whether to render the small Flight Status button
|
||||
}
|
||||
```
|
||||
Renders flight number, codesharing partners below (from `legs.map(l => l.operatingBy)`), `OperatorLogo`, and optionally a small `FlightStatusButton`.
|
||||
|
||||
**`OperatorLogo`**
|
||||
```typescript
|
||||
interface Props {
|
||||
flight: ISimpleFlight;
|
||||
locale: string;
|
||||
round?: boolean;
|
||||
large?: boolean;
|
||||
caption?: boolean;
|
||||
}
|
||||
```
|
||||
Renders an empty `<div>` with classes `company-logo company-logo--{CARRIER} {large? "large" :""} {round? "round" :""} {locale === "ru" ? "ru" : ""}`. The SCSS applies background images.
|
||||
|
||||
**`FlightActions`**
|
||||
```typescript
|
||||
interface Props {
|
||||
flight: ISimpleFlight;
|
||||
locale: string;
|
||||
viewType: "Onlineboard" | "Schedule";
|
||||
showStatus?: boolean; // default true
|
||||
showDetails?: boolean; // default false
|
||||
showPrint?: boolean; // default false (hidden on details page)
|
||||
showShare?: boolean; // default true
|
||||
showRegister?: boolean; // default true
|
||||
showBuy?: boolean; // default true
|
||||
}
|
||||
```
|
||||
Renders each action button conditional on visibility logic + show prop.
|
||||
|
||||
**`FlightEvents`**
|
||||
```typescript
|
||||
interface Props {
|
||||
changeRoute: boolean;
|
||||
reroute: boolean;
|
||||
direction?: "row" | "column-mobile";
|
||||
showDescription?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
**`LastUpdate`**
|
||||
```typescript
|
||||
interface Props {
|
||||
flight: ISimpleFlight;
|
||||
locale: string;
|
||||
}
|
||||
```
|
||||
Renders timestamp (`HH:mm DD.MM.YYYY`) via `date-fns` `format`. Also renders a mobile-only `ShareButton` next to it.
|
||||
|
||||
### Button behaviors (click handlers)
|
||||
|
||||
**BuyTicket:** opens `https://www.aeroflot.ru/sb/app/{locale}-{locale}#/search?adults=1&cabin=economy&children=0&infants=0&routes={dep}.{yyyymmdd}.{arr}&autosearch=Y` in new tab.
|
||||
|
||||
**Registration:** opens `AIRLINES[carrier].registrationUrl` in new tab.
|
||||
|
||||
**FlightStatus:** if `hasNativeStatus`, opens the flight's own details URL in new tab; otherwise opens `statusUrl` in new tab.
|
||||
|
||||
**Share:** toggles `SharePanel`. Panel renders:
|
||||
- Facebook: `https://www.facebook.com/sharer/sharer.php?u={url}`
|
||||
- VK: `http://vk.com/share.php?url={url}`
|
||||
- Twitter: `http://twitter.com/share?text={text}&url={url}`
|
||||
- Weibo (zh only): `http://service.weibo.com/share/share.php?url={url}`
|
||||
- Copy button: `navigator.clipboard.writeText(url)`
|
||||
|
||||
`url` is built from the current page's absolute URL.
|
||||
|
||||
**Print:** empty URL (matches Angular). The component is always rendered hidden on the details page via `showPrint={false}`.
|
||||
|
||||
### Integration into `OnlineBoardDetailsPage`
|
||||
|
||||
Replace:
|
||||
```tsx
|
||||
<div className="flight-details__header">
|
||||
<span className="flight-details__overall-status">{displayFlight.status}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
with:
|
||||
```tsx
|
||||
<BoardDetailsHeader flight={displayFlight} locale={locale} />
|
||||
```
|
||||
|
||||
Move the overall-status indication into the header's badge. Remove the stale `flight-details__header` div from the main content — the header is the entire top section now.
|
||||
|
||||
## Styling
|
||||
|
||||
### Copied from Angular `_logos.scss`
|
||||
|
||||
All 35 `.company-logo--{CODE}` rules with CSS `background-image` URLs. Paths rewritten from `~src/assets/...` to relative (`./airlines-logo/...`). Rspack handles the `.svg`/`.png` imports as file assets.
|
||||
|
||||
### Action buttons
|
||||
|
||||
Shared `actions.scss`:
|
||||
```scss
|
||||
.flight-action-btn {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
|
||||
&--orange { background: #ff9000; color: #fff; }
|
||||
&--blue-light { background: #e3f0ff; color: #1a3a5c; }
|
||||
&--transparent { background: transparent; padding: 8px; }
|
||||
}
|
||||
```
|
||||
|
||||
### Header layout
|
||||
|
||||
```scss
|
||||
.board-details-header {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
|
||||
&__badge { grid-column: 1; }
|
||||
&__actions { grid-column: 2; display: flex; gap: 8px; justify-content: flex-end; }
|
||||
&__events { grid-column: 3; }
|
||||
&__last-update { grid-column: 1 / -1; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 16px;
|
||||
.share-button--desktop { display: none; }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Visibility logic (unit, pure functions)
|
||||
|
||||
- `buyTicketVisibility.test.ts` — window boundary tests (just before showFrom, middle, just after showUntil), status exclusions
|
||||
- `registrationVisibility.test.ts` — airline support matrix, status "InProgress" check
|
||||
- `flightStatusVisibility.test.ts` — same-day boundary, airline filtering, time window
|
||||
|
||||
### Components (unit)
|
||||
|
||||
- `OperatorLogo.test.tsx` — applies correct CSS class per carrier code
|
||||
- `DetailsHeaderBadge.test.tsx` — flight number, codesharing list, small status button visibility
|
||||
- `BuyTicketButton.test.tsx` / `RegistrationButton.test.tsx` / `FlightStatusButton.test.tsx` — render, click opens expected URL (mock `window.open`)
|
||||
- `ShareButton.test.tsx` — toggles panel visibility
|
||||
- `SharePanel.test.tsx` — renders 4 social links, Weibo conditional, copy calls `navigator.clipboard`
|
||||
- `FlightEvents.test.tsx` — conditional rendering of change/reroute icons
|
||||
- `LastUpdate.test.tsx` — timestamp format, mobile share visibility
|
||||
- `FlightActions.test.tsx` — renders only enabled visible buttons
|
||||
- `BoardDetailsHeader.test.tsx` — integration smoke test
|
||||
|
||||
### Hook extension
|
||||
|
||||
- `useAppSettings.test.ts` — new tests for `buttons` parsing
|
||||
|
||||
### Integration
|
||||
|
||||
- `OnlineBoardDetailsPage.test.tsx` — `<BoardDetailsHeader>` replaces the inline header
|
||||
|
||||
## i18n
|
||||
|
||||
No new keys needed. Reuses:
|
||||
- `SHARED.BUY-TICKET`, `SHARED.ONLINE-REGISTRATION`
|
||||
- `SHARED.DETAILS`, `SHARED.DETAILS-TOOLTIP`, `SHARED.FLIGHT-INFO`
|
||||
- `SHARED.LAST-UPDATE`, `SHARED.ROUTE-CHANGE`, `SHARED.RETURN`
|
||||
- `SHARED.AVIACOMPANY`
|
||||
- `BOARD.SHARE`
|
||||
|
||||
(All confirmed present in `src/i18n/locales/en/common.json`.)
|
||||
|
||||
## Assets
|
||||
|
||||
### Airline logos
|
||||
Copy all 35 directories from `ClientApp/src/assets/img/airlines-logo/` to `src/features/online-board/components/BoardDetailsHeader/airlines-logo/`.
|
||||
|
||||
### Event/action icons
|
||||
Copy `change.svg`, `return.svg`, `share.svg`, `print.svg` from Angular's icon directory to `./icons/`.
|
||||
|
||||
## Files Touched
|
||||
|
||||
### New files
|
||||
- All under `src/features/online-board/components/BoardDetailsHeader/` (as listed in file structure)
|
||||
|
||||
### Modified files
|
||||
- `src/shared/hooks/useAppSettings.ts` — extend with buttons config
|
||||
- `src/shared/hooks/useAppSettings.test.ts` — add tests for buttons parsing
|
||||
- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — replace inline header
|
||||
- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — update mock state
|
||||
- `package.json` — add `date-fns` dependency
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Schedule feature equivalent (`schedule-details-header`)
|
||||
- Print page implementation (URL stays empty per Angular)
|
||||
- Registration state re-fetch on button click
|
||||
- Feature flags for China-specific share filtering beyond locale check
|
||||
- Analytics wiring on button clicks
|
||||
Reference in New Issue
Block a user