Files
flights_web/src/features/flights-map/components/FlightsMapStartPage.tsx
T
gnezim dee10544e0
CI / ci (push) Failing after 38s
Deploy / build-and-deploy (push) Failing after 6s
Polish Flights Map page with PageLayout, tabs, and filter styling
Flights Map now uses PageLayout with PageTabs (flights-map tab active),
filter in content-left column, and map in a .frame section. Added SCSS
for filter panel and map wrapper matching Angular structure.
2026-04-15 19:35:02 +03:00

212 lines
6.7 KiB
TypeScript

/**
* Flights Map start page -- container component.
*
* Manages filter state, drives the map and calendar hooks, and renders
* the filter panel + map canvas + loading/empty overlays.
*
* Uses PageLayout for the two-column layout matching the Angular version.
*
* @module
*/
import { type FC, lazy, Suspense, useState, useCallback, useMemo } from "react";
import { useTranslation } from "@/i18n/provider.js";
import { PageLayout } from "@/ui/layout/PageLayout.js";
import { PageTabs } from "@/ui/layout/PageTabs.js";
import { ClientOnly } from "./ClientOnly.js";
import { FlightsMapFilter } from "./FlightsMapFilter.js";
import { useFlightsMapSearch } from "../hooks/useFlightsMapSearch.js";
import { useFlightsMapCalendar } from "../hooks/useFlightsMapCalendar.js";
import { getEnv } from "@/env/index.js";
import type {
IFlightsMapFilterState,
FlightsMapSearchParams,
FlightsMapCalendarParams,
IMapMarker,
IMapPolyline,
} from "../types.js";
import "./FlightsMapStartPage.scss";
const MapCanvas = lazy(() =>
import("./MapCanvas.js").then((m) => ({ default: m.MapCanvas })),
);
// ---------------------------------------------------------------------------
// Date helpers
// ---------------------------------------------------------------------------
function todayYyyymmdd(): string {
const now = new Date();
const y = now.getFullYear().toString();
const m = (now.getMonth() + 1).toString().padStart(2, "0");
const d = now.getDate().toString().padStart(2, "0");
return `${y}${m}${d}`;
}
function addMonthsYyyymmdd(base: string, months: number): string {
const y = Number(base.slice(0, 4));
const m = Number(base.slice(4, 6)) - 1;
const d = Number(base.slice(6, 8));
const date = new Date(y, m + months, d);
const ry = date.getFullYear().toString();
const rm = (date.getMonth() + 1).toString().padStart(2, "0");
const rd = date.getDate().toString().padStart(2, "0");
return `${ry}${rm}${rd}`;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export const FlightsMapStartPage: FC = () => {
const env = getEnv();
const { t } = useTranslation();
const [filterState, setFilterState] = useState<IFlightsMapFilterState>({
connections: false,
domestic: false,
international: false,
});
// Build search params from filter state
const searchParams = useMemo<FlightsMapSearchParams | null>(() => {
if (!filterState.departure) return null;
const today = todayYyyymmdd();
return {
departure: filterState.departure,
arrival: filterState.arrival,
dateFrom: today,
dateTo: addMonthsYyyymmdd(today, 6),
connections: filterState.connections ? 1 : 0,
};
}, [filterState.departure, filterState.arrival, filterState.connections]);
// Build calendar params
const calendarParams = useMemo<FlightsMapCalendarParams | null>(() => {
if (!filterState.departure) return null;
const today = todayYyyymmdd();
return {
date: today,
departure: filterState.departure,
arrival: filterState.arrival,
connections: filterState.connections,
};
}, [filterState.departure, filterState.arrival, filterState.connections]);
const { routes, loading, error } = useFlightsMapSearch(searchParams);
const { availableDays } = useFlightsMapCalendar(calendarParams);
const handleFilterChange = useCallback((newState: IFlightsMapFilterState) => {
setFilterState(newState);
}, []);
const handleMarkerClick = useCallback(
(markerId: string) => {
if (!filterState.departure) {
setFilterState((prev): IFlightsMapFilterState => ({ ...prev, departure: markerId }));
} else if (!filterState.arrival && markerId !== filterState.departure) {
setFilterState((prev): IFlightsMapFilterState => ({ ...prev, arrival: markerId }));
} else {
setFilterState((prev): IFlightsMapFilterState => ({
...prev,
departure: markerId,
arrival: undefined,
}));
}
},
[filterState.departure, filterState.arrival],
);
// Build markers and polylines from routes (placeholder -- real city
// coordinates come from a dictionaries service in a future iteration)
const markers = useMemo<IMapMarker[]>(() => [], []);
const polylines = useMemo<IMapPolyline[]>(() => [], []);
// Tile URL from env or default
const tileUrl = `${env.API_BASE_URL}/tiles/{z}/{x}/{y}.png`;
return (
<div className="flights-map-start-page" data-testid="flights-map-start">
<PageLayout
headerLeft={
<PageTabs viewType="flights-map" showFlightsMap />
}
title={
<h1 className="text--white page-title">
{t("FLIGHTS-MAP.TITLE")}
</h1>
}
contentLeft={
<FlightsMapFilter
value={filterState}
availableDays={availableDays}
onChange={handleFilterChange}
/>
}
>
<section className="frame">
<div className="flights-map-start__map-wrapper">
<ClientOnly
fallback={
<div aria-busy="true" data-testid="map-loading">
Loading map...
</div>
}
>
<Suspense
fallback={
<div aria-busy="true" data-testid="map-loading">
Loading map...
</div>
}
>
<MapCanvas
markers={markers}
polylines={polylines}
tileUrl={tileUrl}
onMarkerClick={handleMarkerClick}
className="flights-map-start__map"
/>
</Suspense>
</ClientOnly>
{loading && (
<div
className="flights-map-start__loader"
aria-busy="true"
data-testid="map-loader"
>
Loading routes...
</div>
)}
{!loading && error && (
<div
className="flights-map-start__error"
role="alert"
data-testid="map-error"
>
Failed to load routes. Please try again.
</div>
)}
{!loading &&
!error &&
searchParams !== null &&
routes.length === 0 && (
<div
className="flights-map-start__empty"
data-testid="map-no-directions"
>
No directions found.
</div>
)}
</div>
</section>
</PageLayout>
</div>
);
};