Add graceful API error state with retry on search pages
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:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user