diff --git a/src/features/popular-requests/components/PopularRequestItem.tsx b/src/features/popular-requests/components/PopularRequestItem.tsx new file mode 100644 index 00000000..14fa86df --- /dev/null +++ b/src/features/popular-requests/components/PopularRequestItem.tsx @@ -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 ( + + ); + } + case "Departure": { + return ( + + ); + } + case "FlightNumber": { + const flightInfo = `${request.carrier}\u00a0${request.flightNumber}`; + return ( +
+ {t("BOARD.FLIGHT_NUMBER")}:{" "} + {flightInfo} +
+ ); + } + case "Route": + case "RouteWithBack": { + const label = getRouteLabel(request.mode, request.type, t); + return ( + + ); + } + } +} + +// --------------------------------------------------------------------------- +// Internal sub-components +// --------------------------------------------------------------------------- + +function ArrivalDisplay({ + label, + cityCode, + onClick, +}: { + label: string; + cityCode: string; + onClick: () => void; +}): JSX.Element { + const cityName = useCityName(cityCode); + return ( +
+ {label}: {cityName} +
+ ); +} + +function DepartureDisplay({ + label, + cityCode, + onClick, +}: { + label: string; + cityCode: string; + onClick: () => void; +}): JSX.Element { + const cityName = useCityName(cityCode); + return ( +
+ {label}: {cityName} +
+ ); +} + +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 ( +
+ {label}:{" "} + + {departureName} - {arrivalName} + +
+ ); +} + +// --------------------------------------------------------------------------- +// 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); +} diff --git a/src/features/popular-requests/components/PopularRequestsPanel.tsx b/src/features/popular-requests/components/PopularRequestsPanel.tsx new file mode 100644 index 00000000..d87c5306 --- /dev/null +++ b/src/features/popular-requests/components/PopularRequestsPanel.tsx @@ -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 ( +
+

+ {t("BOARD.POPULAR-CHAPTERS")} +

+ {visibleRequests.map((request, index) => ( +
+ +
+ ))} +
+ ); +} diff --git a/src/features/popular-requests/components/RequestInfo.tsx b/src/features/popular-requests/components/RequestInfo.tsx new file mode 100644 index 00000000..5235b683 --- /dev/null +++ b/src/features/popular-requests/components/RequestInfo.tsx @@ -0,0 +1,36 @@ +/** + * Styled clickable text wrapper for popular request items. + * + * Angular equivalent: `RequestInfoComponent` (request-info.component.ts) + * — a simple styled `` with blue link color and pointer cursor. + */ + +import type { ReactNode, MouseEvent } from "react"; + +export interface RequestInfoProps { + children: ReactNode; + onClick: (e: MouseEvent) => void; +} + +export function RequestInfo({ children, onClick }: RequestInfoProps): JSX.Element { + return ( + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick(e as unknown as MouseEvent); + } + }} + style={{ + color: "var(--color-blue-link, #0645ad)", + cursor: "pointer", + }} + > + {children} + + ); +} diff --git a/src/mf/expose/PopularRequests.tsx b/src/mf/expose/PopularRequests.tsx index 3d1cf9b1..3dd8f42a 100644 --- a/src/mf/expose/PopularRequests.tsx +++ b/src/mf/expose/PopularRequests.tsx @@ -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 (
-

Popular Requests remote — stub. Populated in Phase 5.

+
); } diff --git a/src/routes/[lang]/popular/page.tsx b/src/routes/[lang]/popular/page.tsx new file mode 100644 index 00000000..1b8e3ad6 --- /dev/null +++ b/src/routes/[lang]/popular/page.tsx @@ -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 ( + Loading...}> + + + ); +}