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 344 additions and 3 deletions
Showing only changes of commit e172df8cf9 - Show all commits
@@ -0,0 +1,152 @@
/**
* Renders a single popular request item, switching display by request mode.
*
* Angular equivalent: `PopularRequestComponent` + the per-mode components
* (ArrivalRequestComponent, DepartureRequestComponent,
* FlightNumberRequestComponent, RouteRequestComponent).
*
* In React, these are inlined as a single component with a switch,
* since each mode branch is 2-3 lines of JSX.
*/
import { useTranslation } from "@/i18n/provider.js";
import { useCityName } from "@/shared/hooks/useDictionaries.js";
import { RequestInfo } from "./RequestInfo.js";
import type { PopularRequest } from "../types.js";
export interface PopularRequestItemProps {
request: PopularRequest;
onClick: (request: PopularRequest) => void;
}
export function PopularRequestItem({
request,
onClick,
}: PopularRequestItemProps): JSX.Element {
const { t } = useTranslation();
const handleClick = () => {
onClick(request);
};
switch (request.mode) {
case "Arrival": {
return (
<ArrivalDisplay
label={t("BOARD.ARRIVAL")}
cityCode={request.arrival}
onClick={handleClick}
/>
);
}
case "Departure": {
return (
<DepartureDisplay
label={t("BOARD.DEPARTURE")}
cityCode={request.departure}
onClick={handleClick}
/>
);
}
case "FlightNumber": {
const flightInfo = `${request.carrier}\u00a0${request.flightNumber}`;
return (
<div className="popular-request">
{t("BOARD.FLIGHT_NUMBER")}:{" "}
<RequestInfo onClick={handleClick}>{flightInfo}</RequestInfo>
</div>
);
}
case "Route":
case "RouteWithBack": {
const label = getRouteLabel(request.mode, request.type, t);
return (
<RouteDisplay
label={label}
departureCode={request.departure}
arrivalCode={request.arrival}
onClick={handleClick}
/>
);
}
}
}
// ---------------------------------------------------------------------------
// Internal sub-components
// ---------------------------------------------------------------------------
function ArrivalDisplay({
label,
cityCode,
onClick,
}: {
label: string;
cityCode: string;
onClick: () => void;
}): JSX.Element {
const cityName = useCityName(cityCode);
return (
<div className="popular-request">
{label}: <RequestInfo onClick={onClick}>{cityName}</RequestInfo>
</div>
);
}
function DepartureDisplay({
label,
cityCode,
onClick,
}: {
label: string;
cityCode: string;
onClick: () => void;
}): JSX.Element {
const cityName = useCityName(cityCode);
return (
<div className="popular-request">
{label}: <RequestInfo onClick={onClick}>{cityName}</RequestInfo>
</div>
);
}
function RouteDisplay({
label,
departureCode,
arrivalCode,
onClick,
}: {
label: string;
departureCode: string;
arrivalCode: string;
onClick: () => void;
}): JSX.Element {
const departureName = useCityName(departureCode);
const arrivalName = useCityName(arrivalCode);
return (
<div className="popular-request">
{label}:{" "}
<RequestInfo onClick={onClick}>
{departureName} - {arrivalName}
</RequestInfo>
</div>
);
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function getRouteLabel(
mode: "Route" | "RouteWithBack",
type: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
t: (key: string) => any,
): string {
if (mode === "RouteWithBack") {
return t("SCHEDULE.SCHEDULE-FULL-ROUTE") as string;
}
return type === "Onlineboard"
? (t("BOARD.ROUTE") as string)
: (t("SCHEDULE.SCHEDULE-OUTBOUND") as string);
}
@@ -0,0 +1,56 @@
/**
* Container component for the Popular Requests panel.
*
* Angular equivalent: `PopularRequestsComponent` (popular-requests.component.ts)
* — fetches popular requests on mount, renders up to 4 items in a 2-column
* grid, and handles click navigation.
*
* This component is embedded in start pages (OnlineBoard, Schedule),
* not rendered as a standalone route.
*/
import { useTranslation } from "@/i18n/provider.js";
import { usePopularRequests } from "../hooks/usePopularRequests.js";
import { PopularRequestItem } from "./PopularRequestItem.js";
import type { PopularRequest } from "../types.js";
export interface PopularRequestsPanelProps {
/** Callback invoked when a user clicks a popular request. The host page
* handles navigation based on the request mode and type. */
onRequestClick: (request: PopularRequest) => void;
}
/**
* Renders the "Popular sections" panel with up to 4 popular request items.
* Shows nothing while loading, nothing on error (graceful degradation).
*/
export function PopularRequestsPanel({
onRequestClick,
}: PopularRequestsPanelProps): JSX.Element | null {
const { t } = useTranslation();
const { requests, loading, error } = usePopularRequests();
// Gracefully degrade: don't render anything on loading or error
if (loading || error || requests.length === 0) {
return null;
}
// Angular renders exactly 4 items (requests[0]..requests[3])
const visibleRequests = requests.slice(0, 4);
return (
<div className="popular-requests">
<h3 className="popular-requests__title">
{t("BOARD.POPULAR-CHAPTERS")}
</h3>
{visibleRequests.map((request, index) => (
<div key={index} className="popular-requests__item">
<PopularRequestItem
request={request}
onClick={onRequestClick}
/>
</div>
))}
</div>
);
}
@@ -0,0 +1,36 @@
/**
* Styled clickable text wrapper for popular request items.
*
* Angular equivalent: `RequestInfoComponent` (request-info.component.ts)
* — a simple styled `<span>` with blue link color and pointer cursor.
*/
import type { ReactNode, MouseEvent } from "react";
export interface RequestInfoProps {
children: ReactNode;
onClick: (e: MouseEvent<HTMLSpanElement>) => void;
}
export function RequestInfo({ children, onClick }: RequestInfoProps): JSX.Element {
return (
<span
className="popular-request__link"
role="button"
tabIndex={0}
onClick={onClick}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick(e as unknown as MouseEvent<HTMLSpanElement>);
}
}}
style={{
color: "var(--color-blue-link, #0645ad)",
cursor: "pointer",
}}
>
{children}
</span>
);
}
+45 -3
View File
@@ -1,17 +1,59 @@
import { useCallback } from "react";
import { useNavigate, useParams } from "@modern-js/runtime/router";
import type { HostContract } from "@/host-contract";
import { PopularRequestsPanel } from "@/features/popular-requests/components/PopularRequestsPanel.js";
import type { PopularRequest } from "@/features/popular-requests/types.js";
/**
* MF expose wrapper for the Popular Requests feature.
* Phase 5 (popular-requests port) replaces the body with the real root.
*
* Renders the PopularRequestsPanel and handles navigation on click
* by routing to the appropriate onlineboard/schedule page within the
* host application.
*/
export interface PopularRequestsRemoteProps {
hostContract: HostContract;
}
export default function PopularRequestsRemote(_props: PopularRequestsRemoteProps): JSX.Element {
export default function PopularRequestsRemote({
hostContract,
}: PopularRequestsRemoteProps): JSX.Element {
const navigate = useNavigate();
const params = useParams<{ lang: string }>();
const lang = params.lang ?? hostContract.locale;
const handleRequestClick = useCallback(
(request: PopularRequest) => {
const nav = hostContract.navigate ?? ((path: string) => void navigate(path));
switch (request.mode) {
case "FlightNumber":
nav(`/${lang}/onlineboard`);
return;
case "Arrival":
nav(`/${lang}/onlineboard`);
return;
case "Departure":
nav(`/${lang}/onlineboard`);
return;
case "Route":
if (request.type === "Onlineboard") {
nav(`/${lang}/onlineboard`);
} else {
nav(`/${lang}/schedule`);
}
return;
case "RouteWithBack":
nav(`/${lang}/schedule`);
return;
}
},
[hostContract.navigate, navigate, lang],
);
return (
<div data-mf-expose="PopularRequests">
<p>Popular Requests remote stub. Populated in Phase 5.</p>
<PopularRequestsPanel onRequestClick={handleRequestClick} />
</div>
);
}
+55
View File
@@ -0,0 +1,55 @@
/**
* Popular Requests standalone page route.
*
* Provides a standalone view of the PopularRequestsPanel. In Angular,
* popular requests were embedded in OnlineBoard and Schedule start pages.
* This route provides a direct-access path for the MF remote and allows
* independent rendering/testing.
*
* URL: /{lang}/popular
*/
import { lazy, Suspense, useCallback } from "react";
import { useParams, useNavigate } from "@modern-js/runtime/router";
import type { PopularRequest } from "@/features/popular-requests/types.js";
const PopularRequestsPanel = lazy(() =>
import("@/features/popular-requests/components/PopularRequestsPanel.js").then(
(m) => ({ default: m.PopularRequestsPanel }),
),
);
export default function PopularPage(): JSX.Element {
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
const navigate = useNavigate();
const handleRequestClick = useCallback(
(request: PopularRequest) => {
switch (request.mode) {
case "FlightNumber":
case "Arrival":
case "Departure":
void navigate(`/${lang}/onlineboard`);
return;
case "Route":
if (request.type === "Onlineboard") {
void navigate(`/${lang}/onlineboard`);
} else {
void navigate(`/${lang}/schedule`);
}
return;
case "RouteWithBack":
void navigate(`/${lang}/schedule`);
return;
}
},
[lang, navigate],
);
return (
<Suspense fallback={<div aria-busy="true">Loading...</div>}>
<PopularRequestsPanel onRequestClick={handleRequestClick} />
</Suspense>
);
}