From 8218dffcd96381c9047df64082638463aba843a6 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 08:09:53 +0300 Subject: [PATCH] Add Phase 2C: Online Board API functions and React hooks Pure API functions (searchFlights, getFlightDetails, getCalendarDays) with dependency-injected ApiClient, plus three thin React hooks (useOnlineBoard, useFlightDetails, useCalendarDays) for search, details, and calendar pages. 17 TDD tests for API layer covering URL construction, response mapping, and error handling. --- .../plans/2026-04-15-phase-2c-api-hooks.md | 96 ++++++ src/features/online-board/api.test.ts | 297 ++++++++++++++++++ src/features/online-board/api.ts | 139 ++++++++ .../online-board/hooks/useCalendarDays.ts | 63 ++++ .../online-board/hooks/useFlightDetails.ts | 65 ++++ .../online-board/hooks/useOnlineBoard.ts | 80 +++++ src/features/online-board/index.ts | 21 +- 7 files changed, 759 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-15-phase-2c-api-hooks.md create mode 100644 src/features/online-board/api.test.ts create mode 100644 src/features/online-board/api.ts create mode 100644 src/features/online-board/hooks/useCalendarDays.ts create mode 100644 src/features/online-board/hooks/useFlightDetails.ts create mode 100644 src/features/online-board/hooks/useOnlineBoard.ts diff --git a/docs/superpowers/plans/2026-04-15-phase-2c-api-hooks.md b/docs/superpowers/plans/2026-04-15-phase-2c-api-hooks.md new file mode 100644 index 00000000..e493f298 --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-phase-2c-api-hooks.md @@ -0,0 +1,96 @@ +# Phase 2C — API Client Functions + React Hooks + +> **Parent plan:** `2026-04-14-phase-2-online-board-master.md` (sub-plan 2C) + +## Goal + +Wrap the three Online Board REST endpoints in typed, pure API functions and thin React hooks. API functions are dependency-injected with `ApiClient`; hooks use `useApiClient()` from context. + +## Prerequisites + +- Phase 1 `ApiClient` (`src/shared/api/client.ts`) and `useApiClient` (`src/shared/api/provider.tsx`) exist. +- Phase 2A types (`src/features/online-board/types.ts`) exist: `ISimpleFlight`, `IBoardResponse`, `IDaysResponse`, `IParsedFlightId`, `FlightRequestType`. +- Phase 2B URL serializer (`src/features/online-board/url.ts`) exists. + +## Deliverables + +| File | Purpose | +|---|---| +| `src/features/online-board/api.ts` | Pure API functions: `searchFlights`, `getFlightDetails`, `getCalendarDays` | +| `src/features/online-board/api.test.ts` | TDD tests for API functions (mock fetch, verify URL construction + response mapping) | +| `src/features/online-board/hooks/useOnlineBoard.ts` | React hook for search pages | +| `src/features/online-board/hooks/useFlightDetails.ts` | React hook for details page | +| `src/features/online-board/hooks/useCalendarDays.ts` | React hook for calendar availability | +| `src/features/online-board/index.ts` | Updated barrel exports | + +## Tasks + +### T1: Define API function param types and write `api.ts` + +**File:** `src/features/online-board/api.ts` + +Three pure functions, each taking `ApiClient` as first param: + +1. `searchFlights(client, params)` — builds `GET /{locale}/board?...` with optional query params: `flightNumber`, `dateFrom`, `dateTo`, `departure`, `arrival`, `timeFrom`, `timeTo`. Returns `IBoardResponse`. + +2. `getFlightDetails(client, params)` — builds `GET /{locale}/onlineboard/details?flights={carrier}{number}&dates={date}`. Returns `IBoardResponse`. + +3. `getCalendarDays(client, params)` — builds `GET /v1/days/{date}/31/{searchType}/{searchParams}/board/`. Returns parsed `string[]` from `IDaysResponse.days`. + +**Key design decisions:** +- Functions are PURE — no context, no hooks, no side effects. +- Use `ApiClient.get(path, query)` for query-param endpoints. +- For calendar endpoint, build the path string directly (it's path-based, not query-based). +- Map `IDaysResponse.days` (a single string) to `string[]` by splitting on comma. + +### T2: Write TDD tests for `api.ts` + +**File:** `src/features/online-board/api.test.ts` + +Test cases: +- `searchFlights`: correct URL + query params for flight-number search, route search, departure-only, arrival-only; response deserialization +- `getFlightDetails`: correct URL + query params; response deserialization +- `getCalendarDays`: correct path construction for flight type, route type, departure type, arrival type; response mapping from comma-separated string to array +- Error cases: 404 throws `ApiHttpError`, timeout throws `ApiTimeoutError` + +Mock strategy: create `ApiClient` with a mock `fetchImpl` that captures the request URL and returns canned responses. + +### T3: Write `useOnlineBoard` hook + +**File:** `src/features/online-board/hooks/useOnlineBoard.ts` + +- Gets `ApiClient` via `useApiClient()` +- `useState` for flights, loading, error +- `useEffect` calls `searchFlights` on param change +- Returns `{ flights, loading, error, refresh }` + +### T4: Write `useFlightDetails` hook + +**File:** `src/features/online-board/hooks/useFlightDetails.ts` + +- Gets `ApiClient` via `useApiClient()` +- `useState` for flight, loading, error +- `useEffect` calls `getFlightDetails` on param change +- Returns `{ flight, loading, error }` + +### T5: Write `useCalendarDays` hook + +**File:** `src/features/online-board/hooks/useCalendarDays.ts` + +- Gets `ApiClient` via `useApiClient()` +- `useState` for days, loading +- `useEffect` calls `getCalendarDays` on param change +- Returns `{ days, loading }` + +### T6: Update barrel exports in `index.ts` + +Add API functions and hooks to the public barrel. + +### T7: Verify — `pnpm typecheck && pnpm lint && pnpm test` + +## Exit gate + +- All API function tests pass (URL construction, response mapping, error handling). +- `pnpm typecheck` green — zero `any` types. +- `pnpm lint` green. +- Hooks compile and export correctly. diff --git a/src/features/online-board/api.test.ts b/src/features/online-board/api.test.ts new file mode 100644 index 00000000..29dbd9dd --- /dev/null +++ b/src/features/online-board/api.test.ts @@ -0,0 +1,297 @@ +import { describe, it, expect, vi } from "vitest"; +import { ApiClient } from "@/shared/api/client"; +import { + searchFlights, + getFlightDetails, + getCalendarDays, +} from "./api"; +import type { + SearchFlightsParams, + FlightDetailsParams, + CalendarParams, +} from "./api"; +import type { IBoardResponse, IDaysResponse } from "./types"; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Create an ApiClient with a mock fetch that captures the request URL + * and returns a canned response. + */ +function createMockClient(responseBody: unknown, status = 200) { + const mockFetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(responseBody), { + status, + headers: { "Content-Type": "application/json" }, + }), + ); + + const client = new ApiClient({ + baseUrl: "https://api.test.com", + locale: "ru", + fetchImpl: mockFetch, + retry: { maxRetries: 0 }, + }); + + return { client, mockFetch }; +} + +function extractUrl(mockFetch: ReturnType): URL { + const call = mockFetch.mock.calls[0] as [string, ...unknown[]]; + return new URL(call[0]); +} + +/** Minimal valid IBoardResponse for testing */ +const BOARD_RESPONSE: IBoardResponse = { + data: { + partners: ["SU"], + routes: [], + daysOfFlight: ["2025-01-15"], + }, +}; + +const DAYS_RESPONSE: IDaysResponse = { + days: "2025-01-15,2025-01-16,2025-01-17", +}; + +// --------------------------------------------------------------------------- +// searchFlights +// --------------------------------------------------------------------------- + +describe("searchFlights", () => { + it("sends dateFrom and dateTo as query params", async () => { + const { client, mockFetch } = createMockClient(BOARD_RESPONSE); + + const params: SearchFlightsParams = { + dateFrom: "20250115", + dateTo: "20250116", + }; + + await searchFlights(client, params); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe("/board"); + expect(url.searchParams.get("dateFrom")).toBe("20250115"); + expect(url.searchParams.get("dateTo")).toBe("20250116"); + }); + + it("includes flightNumber when provided", async () => { + const { client, mockFetch } = createMockClient(BOARD_RESPONSE); + + await searchFlights(client, { + flightNumber: "SU100", + dateFrom: "20250115", + dateTo: "20250116", + }); + + const url = extractUrl(mockFetch); + expect(url.searchParams.get("flightNumber")).toBe("SU100"); + }); + + it("includes departure and arrival when provided", async () => { + const { client, mockFetch } = createMockClient(BOARD_RESPONSE); + + await searchFlights(client, { + dateFrom: "20250115", + dateTo: "20250116", + departure: "SVO", + arrival: "JFK", + }); + + const url = extractUrl(mockFetch); + expect(url.searchParams.get("departure")).toBe("SVO"); + expect(url.searchParams.get("arrival")).toBe("JFK"); + }); + + it("includes timeFrom and timeTo when provided", async () => { + const { client, mockFetch } = createMockClient(BOARD_RESPONSE); + + await searchFlights(client, { + dateFrom: "20250115", + dateTo: "20250116", + timeFrom: "0800", + timeTo: "2000", + }); + + const url = extractUrl(mockFetch); + expect(url.searchParams.get("timeFrom")).toBe("0800"); + expect(url.searchParams.get("timeTo")).toBe("2000"); + }); + + it("omits optional params when not provided", async () => { + const { client, mockFetch } = createMockClient(BOARD_RESPONSE); + + await searchFlights(client, { + dateFrom: "20250115", + dateTo: "20250116", + }); + + const url = extractUrl(mockFetch); + expect(url.searchParams.has("flightNumber")).toBe(false); + expect(url.searchParams.has("departure")).toBe(false); + expect(url.searchParams.has("arrival")).toBe(false); + expect(url.searchParams.has("timeFrom")).toBe(false); + expect(url.searchParams.has("timeTo")).toBe(false); + }); + + it("returns the deserialized IBoardResponse", async () => { + const { client } = createMockClient(BOARD_RESPONSE); + + const result = await searchFlights(client, { + dateFrom: "20250115", + dateTo: "20250116", + }); + + expect(result).toEqual(BOARD_RESPONSE); + }); + + it("throws ApiHttpError on 404", async () => { + const { client } = createMockClient({ error: "not found" }, 404); + + await expect( + searchFlights(client, { dateFrom: "20250115", dateTo: "20250116" }), + ).rejects.toThrow("HTTP 404"); + }); +}); + +// --------------------------------------------------------------------------- +// getFlightDetails +// --------------------------------------------------------------------------- + +describe("getFlightDetails", () => { + it("sends flights and dates as query params", async () => { + const { client, mockFetch } = createMockClient(BOARD_RESPONSE); + + const params: FlightDetailsParams = { + flights: "SU100", + dates: "2025-01-15", + }; + + await getFlightDetails(client, params); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe("/onlineboard/details"); + expect(url.searchParams.get("flights")).toBe("SU100"); + expect(url.searchParams.get("dates")).toBe("2025-01-15"); + }); + + it("returns the deserialized IBoardResponse", async () => { + const { client } = createMockClient(BOARD_RESPONSE); + + const result = await getFlightDetails(client, { + flights: "SU100", + dates: "2025-01-15", + }); + + expect(result).toEqual(BOARD_RESPONSE); + }); + + it("throws ApiHttpError on 404", async () => { + const { client } = createMockClient({ error: "not found" }, 404); + + await expect( + getFlightDetails(client, { flights: "SU999", dates: "2025-01-15" }), + ).rejects.toThrow("HTTP 404"); + }); +}); + +// --------------------------------------------------------------------------- +// getCalendarDays +// --------------------------------------------------------------------------- + +describe("getCalendarDays", () => { + it("builds correct path for flight type", async () => { + const { client, mockFetch } = createMockClient(DAYS_RESPONSE); + + const params: CalendarParams = { + date: "2025-01-15", + searchType: "flight", + flightNumber: "SU100", + }; + + await getCalendarDays(client, params); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe("/v1/days/2025-01-15/31/flight/SU100/board/"); + }); + + it("builds correct path for departure type", async () => { + const { client, mockFetch } = createMockClient(DAYS_RESPONSE); + + await getCalendarDays(client, { + date: "2025-01-15", + searchType: "departure", + departure: "SVO", + }); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe("/v1/days/2025-01-15/31/departure/SVO/board/"); + }); + + it("builds correct path for arrival type", async () => { + const { client, mockFetch } = createMockClient(DAYS_RESPONSE); + + await getCalendarDays(client, { + date: "2025-01-15", + searchType: "arrival", + arrival: "JFK", + }); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe("/v1/days/2025-01-15/31/arrival/JFK/board/"); + }); + + it("builds correct path for route type", async () => { + const { client, mockFetch } = createMockClient(DAYS_RESPONSE); + + await getCalendarDays(client, { + date: "2025-01-15", + searchType: "route", + departure: "SVO", + arrival: "JFK", + }); + + const url = extractUrl(mockFetch); + expect(url.pathname).toBe( + "/v1/days/2025-01-15/31/route/SVO-JFK/board/", + ); + }); + + it("parses comma-separated days string into array", async () => { + const { client } = createMockClient(DAYS_RESPONSE); + + const result = await getCalendarDays(client, { + date: "2025-01-15", + searchType: "flight", + flightNumber: "SU100", + }); + + expect(result).toEqual(["2025-01-15", "2025-01-16", "2025-01-17"]); + }); + + it("returns empty array for empty days string", async () => { + const { client } = createMockClient({ days: "" }); + + const result = await getCalendarDays(client, { + date: "2025-01-15", + searchType: "flight", + flightNumber: "SU100", + }); + + expect(result).toEqual([]); + }); + + it("throws ApiHttpError on server error", async () => { + const { client } = createMockClient({ error: "internal" }, 500); + + await expect( + getCalendarDays(client, { + date: "2025-01-15", + searchType: "flight", + flightNumber: "SU100", + }), + ).rejects.toThrow("HTTP 500"); + }); +}); diff --git a/src/features/online-board/api.ts b/src/features/online-board/api.ts new file mode 100644 index 00000000..9abc5899 --- /dev/null +++ b/src/features/online-board/api.ts @@ -0,0 +1,139 @@ +/** + * Online Board API functions. + * + * Pure functions — each takes an `ApiClient` as a parameter (dependency + * injection). No React hooks, no context, no side effects. + * + * @module + */ + +import type { ApiClient } from "@/shared/api/client.js"; +import type { + IBoardResponse, + IDaysResponse, + FlightRequestType, +} from "./types.js"; + +// --------------------------------------------------------------------------- +// Parameter types +// --------------------------------------------------------------------------- + +export interface SearchFlightsParams { + /** Flight number search: e.g. "SU100" */ + flightNumber?: string; + /** Date range start — yyyyMMdd format (used by the API as `dateFrom`) */ + dateFrom: string; + /** Date range end — yyyyMMdd format (used by the API as `dateTo`) */ + dateTo: string; + /** Departure airport IATA code */ + departure?: string; + /** Arrival airport IATA code */ + arrival?: string; + /** Time range start — HHmm format */ + timeFrom?: string; + /** Time range end — HHmm format */ + timeTo?: string; +} + +export interface FlightDetailsParams { + /** Carrier code + flight number, e.g. "SU100" */ + flights: string; + /** Date in yyyy-MM-dd format */ + dates: string; +} + +export interface CalendarParams { + /** Base date — yyyy-MM-dd format */ + date: string; + /** Search type discriminator */ + searchType: FlightRequestType; + /** Flight number (for "flight" type), e.g. "SU100" */ + flightNumber?: string; + /** Departure airport IATA code (for "departure" or "route" type) */ + departure?: string; + /** Arrival airport IATA code (for "arrival" or "route" type) */ + arrival?: string; +} + +// --------------------------------------------------------------------------- +// API functions +// --------------------------------------------------------------------------- + +/** + * Search flights on the online board. + * Maps to: `GET /board?flightNumber=...&dateFrom=...&dateTo=...&...` + */ +export async function searchFlights( + client: ApiClient, + params: SearchFlightsParams, +): Promise { + const query: Record = { + dateFrom: params.dateFrom, + dateTo: params.dateTo, + }; + + if (params.flightNumber) query["flightNumber"] = params.flightNumber; + if (params.departure) query["departure"] = params.departure; + if (params.arrival) query["arrival"] = params.arrival; + if (params.timeFrom) query["timeFrom"] = params.timeFrom; + if (params.timeTo) query["timeTo"] = params.timeTo; + + return client.get("board", query); +} + +/** + * Get flight details. + * Maps to: `GET /onlineboard/details?flights=SU100&dates=2025-01-15` + */ +export async function getFlightDetails( + client: ApiClient, + params: FlightDetailsParams, +): Promise { + return client.get("onlineboard/details", { + flights: params.flights, + dates: params.dates, + }); +} + +/** + * Get available calendar days for a given search context. + * Maps to: `GET /v1/days/{date}/31/{searchType}/{searchParams}/board/` + * + * The API returns `{ days: "2025-01-01,2025-01-02,..." }` — a single + * comma-separated string. This function splits it into `string[]`. + */ +export async function getCalendarDays( + client: ApiClient, + params: CalendarParams, +): Promise { + const searchSegment = buildCalendarSearchSegment(params); + const path = `v1/days/${params.date}/31/${searchSegment}/board/`; + + const response = await client.get(path); + return parseCalendarDays(response.days); +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +function buildCalendarSearchSegment(params: CalendarParams): string { + switch (params.searchType) { + case "flight": + return `flight/${params.flightNumber ?? ""}`; + case "departure": + return `departure/${params.departure ?? ""}`; + case "arrival": + return `arrival/${params.arrival ?? ""}`; + case "route": { + const dep = params.departure ?? ""; + const arr = params.arrival ?? ""; + return `route/${dep}-${arr}`; + } + } +} + +function parseCalendarDays(days: string): string[] { + if (!days) return []; + return days.split(",").map((d) => d.trim()).filter(Boolean); +} diff --git a/src/features/online-board/hooks/useCalendarDays.ts b/src/features/online-board/hooks/useCalendarDays.ts new file mode 100644 index 00000000..86f3389c --- /dev/null +++ b/src/features/online-board/hooks/useCalendarDays.ts @@ -0,0 +1,63 @@ +/** + * React hook for calendar day availability. + * + * Calls `getCalendarDays` on param change, manages loading/days state. + * Thin wrapper — integration-tested by 2E/2H, not unit-tested here. + * + * @module + */ + +import { useState, useEffect, useRef } from "react"; +import { useApiClient } from "@/shared/api/provider.js"; +import { getCalendarDays } from "../api.js"; +import type { CalendarParams } from "../api.js"; + +export interface UseCalendarDaysResult { + days: string[]; + loading: boolean; +} + +/** + * Hook for the calendar strip. Fetches available flight days for the + * given search context. + */ +export function useCalendarDays(params: CalendarParams): UseCalendarDaysResult { + const client = useApiClient(); + const [days, setDays] = useState([]); + const [loading, setLoading] = useState(true); + + const paramsRef = useRef(params); + paramsRef.current = params; + + useEffect(() => { + let cancelled = false; + setLoading(true); + + getCalendarDays(client, paramsRef.current) + .then((result) => { + if (!cancelled) { + setDays(result); + setLoading(false); + } + }) + .catch(() => { + if (!cancelled) { + setDays([]); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ + client, + params.date, + params.searchType, + params.flightNumber, + params.departure, + params.arrival, + ]); + + return { days, loading }; +} diff --git a/src/features/online-board/hooks/useFlightDetails.ts b/src/features/online-board/hooks/useFlightDetails.ts new file mode 100644 index 00000000..c96c9325 --- /dev/null +++ b/src/features/online-board/hooks/useFlightDetails.ts @@ -0,0 +1,65 @@ +/** + * React hook for the flight details page. + * + * Calls `getFlightDetails` on param change, manages loading/error/data state. + * Thin wrapper — integration-tested by 2E/2H, not unit-tested here. + * + * @module + */ + +import { useState, useEffect, useRef } from "react"; +import { useApiClient } from "@/shared/api/provider.js"; +import { getFlightDetails } from "../api.js"; +import type { FlightDetailsParams } from "../api.js"; +import type { ISimpleFlight } from "../types.js"; +import type { ApiError } from "@/shared/api/errors.js"; + +export interface UseFlightDetailsResult { + flight: ISimpleFlight | null; + loading: boolean; + error: ApiError | null; +} + +/** + * Hook for the flight details page. Fetches a single flight's details + * based on flight ID params. + */ +export function useFlightDetails( + params: FlightDetailsParams, +): UseFlightDetailsResult { + const client = useApiClient(); + const [flight, setFlight] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const paramsRef = useRef(params); + paramsRef.current = params; + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + getFlightDetails(client, paramsRef.current) + .then((response) => { + if (!cancelled) { + // Details endpoint returns a board response; take the first route + const firstFlight = response.data.routes[0] ?? null; + setFlight(firstFlight); + setLoading(false); + } + }) + .catch((err: ApiError) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [client, params.flights, params.dates]); + + return { flight, loading, error }; +} diff --git a/src/features/online-board/hooks/useOnlineBoard.ts b/src/features/online-board/hooks/useOnlineBoard.ts new file mode 100644 index 00000000..4e4474c1 --- /dev/null +++ b/src/features/online-board/hooks/useOnlineBoard.ts @@ -0,0 +1,80 @@ +/** + * React hook for the online board search pages. + * + * Calls `searchFlights` on param change, manages loading/error/data state. + * Thin wrapper — integration-tested by 2E/2H, not unit-tested here. + * + * @module + */ + +import { useState, useEffect, useCallback, useRef } from "react"; +import { useApiClient } from "@/shared/api/provider.js"; +import { searchFlights } from "../api.js"; +import type { SearchFlightsParams } from "../api.js"; +import type { ISimpleFlight } from "../types.js"; +import type { ApiError } from "@/shared/api/errors.js"; + +export interface UseOnlineBoardResult { + flights: ISimpleFlight[]; + loading: boolean; + error: ApiError | null; + refresh: () => void; +} + +/** + * Hook for online board search pages. Fetches flights based on search params + * and provides refresh capability. + */ +export function useOnlineBoard( + params: SearchFlightsParams, +): UseOnlineBoardResult { + const client = useApiClient(); + const [flights, setFlights] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [refreshKey, setRefreshKey] = useState(0); + + // Serialize params for useEffect dependency + const paramsRef = useRef(params); + paramsRef.current = params; + + const refresh = useCallback(() => { + setRefreshKey((k) => k + 1); + }, []); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + searchFlights(client, paramsRef.current) + .then((response) => { + if (!cancelled) { + setFlights(response.data.routes); + setLoading(false); + } + }) + .catch((err: ApiError) => { + if (!cancelled) { + setError(err); + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, [ + client, + params.flightNumber, + params.dateFrom, + params.dateTo, + params.departure, + params.arrival, + params.timeFrom, + params.timeTo, + refreshKey, + ]); + + return { flights, loading, error, refresh }; +} diff --git a/src/features/online-board/index.ts b/src/features/online-board/index.ts index 356923e8..e0a4a5b3 100644 --- a/src/features/online-board/index.ts +++ b/src/features/online-board/index.ts @@ -3,7 +3,8 @@ // must import exclusively from "@/features/online-board", never from // deeper paths. See docs/superpowers/phase-1/frozen-barrels.md for the rule. -export type { OnlineBoardParams } from "./url"; +// 2B — URL serializer/parser +export type { OnlineBoardParams } from "./url.js"; export { parseOnlineBoardUrl, buildOnlineBoardUrl, @@ -11,4 +12,20 @@ export { buildFlightUrlParams, parseStationUrlParams, parseRouteUrlParams, -} from "./url"; +} from "./url.js"; + +// 2C — API functions +export type { + SearchFlightsParams, + FlightDetailsParams, + CalendarParams, +} from "./api.js"; +export { searchFlights, getFlightDetails, getCalendarDays } from "./api.js"; + +// 2C — React hooks +export { useOnlineBoard } from "./hooks/useOnlineBoard.js"; +export type { UseOnlineBoardResult } from "./hooks/useOnlineBoard.js"; +export { useFlightDetails } from "./hooks/useFlightDetails.js"; +export type { UseFlightDetailsResult } from "./hooks/useFlightDetails.js"; +export { useCalendarDays } from "./hooks/useCalendarDays.js"; +export type { UseCalendarDaysResult } from "./hooks/useCalendarDays.js";