plan/react-rewrite #1
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user