diff --git a/docs/superpowers/specs/2026-04-17-board-details-header-design.md b/docs/superpowers/specs/2026-04-17-board-details-header-design.md new file mode 100644 index 00000000..443b9f8a --- /dev/null +++ b/docs/superpowers/specs/2026-04-17-board-details-header-design.md @@ -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 +
+

{flightNumber}

// Now in PageLayout title from B.2 + {displayFlight.status} +
+``` + +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 = { + 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 `
` 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 +
+ {displayFlight.status} +
+``` + +with: +```tsx + +``` + +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` — `` 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