dee10544e0
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.
212 lines
6.7 KiB
TypeScript
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>
|
|
);
|
|
};
|