diff --git a/src/features/schedule/ScheduleDetailsCatchAllRoute.tsx b/src/features/schedule/ScheduleDetailsCatchAllRoute.tsx new file mode 100644 index 00000000..b7f82752 --- /dev/null +++ b/src/features/schedule/ScheduleDetailsCatchAllRoute.tsx @@ -0,0 +1,84 @@ +/** + * Schedule flight-details catch-all route. + * + * This module backs the framework-mandated `src/routes/[lang]/schedule/$.tsx` + * splat file. The `$.tsx` filename is how Modern.js (via react-router) marks + * a route that captures every trailing segment, so we keep it as a thin + * re-export and put the real code under a readable path. + * + * Captures variable-length paths for multi-flight chains, e.g. + * /{lang}/schedule/{flight1-date}[/{flight2-date}]... + * Also handles the airport-code detail variant: + * /{lang}/schedule/{depCode}/{flight-date}/{arrCode}/... + */ + +import { lazy, Suspense } from "react"; +import { useParams } from "@modern-js/runtime/router"; +import { useTranslation } from "@/i18n/provider.js"; +import { parseFlightUrlParams } from "@/features/online-board/url.js"; +import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js"; +import { getEnv } from "@/env/index.js"; +import type { IScheduleFlightId } from "./types.js"; + +const ScheduleDetailsPage = lazy(() => + import("./components/ScheduleDetailsPage.js").then( + (m) => ({ default: m.ScheduleDetailsPage }), + ), +); + +/** + * Parse the catch-all segments into flight IDs. + * Segments that are 3 chars or less are treated as airport codes and skipped. + */ +function parseFlightSegments(segments: string[]): IScheduleFlightId[] { + const flights: IScheduleFlightId[] = []; + + for (const segment of segments) { + if (segment.length <= 3) continue; + + const parsed = parseFlightUrlParams(segment); + if (parsed) { + const flight: IScheduleFlightId = { + carrier: parsed.carrier, + flightNumber: parsed.flightNumber, + date: parsed.date, + }; + if (parsed.suffix !== undefined) { + flight.suffix = parsed.suffix; + } + flights.push(flight); + } + } + + return flights; +} + +export default function ScheduleDetailsCatchAllRoute(): JSX.Element { + const { t } = useTranslation(); + const routeParams = useParams<{ "*": string; lang: string }>(); + const locale = routeParams.lang ?? "ru"; + const canonicalOrigin = getEnv().PROD_ORIGIN; + + // Modern.js splat route ($.tsx) provides the remaining path via "*" param. + const rawFlights = routeParams["*"] ?? ""; + const segments = rawFlights.split("/").filter(Boolean); + const flights = parseFlightSegments(segments); + + if (flights.length === 0) { + return ( +
+

{t("SHARED.INVALID-PARAMS")}

+
+ ); + } + + return ( + }> + + + ); +} diff --git a/src/routes/[lang]/schedule/$.tsx b/src/routes/[lang]/schedule/$.tsx index e0506e8e..1e7480b3 100644 --- a/src/routes/[lang]/schedule/$.tsx +++ b/src/routes/[lang]/schedule/$.tsx @@ -1,80 +1,12 @@ /** - * Schedule flight details catch-all route. + * Modern.js/react-router splat file. * - * Captures variable-length paths for multi-flight chains. - * URL: /{lang}/schedule/{flight1-date}[/{flight2-date}]... - * Also handles: /{lang}/schedule/{depCode}/{flight-date}/{arrCode}/... - * - * Modern.js catch-all route: [...flights] captures all remaining segments. + * The `$.tsx` filename is a framework convention that marks a catch-all + * route — it captures every trailing path segment. The name is mandated + * by the router, but cryptic at a glance; the real component lives at + * src/features/schedule/ScheduleDetailsCatchAllRoute.tsx and this file + * is intentionally a thin re-export so the readable name appears in + * imports, search results and stack traces. */ -import { lazy, Suspense } from "react"; -import { useParams } from "@modern-js/runtime/router"; -import { useTranslation } from "@/i18n/provider.js"; -import { parseFlightUrlParams } from "@/features/online-board/url.js"; -import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js"; -import { getEnv } from "@/env/index.js"; -import type { IScheduleFlightId } from "@/features/schedule/types.js"; - -const ScheduleDetailsPage = lazy(() => - import("@/features/schedule/components/ScheduleDetailsPage.js").then( - (m) => ({ default: m.ScheduleDetailsPage }), - ), -); - -/** - * Parse the catch-all segments into flight IDs. - * Segments that are 3 chars or less are treated as airport codes and skipped. - */ -function parseFlightSegments(segments: string[]): IScheduleFlightId[] { - const flights: IScheduleFlightId[] = []; - - for (const segment of segments) { - if (segment.length <= 3) continue; - - const parsed = parseFlightUrlParams(segment); - if (parsed) { - const flight: IScheduleFlightId = { - carrier: parsed.carrier, - flightNumber: parsed.flightNumber, - date: parsed.date, - }; - if (parsed.suffix !== undefined) { - flight.suffix = parsed.suffix; - } - flights.push(flight); - } - } - - return flights; -} - -export default function ScheduleDetailsRoute(): JSX.Element { - const { t } = useTranslation(); - const routeParams = useParams<{ "*": string; lang: string }>(); - const locale = routeParams.lang ?? "ru"; - const canonicalOrigin = getEnv().PROD_ORIGIN; - - // Modern.js $.tsx splat route provides the remaining path via "*" param. - const rawFlights = routeParams["*"] ?? ""; - const segments = rawFlights.split("/").filter(Boolean); - const flights = parseFlightSegments(segments); - - if (flights.length === 0) { - return ( -
-

{t("SHARED.INVALID-PARAMS")}

-
- ); - } - - return ( - }> - - - ); -} +export { default } from "@/features/schedule/ScheduleDetailsCatchAllRoute.js";