Match Angular pixel-for-pixel: error page, filter default, breadcrumbs, feedback button

Error page: add search input bar, align flex/spacing to Angular SCSS mixins,
match button display and illustration flex.

Online board filter: default to "route" tab expanded (Angular defaults to
route, not flight number).

Start pages: remove extra breadcrumb items — Angular start pages show only
"Главная", not the page title.

PageLayout: hide FeedbackButton — Angular gates it behind
FEEDBACK_BUTTON_AVAILABLE feature flag (off by default).
This commit is contained in:
2026-04-16 09:03:00 +03:00
parent bb0353bb40
commit 46f6f3ef86
6 changed files with 112 additions and 25 deletions
@@ -44,7 +44,7 @@ export const OnlineBoardFilter: FC = () => {
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
const [activeTab, setActiveTab] = useState<AccordionTab>("flight");
const [activeTab, setActiveTab] = useState<AccordionTab>("route");
// Flight number fields
const [flightNumber, setFlightNumber] = useState("");
@@ -41,9 +41,7 @@ export const OnlineBoardStartPage: FC = () => {
{t("BOARD.TITLE")}
</h1>
}
breadcrumbs={[
{ label: t("BOARD.TITLE") },
]}
breadcrumbs={[]}
contentLeft={
<>
<OnlineBoardFilter />
@@ -295,9 +295,7 @@ export const ScheduleStartPage: FC = () => {
{t("SCHEDULE.TITLE")}
</h1>
}
breadcrumbs={[
{ label: t("SCHEDULE.TITLE"), url: `/${lang}/schedule` },
]}
breadcrumbs={[]}
contentLeft={
<>
{scheduleFilter}
+81 -16
View File
@@ -12,8 +12,10 @@
display: flex;
padding: 2rem;
& > * + * {
margin-left: 2rem;
// Match Angular: v-spacing(2rem) + h-spacing(2rem)
& > *:not(:last-child) {
margin-right: 2rem;
margin-bottom: 2rem;
}
@include screen.desktop {
@@ -25,16 +27,16 @@
@include screen.mobile {
flex-direction: column;
& > * + * {
margin-left: 0;
margin-top: 2rem;
& > *:not(:last-child) {
margin-right: 0;
margin-bottom: 2rem;
}
}
}
&__illustration {
flex: 0 0 30%;
min-height: 300px;
// Match Angular: flex: 30% (equivalent to flex: 0 1 30%)
flex: 30%;
background-position: bottom;
background-repeat: no-repeat;
background-size: contain;
@@ -48,21 +50,19 @@
&__code {
font-size: 8rem;
color: #a3a3a3;
line-height: 1;
margin: 0;
}
&__title {
font-size: 3rem;
color: colors.$text-color;
margin: 0;
}
&__content {
flex: 1 1 70%;
// Match Angular: flex: 70% + v-spacing(1rem)
flex: 70%;
& > * + * {
margin-top: 1rem;
& > *:not(:last-child) {
margin-bottom: 1rem;
}
@include screen.mobile {
@@ -76,20 +76,85 @@
line-height: 1.5;
}
// Search input — matches Angular error-page-search
&__search {
width: 100%;
@include screen.desktop {
width: 17rem;
}
margin-bottom: 1rem;
&-control {
position: relative;
}
&-icon {
position: absolute;
top: 0;
right: 0;
background: no-repeat;
background-image: url("/assets/img/icon--search.svg");
width: 2.25rem;
height: 2.25rem;
background-position: center;
cursor: pointer;
&:hover {
background-image: url("/assets/img/icon--search-blue.svg");
}
}
input {
font-size: 0.875rem;
box-sizing: border-box;
width: 100%;
height: 2.25rem;
padding: 0.25rem 0.5rem;
border: 1px solid #dfdfdf;
background: #fff;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
transition: all 0.2s;
outline: 0 solid transparent;
border-radius: 0.1875rem;
position: relative;
top: 0;
left: 0;
&:focus {
border: 1px solid #b7d3f3;
box-shadow: 0 0.0625rem 0.1875rem #cad6e5;
}
}
}
&__actions {
display: flex;
padding: 2rem 0;
flex-wrap: wrap;
gap: 1rem;
// Match Angular: h-spacing(1rem) on gt-mobile
@include screen.desktop {
& > *:not(:last-child) {
margin-right: 1rem;
}
}
@include screen.mobile {
flex-direction: column;
flex-wrap: wrap;
& > *:not(:last-child) {
margin-bottom: 1rem;
}
}
}
&__btn {
height: 35px;
display: inline-flex;
display: flex;
align-items: center;
justify-content: center;
padding: 0 1.5rem;
+27 -1
View File
@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useParams } from "@modern-js/runtime/router";
import { createI18nInstance } from "@/i18n/config.js";
import { resolveLocaleFromPath, type Language } from "@/i18n/resolver.js";
@@ -52,9 +52,11 @@ const FALLBACK_CONFIG: ErrorConfig = {
export default function ErrorPage(): 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 : "";
@@ -73,6 +75,15 @@ export default function ErrorPage(): JSX.Element {
});
}, [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;
@@ -92,6 +103,21 @@ export default function ErrorPage(): JSX.Element {
<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"
+1 -1
View File
@@ -47,7 +47,7 @@ export const PageLayout: FC<PageLayoutProps> = ({
<div className="page-layout__title">
{title}
</div>
<FeedbackButton />
{/* FeedbackButton hidden: Angular has it behind FEEDBACK_BUTTON_AVAILABLE feature flag (off by default) */}
</div>
</div>
</div>