8d409572b7
- ErrorPage.tsx: FALLBACK_CONFIG literal instead of ERROR_CONFIG["500"]! - ErrorBoundary.tsx: hoist FALLBACK_RU / FALLBACK_EN to consts so pickStrings returns them without the bang. - routesToPolylines.ts: narrow spider-mode block on filterState.departure truthy; guard each route-code lookup. - FlightsMapStartPage.tsx: narrow firstRoute/depCode/arrCode together instead of asserting each individually. - OnlineBoardDetailsPage.tsx: IIFE over legs[i+1] for TransferBar; `_canonicalOrigin` prefix for currently-unused prop. Warning count: 30 → 19.
168 lines
5.2 KiB
TypeScript
168 lines
5.2 KiB
TypeScript
/**
|
|
* 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 = {
|
|
titleKey: "PAGE500.HEADER",
|
|
descriptionKey: "PAGE500.DESCRIPTION",
|
|
image: "/assets/img/lady500.png",
|
|
buyTicketKey: "PAGE500.BUY-TICKET",
|
|
homeKey: "PAGE500.TO-HOME",
|
|
supportKey: "PAGE500.SUPPORT",
|
|
};
|
|
|
|
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>
|
|
);
|
|
}
|