Add Phase 5A types, API function, and usePopularRequests hook
Ports Angular PopularRequestsApiService and IPopularRequest types to React with pure API function + React hook pattern matching existing features (online-board, schedule).
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { ApiClient } from "@/shared/api/client";
|
||||
import { getPopularRequests } from "./api";
|
||||
import type { PopularRequest } 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 POPULAR_RESPONSE: PopularRequest[] = [
|
||||
{
|
||||
mode: "FlightNumber",
|
||||
carrier: "SU",
|
||||
flightNumber: "9027",
|
||||
type: "Onlineboard",
|
||||
},
|
||||
{
|
||||
mode: "FlightNumber",
|
||||
carrier: "SU",
|
||||
flightNumber: "9006",
|
||||
type: "Onlineboard",
|
||||
},
|
||||
{
|
||||
mode: "Route",
|
||||
departure: "KUF",
|
||||
arrival: "ABA",
|
||||
type: "Onlineboard",
|
||||
},
|
||||
{
|
||||
mode: "Route",
|
||||
departure: "RTW",
|
||||
arrival: "ABA",
|
||||
type: "Onlineboard",
|
||||
},
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getPopularRequests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("getPopularRequests", () => {
|
||||
it("calls the correct API path", async () => {
|
||||
const { client, mockFetch } = createMockClient(POPULAR_RESPONSE);
|
||||
|
||||
await getPopularRequests(client);
|
||||
|
||||
const url = extractUrl(mockFetch);
|
||||
expect(url.pathname).toBe("/Requests/1/getpopular");
|
||||
});
|
||||
|
||||
it("returns the deserialized PopularRequest[]", async () => {
|
||||
const { client } = createMockClient(POPULAR_RESPONSE);
|
||||
|
||||
const result = await getPopularRequests(client);
|
||||
|
||||
expect(result).toEqual(POPULAR_RESPONSE);
|
||||
});
|
||||
|
||||
it("throws ApiHttpError on 404", async () => {
|
||||
const { client } = createMockClient({ error: "not found" }, 404);
|
||||
|
||||
await expect(getPopularRequests(client)).rejects.toThrow("HTTP 404");
|
||||
});
|
||||
|
||||
it("throws ApiHttpError on 500", async () => {
|
||||
const { client } = createMockClient({ error: "internal" }, 500);
|
||||
|
||||
await expect(getPopularRequests(client)).rejects.toThrow("HTTP 500");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Popular Requests 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 { PopularRequest } from "./types.js";
|
||||
|
||||
/**
|
||||
* Fetch popular requests from the API.
|
||||
* Maps to: `GET /Requests/1/getpopular`
|
||||
*
|
||||
* Angular equivalent: `PopularRequestsApiService.getPopularRequests()`
|
||||
* which called `endpointService.buildCommonURL('getpopular', '1', 'Requests')`
|
||||
*/
|
||||
export async function getPopularRequests(
|
||||
client: ApiClient,
|
||||
): Promise<PopularRequest[]> {
|
||||
return client.get<PopularRequest[]>("Requests/1/getpopular");
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* React hook for fetching popular requests.
|
||||
*
|
||||
* Calls `getPopularRequests` on mount, manages loading/error/data state.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useApiClient } from "@/shared/api/provider.js";
|
||||
import { getPopularRequests } from "../api.js";
|
||||
import type { PopularRequest } from "../types.js";
|
||||
import type { ApiError } from "@/shared/api/errors.js";
|
||||
|
||||
export interface UsePopularRequestsResult {
|
||||
requests: PopularRequest[];
|
||||
loading: boolean;
|
||||
error: ApiError | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook that fetches popular requests on mount. Returns loading/error/data.
|
||||
*/
|
||||
export function usePopularRequests(): UsePopularRequestsResult {
|
||||
const client = useApiClient();
|
||||
const [requests, setRequests] = useState<PopularRequest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<ApiError | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
getPopularRequests(client)
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setRequests(data);
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err: ApiError) => {
|
||||
if (!cancelled) {
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [client]);
|
||||
|
||||
return { requests, loading, error };
|
||||
}
|
||||
@@ -1,2 +1,27 @@
|
||||
// Public barrel for the popular-requests feature. See frozen-barrels.md.
|
||||
export {};
|
||||
|
||||
// 5A — Types
|
||||
export type {
|
||||
RequestMode,
|
||||
PopularRequestType,
|
||||
PopularRequest,
|
||||
PopularRouteRequest,
|
||||
PopularArrivalRequest,
|
||||
PopularDepartureRequest,
|
||||
PopularFlightNumberRequest,
|
||||
} from "./types.js";
|
||||
|
||||
// 5A — API functions
|
||||
export { getPopularRequests } from "./api.js";
|
||||
|
||||
// 5A — React hooks
|
||||
export { usePopularRequests } from "./hooks/usePopularRequests.js";
|
||||
export type { UsePopularRequestsResult } from "./hooks/usePopularRequests.js";
|
||||
|
||||
// 5B — Components
|
||||
export { PopularRequestsPanel } from "./components/PopularRequestsPanel.js";
|
||||
export type { PopularRequestsPanelProps } from "./components/PopularRequestsPanel.js";
|
||||
export { PopularRequestItem } from "./components/PopularRequestItem.js";
|
||||
export type { PopularRequestItemProps } from "./components/PopularRequestItem.js";
|
||||
export { RequestInfo } from "./components/RequestInfo.js";
|
||||
export type { RequestInfoProps } from "./components/RequestInfo.js";
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Data model types for the Popular Requests feature.
|
||||
*
|
||||
* Ported from Angular typings (ClientApp/src/typings/popular-request.ts,
|
||||
* ClientApp/src/typings/enums.ts) — flattened into minimal, UI-oriented
|
||||
* types with no Angular dependencies.
|
||||
*/
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enums & literals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Request mode — discriminator for the popular request union */
|
||||
export type RequestMode =
|
||||
| "FlightNumber"
|
||||
| "Route"
|
||||
| "RouteWithBack"
|
||||
| "Departure"
|
||||
| "Arrival";
|
||||
|
||||
/** Which feature area the request navigates to */
|
||||
export type PopularRequestType = "Schedule" | "Onlineboard";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Request sub-types (discriminated by `mode`)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PopularRouteRequest {
|
||||
mode: "Route" | "RouteWithBack";
|
||||
departure: string;
|
||||
arrival: string;
|
||||
type: PopularRequestType;
|
||||
}
|
||||
|
||||
export interface PopularArrivalRequest {
|
||||
mode: "Arrival";
|
||||
arrival: string;
|
||||
type: "Onlineboard";
|
||||
}
|
||||
|
||||
export interface PopularDepartureRequest {
|
||||
mode: "Departure";
|
||||
departure: string;
|
||||
type: "Onlineboard";
|
||||
}
|
||||
|
||||
export interface PopularFlightNumberRequest {
|
||||
mode: "FlightNumber";
|
||||
carrier: string;
|
||||
flightNumber: string;
|
||||
type: "Onlineboard";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Union type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Any popular request returned by the API */
|
||||
export type PopularRequest =
|
||||
| PopularRouteRequest
|
||||
| PopularArrivalRequest
|
||||
| PopularDepartureRequest
|
||||
| PopularFlightNumberRequest;
|
||||
Reference in New Issue
Block a user