Files
flights_web/src/ui/errors/ErrorPage.tsx
T
gnezim 8d409572b7 Drop 11 more non-null assertions across 5 files
- 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.
2026-04-20 09:22:49 +03:00

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>
);
}