Add graceful API error state with retry on search pages
CI / ci (push) Failing after 37s
Deploy / build-and-deploy (push) Failing after 6s

When the API fetch fails (backend unavailable), show a styled white
error card with a Russian-language message and retry button instead
of barely-visible text on a dark background.
This commit is contained in:
2026-04-15 20:58:07 +03:00
parent e8935276a0
commit 11026cd244
2 changed files with 111 additions and 53 deletions
@@ -19,11 +19,41 @@
}
}
&__error {
&__error-card {
background: colors.$white;
border-radius: vars.$border-radius;
padding: vars.$space-xl;
text-align: center;
}
&__error-title {
margin: 0 0 vars.$space-m;
font-size: 18px;
color: colors.$red;
}
&__error-message {
margin: 0 0 vars.$space-xl;
color: #666;
font-size: 14px;
}
&__retry-btn {
display: inline-block;
padding: vars.$space-s2 vars.$space-xl;
background-color: colors.$blue;
color: colors.$white;
border: none;
border-radius: vars.$border-radius;
cursor: pointer;
font-size: 14px;
font-weight: 500;
&:hover {
opacity: 0.9;
}
}
&__actions {
display: none;
}
@@ -15,7 +15,11 @@
import type { FC } from "react";
import { useCallback } from "react";
import { useNavigate, useParams } from "@modern-js/runtime/router";
import { useTranslation } from "@/i18n/provider.js";
import { FlightList } from "@/ui/flights/FlightList.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { OnlineBoardFilter } from "./OnlineBoardFilter.js";
import "./OnlineBoardSearchPage.scss";
import { JsonLdRenderer } from "@/shared/seo/json-ld.js";
import { useOnlineBoard } from "../hooks/useOnlineBoard.js";
@@ -133,6 +137,7 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
params,
}) => {
const navigate = useNavigate();
const { t } = useTranslation();
const routeParams = useParams<{ lang: string }>();
const lang = routeParams.lang ?? "ru";
@@ -196,62 +201,85 @@ export const OnlineBoardSearchPage: FC<OnlineBoardSearchPageProps> = ({
return (
<div className="online-board-search" data-testid="online-board-search">
{jsonLd && <JsonLdRenderer data={jsonLd} />}
{/* Connection status indicator */}
<div className="online-board-search__status" data-testid="connection-status">
{connectionStatus === "live" && (
<span className="connection-badge connection-badge--live">Live</span>
)}
{connectionStatus === "reconnecting" && (
<span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
)}
{connectionStatus === "offline" && (
<span className="connection-badge connection-badge--offline">Offline</span>
)}
</div>
{/* Calendar strip (simple date list for now) */}
{calendarDays.length > 0 && (
<div className="online-board-search__calendar" data-testid="calendar-strip">
{calendarDays.map((day) => (
<button
key={day}
type="button"
className={`calendar-day${day === params.date ? " calendar-day--active" : ""}`}
onClick={() => handleDateChange(day)}
>
{day}
</button>
))}
<PageLayout
headerLeft={<PageTabs viewType="onlineboard" />}
title={
<h1 className="text--white page-title">
{t("BOARD.TITLE")}
</h1>
}
contentLeft={<OnlineBoardFilter />}
stickyContent={
calendarDays.length > 0 ? (
<div className="online-board-search__calendar" data-testid="calendar-strip">
{calendarDays.map((day) => (
<button
key={day}
type="button"
className={`calendar-day${day === params.date ? " calendar-day--active" : ""}`}
onClick={() => handleDateChange(day)}
>
{day}
</button>
))}
</div>
) : undefined
}
>
{/* Connection status indicator */}
<div className="online-board-search__status" data-testid="connection-status">
{connectionStatus === "live" && (
<span className="connection-badge connection-badge--live">Live</span>
)}
{connectionStatus === "reconnecting" && (
<span className="connection-badge connection-badge--reconnecting">Reconnecting...</span>
)}
{connectionStatus === "offline" && (
<span className="connection-badge connection-badge--offline">Offline</span>
)}
</div>
)}
{/* Error state */}
{error && (
<div className="online-board-search__error" data-testid="search-error">
<p>Failed to load flights. Please try again.</p>
<button type="button" onClick={refresh}>Retry</button>
</div>
)}
{/* Error state */}
{error && (
<section className="frame" data-testid="search-error">
<div className="online-board-search__error-card">
<h3 className="online-board-search__error-title">
Не удалось загрузить данные
</h3>
<p className="online-board-search__error-message">
API сервер недоступен. Проверьте подключение и попробуйте снова.
</p>
<button
type="button"
className="online-board-search__retry-btn"
onClick={refresh}
>
Повторить
</button>
</div>
</section>
)}
{/* Flight list */}
<FlightList flights={displayFlights} loading={loading} />
{/* Flight list */}
{!error && <FlightList flights={displayFlights} loading={loading} />}
{/* Flight click overlay — we make the list clickable */}
{!loading && displayFlights.length > 0 && (
<div className="online-board-search__actions" data-testid="flight-actions">
{displayFlights.map((flight) => (
<button
key={flight.id}
type="button"
className="flight-detail-link"
data-testid={`flight-link-${flight.id}`}
onClick={() => handleFlightClick(flight)}
>
View details for {flight.flightId.carrier} {flight.flightId.flightNumber}
</button>
))}
</div>
)}
{/* Flight click overlay — we make the list clickable */}
{!loading && displayFlights.length > 0 && (
<div className="online-board-search__actions" data-testid="flight-actions">
{displayFlights.map((flight) => (
<button
key={flight.id}
type="button"
className="flight-detail-link"
data-testid={`flight-link-${flight.id}`}
onClick={() => handleFlightClick(flight)}
>
View details for {flight.flightId.carrier} {flight.flightId.flightNumber}
</button>
))}
</div>
)}
</PageLayout>
</div>
);
};