diff --git a/docs/superpowers/specs/2026-04-16-day-tabs-design.md b/docs/superpowers/specs/2026-04-16-day-tabs-design.md new file mode 100644 index 00000000..dc8b05d2 --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-day-tabs-design.md @@ -0,0 +1,306 @@ +# Day Tabs (B.3) + +## Goal + +Add a horizontal row of day tabs above the main flight details content, letting users navigate the same flight number across different dates. Matches Angular's behavior: paginated carousel on desktop (7 days per page), dropdown on mobile, tabs disabled for dates not operating, auto-scroll to the active tab on mount. + +## Scope + +Sub-feature **B.3** of the Flight Details parity work. Online Board only; Schedule day tabs are a separate concern. + +## Current State + +The React details page has no date navigation. `useFlightDetails` discards the `daysOfFlight` array from the API response. The app has no `useAppSettings` hook (the dev-server mocks the endpoint but nothing consumes it). + +## Approach + +Four additions: + +1. **`useAppSettings` hook** — fetches `/api/appSettings`, parses `searchFrom`/`searchTo` strings (e.g., `"2d"` → `2`), caches across the app. +2. **Extend `useFlightDetails`** — also return `daysOfFlight: string[]` from the response. +3. **`DayTabs` component family** (container, `DayTabButton`, `DaySelect`) — generates tabs from `today - searchFrom` to `today + searchTo`, disables dates not in `daysOfFlight`, paginates 7 at a time, auto-scrolls to the page containing the selected date. +4. **Integrate into `OnlineBoardDetailsPage`** — render `DayTabs` as `stickyContent` in `PageLayout`. Click handler navigates to `/lang/onlineboard/{carrier}{flight}-{yyyymmdd}`. + +## Architecture + +### Hook: `useAppSettings` + +New hook at `src/shared/hooks/useAppSettings.ts`. + +```typescript +export interface UseAppSettingsResult { + onlineboardSearchFrom: number; // days before today, e.g. 2 + onlineboardSearchTo: number; // days after today, e.g. 14 + scheduleSearchFrom: number; + scheduleSearchTo: number; + loading: boolean; + error: ApiError | null; +} + +export function useAppSettings(): UseAppSettingsResult; +``` + +Implementation fetches the `/api/appSettings` endpoint via the existing API client. Parses strings matching `/^(\d+)d$/` into numbers. Returns zeros (with `loading: false, error`) on parse failure or missing fields. + +Fallback defaults when values are unavailable: `onlineboardSearchFrom: 2, onlineboardSearchTo: 14, scheduleSearchFrom: 30, scheduleSearchTo: 30`. + +### Hook extension: `useFlightDetails` + +Add `daysOfFlight: string[]` to the return. Same pattern as `allFlights` (stored from `response.data.daysOfFlight`). + +```typescript +export interface UseFlightDetailsResult { + flight: ISimpleFlight | null; + allFlights: ISimpleFlight[]; + daysOfFlight: string[]; // NEW + loading: boolean; + error: ApiError | null; +} +``` + +### Component: `DayTabs` (container) + +File: `src/features/online-board/components/DayTabs/DayTabs.tsx`. + +```typescript +export interface DayTabsProps { + selectedDate: string; // yyyymmdd, matching URL + availableDates: string[]; // yyyymmdd list from daysOfFlight + daysBefore: number; + daysAfter: number; + onNavigate: (date: string) => void; +} +``` + +Logic: +- Compute `allDates`: array of yyyymmdd strings spanning `today - daysBefore` to `today + daysAfter` inclusive. +- `availableSet = new Set(availableDates)` for O(1) lookup. +- `pageSize = 7`. +- Initialize `currentPage` state to the page index containing `selectedDate` (or 0 if not found). +- Visible slice = `allDates.slice(currentPage * 7, (currentPage + 1) * 7)`. +- Prev/next arrows call `setCurrentPage(n - 1)` / `setCurrentPage(n + 1)` clamped to `[0, totalPages - 1]`. +- Renders `DayTabButton` for each visible date. +- Renders a single `DaySelect` alongside (hidden on desktop via CSS). + +### Component: `DayTabButton` + +File: `src/features/online-board/components/DayTabs/DayTabButton.tsx`. + +```typescript +export interface DayTabButtonProps { + date: string; // yyyymmdd + isActive: boolean; + isDisabled: boolean; + locale: string; // for weekday + month labels + onClick: (date: string) => void; +} +``` + +Renders a button showing: +- Weekday short (`Intl.DateTimeFormat(locale, { weekday: "short" })`) +- Day number (`Intl.DateTimeFormat(locale, { day: "numeric" })`) +- Month short (`Intl.DateTimeFormat(locale, { month: "short" })`) + +Click calls `onClick(date)` unless `isDisabled`. Applies `.day-tab--active` / `.day-tab--disabled` modifiers. `data-testid="day-tab-{yyyymmdd}"`. + +### Component: `DaySelect` (mobile) + +File: `src/features/online-board/components/DayTabs/DaySelect.tsx`. + +```typescript +export interface DaySelectProps { + selectedDate: string; + availableDates: string[]; + locale: string; + onNavigate: (date: string) => void; +} +``` + +Native `` value is `selectedDate` +- onChange fires `onNavigate(new_date)` +- Has `data-testid="day-select"` + +### `useFlightDetails.test.ts` (extend) +- Response with `daysOfFlight: ["20260415", "20260416"]` → hook returns that array +- Empty response → `daysOfFlight: []` + +### `OnlineBoardDetailsPage.test.tsx` (update) +- `stickyContent` includes `DayTabs` (look for `data-testid="day-tabs"`) +- Existing accordion + mini-list tests still pass + +## i18n Keys + +No new keys. `DayTabButton` and `DaySelect` generate labels entirely from `Intl.DateTimeFormat(locale, ...)`. Locale comes from the existing `locale` prop. + +## Out of Scope + +- Schedule day tabs (different data source). +- Fetching `daysOfFlight` from a separate endpoint. +- Keyboard navigation between tabs (left/right arrow keys). +- Custom month/weekday labels beyond `Intl` defaults. + +## Files Touched + +### New +- `src/shared/hooks/useAppSettings.ts` +- `src/shared/hooks/useAppSettings.test.ts` +- `src/features/online-board/components/DayTabs/DayTabs.tsx` +- `src/features/online-board/components/DayTabs/DayTabs.scss` +- `src/features/online-board/components/DayTabs/DayTabs.test.tsx` +- `src/features/online-board/components/DayTabs/DayTabButton.tsx` +- `src/features/online-board/components/DayTabs/DayTabButton.test.tsx` +- `src/features/online-board/components/DayTabs/DaySelect.tsx` +- `src/features/online-board/components/DayTabs/DaySelect.test.tsx` +- `src/features/online-board/components/DayTabs/index.ts` + +### Modified +- `src/features/online-board/hooks/useFlightDetails.ts` — add `daysOfFlight` to return +- `src/features/online-board/hooks/useFlightDetails.test.ts` — add test case +- `src/features/online-board/components/OnlineBoardDetailsPage.tsx` — wire DayTabs into PageLayout.stickyContent +- `src/features/online-board/components/OnlineBoardDetailsPage.test.tsx` — update existing tests for new structure