Render branded 404 page on invalid URLs and malformed params
Replace the inline 'Invalid parameters' fallbacks and the framework's default '404' text with the existing Aeroflot 404 screen. Unknown locale, malformed flight/route/station params, and unmatched URLs (including bad paths like onlineboard//route/...) now all land on the same ErrorPage component.
This commit is contained in:
@@ -17,6 +17,7 @@ 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 { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
import type { IScheduleFlightId } from "./types.js";
|
||||
|
||||
@@ -64,13 +65,7 @@ export default function ScheduleDetailsCatchAllRoute(): JSX.Element {
|
||||
const segments = rawFlights.split("/").filter(Boolean);
|
||||
const flights = parseFlightSegments(segments);
|
||||
|
||||
if (flights.length === 0) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (flights.length === 0) return <ErrorPage code="404" />;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<FlightListSkeleton count={flights.length} />}>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Global catch-all (splat) route.
|
||||
*
|
||||
* Modern.js/react-router resolves any URL that doesn't match a more
|
||||
* specific route to this file. We render the branded 404 page so
|
||||
* mistyped deep links like `/onlineboard//route/...` (double slash) or
|
||||
* `/does-not-exist` land on the same "Страница не найдена" screen as
|
||||
* the explicit `/error/404` route.
|
||||
*/
|
||||
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
|
||||
export default function NotFoundRoute(): JSX.Element {
|
||||
return <ErrorPage code="404" />;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Locale-scoped catch-all (splat) route.
|
||||
*
|
||||
* Sibling of the global `/src/routes/$.tsx` splat, scoped to any path
|
||||
* under `/{lang}/...` that doesn't match a specific feature route.
|
||||
* Required so `/ru-ru/onlineboard//route/...`-style bad URLs render the
|
||||
* branded 404 instead of dropping out to the framework's default
|
||||
* error overlay.
|
||||
*/
|
||||
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
|
||||
export default function LangNotFoundRoute(): JSX.Element {
|
||||
return <ErrorPage code="404" />;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { createI18nInstance } from "@/i18n/config";
|
||||
import { I18nProvider } from "@/i18n/provider";
|
||||
import { useApiClient } from "@/shared/api/provider";
|
||||
import { registerPrimeLocales, primeLocaleNameFor } from "@/i18n/primeLocales";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import type i18next from "i18next";
|
||||
|
||||
// Register all PrimeReact locales once at module load. The active
|
||||
@@ -60,13 +61,10 @@ export default function LangLayout(): JSX.Element {
|
||||
};
|
||||
}, [locale, language, apiClient]);
|
||||
|
||||
// Unknown locale → branded 404 instead of the framework's default
|
||||
// "404" text; matches the `/error/404` route rendering.
|
||||
if (!locale) {
|
||||
return (
|
||||
<div>
|
||||
<h2>404 — Unknown locale: {rawLang}</h2>
|
||||
<p>Supported: ru-ru, en-en, es-es, fr-fr, it-it, ja-ja, ko-ko, zh-zh, de-de</p>
|
||||
</div>
|
||||
);
|
||||
return <ErrorPage code="404" />;
|
||||
}
|
||||
|
||||
if (!i18n) {
|
||||
|
||||
@@ -12,6 +12,7 @@ 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 { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { buildFlightDetailsSeoFromId } from "@/features/online-board/seo.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
@@ -29,13 +30,7 @@ export default function FlightDetailsPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseFlightUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
// Render SeoHead OUTSIDE the lazy Suspense boundary so SSR gets the
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseStationUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildArrivalSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -27,13 +28,7 @@ export default function ArrivalSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseStationUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseStationUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildDepartureSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -27,13 +28,7 @@ export default function DepartureSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseStationUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseFlightUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildFlightSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -27,13 +28,7 @@ export default function FlightSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseFlightUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = parsed.suffix
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseRouteUrlParams } from "@/features/online-board/url.js";
|
||||
import { buildRouteSearchSeo } from "@/features/online-board/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const OnlineBoardSearchPage = lazy(() =>
|
||||
@@ -27,13 +28,7 @@ export default function RouteSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseRouteUrlParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const searchParams = {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseScheduleRouteParams } from "@/features/schedule/url.js";
|
||||
import { buildScheduleSearchSeo } from "@/features/schedule/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const ScheduleSearchPage = lazy(() =>
|
||||
@@ -30,13 +31,7 @@ export default function ScheduleRoundTripSearchPage(): JSX.Element {
|
||||
const outbound = parseScheduleRouteParams(outboundRaw);
|
||||
const inbound = parseScheduleRouteParams(inboundRaw);
|
||||
|
||||
if (!outbound || !inbound) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!outbound || !inbound) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const scheduleParams = { type: "roundtrip" as const, outbound, inbound };
|
||||
|
||||
@@ -12,6 +12,7 @@ import { parseScheduleRouteParams } from "@/features/schedule/url.js";
|
||||
import { buildScheduleSearchSeo } from "@/features/schedule/seo.js";
|
||||
import { SeoHead } from "@/ui/seo/SeoHead.js";
|
||||
import { FlightListSkeleton } from "@/ui/flights/FlightListSkeleton.js";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
import { getEnv } from "@/env/index.js";
|
||||
|
||||
const ScheduleSearchPage = lazy(() =>
|
||||
@@ -27,13 +28,7 @@ export default function ScheduleRouteSearchPage(): JSX.Element {
|
||||
const locale = routeParams.lang ?? "ru-ru";
|
||||
const parsed = parseScheduleRouteParams(raw);
|
||||
|
||||
if (!parsed) {
|
||||
return (
|
||||
<div data-testid="invalid-params">
|
||||
<p>{t("SHARED.INVALID-PARAMS")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (!parsed) return <ErrorPage code="404" />;
|
||||
|
||||
const canonicalOrigin = getEnv().PROD_ORIGIN;
|
||||
const scheduleParams = { type: "route" as const, outbound: parsed };
|
||||
|
||||
@@ -1,147 +1,7 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useParams } from "@modern-js/runtime/router";
|
||||
import { createI18nInstance } from "@/i18n/config.js";
|
||||
import { resolveLocaleFromPath, localeToLanguage, type Language } from "@/i18n/resolver.js";
|
||||
import "./page.scss";
|
||||
import { ErrorPage } from "@/ui/errors/ErrorPage.js";
|
||||
|
||||
interface ErrorConfig {
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
image: string;
|
||||
buyTicketKey: string;
|
||||
homeKey: string;
|
||||
supportKey: string;
|
||||
}
|
||||
|
||||
const ERROR_CONFIG: Record<string, ErrorConfig> = {
|
||||
"404": {
|
||||
titleKey: "PAGE404.HEADER",
|
||||
descriptionKey: "PAGE404.DESCRIPTION",
|
||||
image: "/assets/img/lady404.png",
|
||||
buyTicketKey: "PAGE404.BUY-TICKET",
|
||||
homeKey: "PAGE404.TO-HOME",
|
||||
supportKey: "PAGE404.SUPPORT",
|
||||
},
|
||||
"500": {
|
||||
titleKey: "PAGE500.HEADER",
|
||||
descriptionKey: "PAGE500.DESCRIPTION",
|
||||
image: "/assets/img/lady500.png",
|
||||
buyTicketKey: "PAGE500.BUY-TICKET",
|
||||
homeKey: "PAGE500.TO-HOME",
|
||||
supportKey: "PAGE500.SUPPORT",
|
||||
},
|
||||
"503": {
|
||||
titleKey: "PAGE500.HEADER",
|
||||
descriptionKey: "PAGE500.DESCRIPTION",
|
||||
image: "/assets/img/lady500.png",
|
||||
buyTicketKey: "PAGE500.BUY-TICKET",
|
||||
homeKey: "PAGE500.TO-HOME",
|
||||
supportKey: "PAGE500.SUPPORT",
|
||||
},
|
||||
};
|
||||
|
||||
const FALLBACK_CONFIG: ErrorConfig = {
|
||||
titleKey: "PAGE500.HEADER",
|
||||
descriptionKey: "PAGE500.DESCRIPTION",
|
||||
image: "/assets/img/lady500.png",
|
||||
buyTicketKey: "PAGE500.BUY-TICKET",
|
||||
homeKey: "PAGE500.TO-HOME",
|
||||
supportKey: "PAGE500.SUPPORT",
|
||||
};
|
||||
|
||||
export default function ErrorPage(): JSX.Element {
|
||||
export default function ErrorRoute(): JSX.Element {
|
||||
const { code } = useParams<{ code: string }>();
|
||||
const config = (code ? ERROR_CONFIG[code] : undefined) ?? FALLBACK_CONFIG;
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Attempt to detect locale from referrer or default to "ru"
|
||||
const [translations, setTranslations] = useState<Record<string, string> | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const pathname = typeof window !== "undefined" ? window.location.pathname : "";
|
||||
const detected = resolveLocaleFromPath(pathname);
|
||||
// Error pages run outside the [lang]/layout, so derive the short
|
||||
// language for i18n file loading from whatever the URL resolves to.
|
||||
const locale: Language = detected ? localeToLanguage(detected) : "ru";
|
||||
|
||||
void createI18nInstance({ locale }).then((i18n) => {
|
||||
const t = (key: string) => i18n.t(key) as string;
|
||||
setTranslations({
|
||||
title: t(config.titleKey),
|
||||
description: t(config.descriptionKey),
|
||||
buyTicket: t(config.buyTicketKey),
|
||||
home: t(config.homeKey),
|
||||
support: t(config.supportKey),
|
||||
});
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchTerm) {
|
||||
window.open(
|
||||
`https://www.aeroflot.ru/search?text=${encodeURIComponent(searchTerm)}`,
|
||||
"_blank",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Show content immediately (with hardcoded Russian fallback for SSR),
|
||||
// then replace with i18n translations on the client
|
||||
const title = translations?.title ?? config.titleKey;
|
||||
const description = translations?.description ?? config.descriptionKey;
|
||||
const buyTicket = translations?.buyTicket ?? "Купить билет";
|
||||
const home = translations?.home ?? "На главную";
|
||||
const support = translations?.support ?? "Поддержка";
|
||||
|
||||
return (
|
||||
<div className="error-page">
|
||||
<section className="error-page__section frame">
|
||||
<div
|
||||
className={`error-page__illustration errorCode-${code ?? "500"}`}
|
||||
style={{ backgroundImage: `url('${config.image}')` }}
|
||||
/>
|
||||
<div className="error-page__content">
|
||||
<div className="error-page__code">{code ?? "?"}</div>
|
||||
<div className="error-page__title">{title}</div>
|
||||
<div className="error-page__description">{description}</div>
|
||||
<div className="error-page__search">
|
||||
<div className="error-page__search-control">
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="search"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
/>
|
||||
<div
|
||||
className="error-page__search-icon"
|
||||
onClick={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error-page__actions">
|
||||
<a
|
||||
className="error-page__btn error-page__btn--primary"
|
||||
href={`https://www.aeroflot.ru/booking?from=${code ?? "500"}`}
|
||||
>
|
||||
{buyTicket}
|
||||
</a>
|
||||
<a
|
||||
className="error-page__btn error-page__btn--secondary"
|
||||
href="/"
|
||||
>
|
||||
{home}
|
||||
</a>
|
||||
<a
|
||||
className="error-page__btn error-page__btn--link"
|
||||
href={`https://www.aeroflot.ru/help?from=${code ?? "500"}`}
|
||||
>
|
||||
{support}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
return <ErrorPage {...(code ? { code } : {})} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use "../../../styles/variables" as vars;
|
||||
@use "../../../styles/colors" as colors;
|
||||
@use "../../../styles/screen" as screen;
|
||||
@use "../../styles/variables" as vars;
|
||||
@use "../../styles/colors" as colors;
|
||||
@use "../../styles/screen" as screen;
|
||||
|
||||
.error-page {
|
||||
display: block;
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Reusable error-page component.
|
||||
*
|
||||
* Rendered by the `/error/[code]` route AND directly inline by any
|
||||
* feature route that has to reject an invalid URL (bad flight number,
|
||||
* missing date, double-slash path, etc.). Matches Angular's global
|
||||
* 404/500 pages: illustration + code + title + description + search
|
||||
* box + three action buttons.
|
||||
*/
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type JSX,
|
||||
} from "react";
|
||||
import { createI18nInstance } from "@/i18n/config.js";
|
||||
import {
|
||||
resolveLocaleFromPath,
|
||||
localeToLanguage,
|
||||
type Language,
|
||||
} from "@/i18n/resolver.js";
|
||||
import "./ErrorPage.scss";
|
||||
|
||||
interface ErrorConfig {
|
||||
titleKey: string;
|
||||
descriptionKey: string;
|
||||
image: string;
|
||||
buyTicketKey: string;
|
||||
homeKey: string;
|
||||
supportKey: string;
|
||||
}
|
||||
|
||||
const ERROR_CONFIG: Record<string, ErrorConfig> = {
|
||||
"404": {
|
||||
titleKey: "PAGE404.HEADER",
|
||||
descriptionKey: "PAGE404.DESCRIPTION",
|
||||
image: "/assets/img/lady404.png",
|
||||
buyTicketKey: "PAGE404.BUY-TICKET",
|
||||
homeKey: "PAGE404.TO-HOME",
|
||||
supportKey: "PAGE404.SUPPORT",
|
||||
},
|
||||
"500": {
|
||||
titleKey: "PAGE500.HEADER",
|
||||
descriptionKey: "PAGE500.DESCRIPTION",
|
||||
image: "/assets/img/lady500.png",
|
||||
buyTicketKey: "PAGE500.BUY-TICKET",
|
||||
homeKey: "PAGE500.TO-HOME",
|
||||
supportKey: "PAGE500.SUPPORT",
|
||||
},
|
||||
"503": {
|
||||
titleKey: "PAGE500.HEADER",
|
||||
descriptionKey: "PAGE500.DESCRIPTION",
|
||||
image: "/assets/img/lady500.png",
|
||||
buyTicketKey: "PAGE500.BUY-TICKET",
|
||||
homeKey: "PAGE500.TO-HOME",
|
||||
supportKey: "PAGE500.SUPPORT",
|
||||
},
|
||||
};
|
||||
|
||||
const FALLBACK_CONFIG: ErrorConfig = ERROR_CONFIG["500"]!;
|
||||
|
||||
export interface ErrorPageProps {
|
||||
/** HTTP status code ("404", "500", "503"). Unknown codes fall back to 500. */
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export function ErrorPage({ code }: ErrorPageProps): JSX.Element {
|
||||
const config = (code ? ERROR_CONFIG[code] : undefined) ?? FALLBACK_CONFIG;
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [translations, setTranslations] = useState<Record<string, string> | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const pathname = typeof window !== "undefined" ? window.location.pathname : "";
|
||||
const detected = resolveLocaleFromPath(pathname);
|
||||
const locale: Language = detected ? localeToLanguage(detected) : "ru";
|
||||
|
||||
void createI18nInstance({ locale }).then((i18n) => {
|
||||
const t = (key: string): string => i18n.t(key) as string;
|
||||
setTranslations({
|
||||
title: t(config.titleKey),
|
||||
description: t(config.descriptionKey),
|
||||
buyTicket: t(config.buyTicketKey),
|
||||
home: t(config.homeKey),
|
||||
support: t(config.supportKey),
|
||||
});
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
const handleSearch = (): void => {
|
||||
if (searchTerm) {
|
||||
window.open(
|
||||
`https://www.aeroflot.ru/search?text=${encodeURIComponent(searchTerm)}`,
|
||||
"_blank",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Show content immediately (with hardcoded Russian fallback for SSR),
|
||||
// then replace with i18n translations on the client.
|
||||
const title = translations?.title ?? config.titleKey;
|
||||
const description = translations?.description ?? config.descriptionKey;
|
||||
const buyTicket = translations?.buyTicket ?? "Купить билет";
|
||||
const home = translations?.home ?? "На главную";
|
||||
const support = translations?.support ?? "Поддержка";
|
||||
const displayCode = code ?? "?";
|
||||
|
||||
return (
|
||||
<div className="error-page" data-testid={`error-page-${code ?? "unknown"}`}>
|
||||
<section className="error-page__section frame">
|
||||
<div
|
||||
className={`error-page__illustration errorCode-${code ?? "500"}`}
|
||||
style={{ backgroundImage: `url('${config.image}')` }}
|
||||
/>
|
||||
<div className="error-page__content">
|
||||
<div className="error-page__code">{displayCode}</div>
|
||||
<div className="error-page__title">{title}</div>
|
||||
<div className="error-page__description">{description}</div>
|
||||
<div className="error-page__search">
|
||||
<div className="error-page__search-control">
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="search"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
||||
/>
|
||||
<div
|
||||
className="error-page__search-icon"
|
||||
onClick={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="error-page__actions">
|
||||
<a
|
||||
className="error-page__btn error-page__btn--primary"
|
||||
href={`https://www.aeroflot.ru/booking?from=${code ?? "500"}`}
|
||||
>
|
||||
{buyTicket}
|
||||
</a>
|
||||
<a
|
||||
className="error-page__btn error-page__btn--secondary"
|
||||
href="/"
|
||||
>
|
||||
{home}
|
||||
</a>
|
||||
<a
|
||||
className="error-page__btn error-page__btn--link"
|
||||
href={`https://www.aeroflot.ru/help?from=${code ?? "500"}`}
|
||||
>
|
||||
{support}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user