plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
5 changed files with 261 additions and 1 deletions
Showing only changes of commit 0a8ccfe36e - Show all commits
+93
View File
@@ -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");
});
});
+24
View File
@@ -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 };
}
+26 -1
View File
@@ -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";
+63
View File
@@ -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;