Add flights-map types, API functions, hooks, and feature flag
Phase 4A: Define IFlightRoute, IMapMarker, IMapPolyline types; implement searchDestinations and getFlightsMapCalendar API functions with 11 tests; add useFlightsMapSearch, useFlightsMapCalendar hooks; add FEATURE_FLIGHTS_MAP env var for feature flag gating.
This commit is contained in:
Vendored
+3
@@ -18,6 +18,7 @@ const EnvSchema = z.object({
|
|||||||
ANALYTICS_CTM: boolish.default("false"),
|
ANALYTICS_CTM: boolish.default("false"),
|
||||||
ANALYTICS_VARIOCUBE: boolish.default("false"),
|
ANALYTICS_VARIOCUBE: boolish.default("false"),
|
||||||
ANALYTICS_DYNATRACE: boolish.default("false"),
|
ANALYTICS_DYNATRACE: boolish.default("false"),
|
||||||
|
FEATURE_FLIGHTS_MAP: boolish.default("false"),
|
||||||
VERSION: z.string().min(1),
|
VERSION: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ export interface Env {
|
|||||||
OTEL_EXPORTER_OTLP_HEADERS?: string;
|
OTEL_EXPORTER_OTLP_HEADERS?: string;
|
||||||
LOGS_ENDPOINT?: string;
|
LOGS_ENDPOINT?: string;
|
||||||
ANALYTICS_ENABLED: AnalyticsProviders;
|
ANALYTICS_ENABLED: AnalyticsProviders;
|
||||||
|
FEATURE_FLIGHTS_MAP: boolean;
|
||||||
VERSION: string;
|
VERSION: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ export function getEnv(): Env {
|
|||||||
variocube: raw.ANALYTICS_VARIOCUBE,
|
variocube: raw.ANALYTICS_VARIOCUBE,
|
||||||
dynatrace: raw.ANALYTICS_DYNATRACE,
|
dynatrace: raw.ANALYTICS_DYNATRACE,
|
||||||
},
|
},
|
||||||
|
FEATURE_FLIGHTS_MAP: raw.FEATURE_FLIGHTS_MAP,
|
||||||
VERSION: raw.VERSION,
|
VERSION: raw.VERSION,
|
||||||
};
|
};
|
||||||
if (raw.OTEL_EXPORTER_OTLP_ENDPOINT !== undefined) {
|
if (raw.OTEL_EXPORTER_OTLP_ENDPOINT !== undefined) {
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
import { describe, it, expect, vi } from "vitest";
|
||||||
|
import { ApiClient } from "@/shared/api/client";
|
||||||
|
import { searchDestinations, getFlightsMapCalendar } from "./api";
|
||||||
|
import type {
|
||||||
|
FlightsMapSearchParams,
|
||||||
|
FlightsMapCalendarParams,
|
||||||
|
IDestinationsResponse,
|
||||||
|
IFlightsMapDaysResponse,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function createMockClient(responseBody: unknown, status = 200) {
|
||||||
|
const mockFetch = vi.fn<typeof fetch>().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<typeof vi.fn>): URL {
|
||||||
|
const call = mockFetch.mock.calls[0] as [string, ...unknown[]];
|
||||||
|
return new URL(call[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const DESTINATIONS_RESPONSE: IDestinationsResponse = {
|
||||||
|
data: {
|
||||||
|
routes: [
|
||||||
|
{ route: ["SVO", "LED"], isDirect: true },
|
||||||
|
{ route: ["SVO", "DME", "LED"], isDirect: false },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DAYS_RESPONSE: IFlightsMapDaysResponse = {
|
||||||
|
days: "01101",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// searchDestinations
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("searchDestinations", () => {
|
||||||
|
it("sends GET to destinations/1 with query params", async () => {
|
||||||
|
const { client, mockFetch } = createMockClient(DESTINATIONS_RESPONSE);
|
||||||
|
|
||||||
|
const params: FlightsMapSearchParams = {
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
dateFrom: "20250601",
|
||||||
|
dateTo: "20251201",
|
||||||
|
};
|
||||||
|
|
||||||
|
await searchDestinations(client, params);
|
||||||
|
|
||||||
|
const url = extractUrl(mockFetch);
|
||||||
|
expect(url.pathname).toBe("/destinations/1");
|
||||||
|
expect(url.searchParams.get("departure")).toBe("SVO");
|
||||||
|
expect(url.searchParams.get("arrival")).toBe("LED");
|
||||||
|
expect(url.searchParams.get("dateFrom")).toBe("20250601");
|
||||||
|
expect(url.searchParams.get("dateTo")).toBe("20251201");
|
||||||
|
expect(url.searchParams.get("connections")).toBe("0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits arrival param when not provided", async () => {
|
||||||
|
const { client, mockFetch } = createMockClient(DESTINATIONS_RESPONSE);
|
||||||
|
|
||||||
|
await searchDestinations(client, {
|
||||||
|
departure: "SVO",
|
||||||
|
dateFrom: "20250601",
|
||||||
|
dateTo: "20251201",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = extractUrl(mockFetch);
|
||||||
|
expect(url.searchParams.has("arrival")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sends connections param when specified", async () => {
|
||||||
|
const { client, mockFetch } = createMockClient(DESTINATIONS_RESPONSE);
|
||||||
|
|
||||||
|
await searchDestinations(client, {
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
dateFrom: "20250601",
|
||||||
|
dateTo: "20251201",
|
||||||
|
connections: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = extractUrl(mockFetch);
|
||||||
|
expect(url.searchParams.get("connections")).toBe("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the deserialized response", async () => {
|
||||||
|
const { client } = createMockClient(DESTINATIONS_RESPONSE);
|
||||||
|
|
||||||
|
const result = await searchDestinations(client, {
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
dateFrom: "20250601",
|
||||||
|
dateTo: "20251201",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(DESTINATIONS_RESPONSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on server error", async () => {
|
||||||
|
const { client } = createMockClient({ error: "internal" }, 500);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
searchDestinations(client, {
|
||||||
|
departure: "SVO",
|
||||||
|
dateFrom: "20250601",
|
||||||
|
dateTo: "20251201",
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("HTTP 500");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// getFlightsMapCalendar
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("getFlightsMapCalendar", () => {
|
||||||
|
it("builds correct path for direct route", async () => {
|
||||||
|
const { client, mockFetch } = createMockClient(DAYS_RESPONSE);
|
||||||
|
|
||||||
|
await getFlightsMapCalendar(client, {
|
||||||
|
date: "20250601",
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
connections: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = extractUrl(mockFetch);
|
||||||
|
expect(url.pathname).toBe("/days/20250601/200/route/SVO-LED/flights-map/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds correct path for connecting route", async () => {
|
||||||
|
const { client, mockFetch } = createMockClient(DAYS_RESPONSE);
|
||||||
|
|
||||||
|
await getFlightsMapCalendar(client, {
|
||||||
|
date: "20250601",
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
connections: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = extractUrl(mockFetch);
|
||||||
|
expect(url.pathname).toBe(
|
||||||
|
"/days/20250601/200/connections/SVO-LED-1/flights-map/v1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds correct path for departure-only (spider mode)", async () => {
|
||||||
|
const { client, mockFetch } = createMockClient(DAYS_RESPONSE);
|
||||||
|
|
||||||
|
await getFlightsMapCalendar(client, {
|
||||||
|
date: "20250601",
|
||||||
|
departure: "SVO",
|
||||||
|
connections: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = extractUrl(mockFetch);
|
||||||
|
expect(url.pathname).toBe("/days/20250601/200/departure/SVO/flights-map/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses binary days string into available date strings", async () => {
|
||||||
|
const { client } = createMockClient({ days: "10110" });
|
||||||
|
|
||||||
|
const result = await getFlightsMapCalendar(client, {
|
||||||
|
date: "20250601",
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
connections: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(["20250601", "20250603", "20250604"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array for empty days string", async () => {
|
||||||
|
const { client } = createMockClient({ days: "" });
|
||||||
|
|
||||||
|
const result = await getFlightsMapCalendar(client, {
|
||||||
|
date: "20250601",
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
connections: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws on server error", async () => {
|
||||||
|
const { client } = createMockClient({ error: "internal" }, 500);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
getFlightsMapCalendar(client, {
|
||||||
|
date: "20250601",
|
||||||
|
departure: "SVO",
|
||||||
|
arrival: "LED",
|
||||||
|
connections: false,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("HTTP 500");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* Flights Map 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 {
|
||||||
|
FlightsMapSearchParams,
|
||||||
|
FlightsMapCalendarParams,
|
||||||
|
IDestinationsResponse,
|
||||||
|
IFlightsMapDaysResponse,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// API functions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search flight destinations/routes on the map.
|
||||||
|
* Maps to: `GET destinations/1?departure=...&arrival=...&dateFrom=...&dateTo=...&connections=0`
|
||||||
|
*/
|
||||||
|
export async function searchDestinations(
|
||||||
|
client: ApiClient,
|
||||||
|
params: FlightsMapSearchParams,
|
||||||
|
): Promise<IDestinationsResponse> {
|
||||||
|
const query: Record<string, string> = {
|
||||||
|
departure: params.departure,
|
||||||
|
dateFrom: params.dateFrom,
|
||||||
|
dateTo: params.dateTo,
|
||||||
|
connections: String(params.connections ?? 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.arrival) {
|
||||||
|
query["arrival"] = params.arrival;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.get<IDestinationsResponse>("destinations/1", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available calendar days for a flights-map route.
|
||||||
|
* Maps to: `GET days/{date}/200/{routeSegment}/flights-map/v1`
|
||||||
|
*
|
||||||
|
* The `days` response field is a binary string ("0110...") where each
|
||||||
|
* character represents one day starting from `date`. We parse it into
|
||||||
|
* an array of available date strings.
|
||||||
|
*/
|
||||||
|
export async function getFlightsMapCalendar(
|
||||||
|
client: ApiClient,
|
||||||
|
params: FlightsMapCalendarParams,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const routeSegment = buildRouteSegment(params);
|
||||||
|
if (!routeSegment) return [];
|
||||||
|
|
||||||
|
const path = `days/${params.date}/200/${routeSegment}/flights-map/v1`;
|
||||||
|
const response = await client.get<IFlightsMapDaysResponse>(path);
|
||||||
|
return parseBinaryDays(response.days, params.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function buildRouteSegment(params: FlightsMapCalendarParams): string | null {
|
||||||
|
if (params.departure && params.arrival) {
|
||||||
|
return params.connections
|
||||||
|
? `connections/${params.departure}-${params.arrival}-1`
|
||||||
|
: `route/${params.departure}-${params.arrival}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.departure) {
|
||||||
|
return `departure/${params.departure}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a binary days string ("01101...") into an array of ISO date strings
|
||||||
|
* representing the available (=1) days, starting from `startDate`.
|
||||||
|
*/
|
||||||
|
function parseBinaryDays(days: string, startDate: string): string[] {
|
||||||
|
if (!days) return [];
|
||||||
|
|
||||||
|
const result: string[] = [];
|
||||||
|
const start = parseYyyymmdd(startDate);
|
||||||
|
if (!start) return [];
|
||||||
|
|
||||||
|
for (let i = 0; i < days.length; i++) {
|
||||||
|
if (days[i] === "1") {
|
||||||
|
const d = new Date(start);
|
||||||
|
d.setDate(d.getDate() + i);
|
||||||
|
result.push(formatYyyymmdd(d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseYyyymmdd(s: string): Date | null {
|
||||||
|
if (s.length !== 8) return null;
|
||||||
|
const year = Number(s.slice(0, 4));
|
||||||
|
const month = Number(s.slice(4, 6)) - 1;
|
||||||
|
const day = Number(s.slice(6, 8));
|
||||||
|
const d = new Date(year, month, day);
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatYyyymmdd(d: Date): string {
|
||||||
|
const y = d.getFullYear().toString();
|
||||||
|
const m = (d.getMonth() + 1).toString().padStart(2, "0");
|
||||||
|
const day = d.getDate().toString().padStart(2, "0");
|
||||||
|
return `${y}${m}${day}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Feature flag hook for the flights-map feature.
|
||||||
|
*
|
||||||
|
* Reads from environment configuration. In future phases this can be
|
||||||
|
* extended to read from a remote feature flag service.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getEnv } from "@/env/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known feature flag names.
|
||||||
|
*/
|
||||||
|
type FeatureFlagName = "flightsMap";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a feature flag is enabled.
|
||||||
|
* Currently backed by env vars; extend to remote config as needed.
|
||||||
|
*/
|
||||||
|
export function useFeatureFlag(flag: FeatureFlagName): boolean {
|
||||||
|
const env = getEnv();
|
||||||
|
|
||||||
|
switch (flag) {
|
||||||
|
case "flightsMap":
|
||||||
|
return env.FEATURE_FLIGHTS_MAP ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* React hook for flights map calendar day availability.
|
||||||
|
*
|
||||||
|
* Calls `getFlightsMapCalendar` on param change, manages loading/days state.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useApiClient } from "@/shared/api/provider.js";
|
||||||
|
import { getFlightsMapCalendar } from "../api.js";
|
||||||
|
import type { FlightsMapCalendarParams } from "../types.js";
|
||||||
|
|
||||||
|
export interface UseFlightsMapCalendarResult {
|
||||||
|
availableDays: string[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for the calendar strip. Fetches available flights-map days for the
|
||||||
|
* given route context.
|
||||||
|
*/
|
||||||
|
export function useFlightsMapCalendar(
|
||||||
|
params: FlightsMapCalendarParams | null,
|
||||||
|
): UseFlightsMapCalendarResult {
|
||||||
|
const client = useApiClient();
|
||||||
|
const [availableDays, setAvailableDays] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const paramsRef = useRef(params);
|
||||||
|
paramsRef.current = params;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!paramsRef.current) {
|
||||||
|
setAvailableDays([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
getFlightsMapCalendar(client, paramsRef.current)
|
||||||
|
.then((result) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setAvailableDays(result);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setAvailableDays([]);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
client,
|
||||||
|
params?.date,
|
||||||
|
params?.departure,
|
||||||
|
params?.arrival,
|
||||||
|
params?.connections,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { availableDays, loading };
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* React hook for flights map destination search.
|
||||||
|
*
|
||||||
|
* Calls `searchDestinations` on param change, manages loading/error/data state.
|
||||||
|
* No SignalR -- map data is static.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
|
import { useApiClient } from "@/shared/api/provider.js";
|
||||||
|
import { searchDestinations } from "../api.js";
|
||||||
|
import type { FlightsMapSearchParams, IDestinationsResponse, IFlightRoute } from "../types.js";
|
||||||
|
import type { ApiError } from "@/shared/api/errors.js";
|
||||||
|
|
||||||
|
export interface UseFlightsMapSearchResult {
|
||||||
|
routes: IFlightRoute[];
|
||||||
|
loading: boolean;
|
||||||
|
error: ApiError | null;
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for the flights map. Fetches destination routes based on search params
|
||||||
|
* and provides refresh capability.
|
||||||
|
*/
|
||||||
|
export function useFlightsMapSearch(
|
||||||
|
params: FlightsMapSearchParams | null,
|
||||||
|
): UseFlightsMapSearchResult {
|
||||||
|
const client = useApiClient();
|
||||||
|
const [routes, setRoutes] = useState<IFlightRoute[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<ApiError | null>(null);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
|
||||||
|
const paramsRef = useRef(params);
|
||||||
|
paramsRef.current = params;
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
setRefreshKey((k) => k + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!paramsRef.current) {
|
||||||
|
setRoutes([]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
searchDestinations(client, paramsRef.current)
|
||||||
|
.then((response: IDestinationsResponse) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setRoutes(response.data.routes);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: ApiError) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(err);
|
||||||
|
setRoutes([]);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
client,
|
||||||
|
params?.departure,
|
||||||
|
params?.arrival,
|
||||||
|
params?.dateFrom,
|
||||||
|
params?.dateTo,
|
||||||
|
params?.connections,
|
||||||
|
refreshKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { routes, loading, error, refresh };
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* Data model types for the Flights Map feature.
|
||||||
|
*
|
||||||
|
* Covers API request/response shapes, map marker/polyline props,
|
||||||
|
* and filter state.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// API request types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for GET destinations/1 search.
|
||||||
|
*/
|
||||||
|
export interface FlightsMapSearchParams {
|
||||||
|
departure: string;
|
||||||
|
arrival?: string;
|
||||||
|
dateFrom: string;
|
||||||
|
dateTo: string;
|
||||||
|
connections?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for GET days/{date}/200/{route}/flights-map/v1 calendar.
|
||||||
|
*/
|
||||||
|
export interface FlightsMapCalendarParams {
|
||||||
|
date: string;
|
||||||
|
departure: string;
|
||||||
|
arrival?: string;
|
||||||
|
connections: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// API response types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single flight route from the destinations API.
|
||||||
|
*/
|
||||||
|
export interface IFlightRoute {
|
||||||
|
route: string[];
|
||||||
|
isDirect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from GET destinations/1.
|
||||||
|
*/
|
||||||
|
export interface IDestinationsResponse {
|
||||||
|
data: {
|
||||||
|
routes: IFlightRoute[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response from GET days/.../flights-map/v1.
|
||||||
|
* The `days` field is a string of "0" and "1" characters, one per day.
|
||||||
|
*/
|
||||||
|
export interface IFlightsMapDaysResponse {
|
||||||
|
days: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Map component prop types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker style variant for the map.
|
||||||
|
*/
|
||||||
|
export type MarkerStyle = "blue" | "blue-small" | "orange";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A marker to render on the map.
|
||||||
|
*/
|
||||||
|
export interface IMapMarker {
|
||||||
|
id: string;
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
style: MarkerStyle;
|
||||||
|
label?: string;
|
||||||
|
tooltipPermanent?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polyline style variant for the map.
|
||||||
|
*/
|
||||||
|
export type PolylineStyle = "direct" | "connecting";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A polyline to render on the map.
|
||||||
|
*/
|
||||||
|
export interface IMapPolyline {
|
||||||
|
id: string;
|
||||||
|
points: Array<{ lat: number; lng: number }>;
|
||||||
|
style: PolylineStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Popup content to show on the map.
|
||||||
|
*/
|
||||||
|
export interface IMapPopup {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Filter state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State shape for the flights map filter.
|
||||||
|
*/
|
||||||
|
export interface IFlightsMapFilterState {
|
||||||
|
departure?: string;
|
||||||
|
arrival?: string;
|
||||||
|
date?: string;
|
||||||
|
connections: boolean;
|
||||||
|
domestic: boolean;
|
||||||
|
international: boolean;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user