Extract components
Some checks failed
CI / Check (push) Failing after 3m35s
Build and publish Docker image / build-and-push (push) Failing after 3m49s

This commit is contained in:
Andras Schmelczer 2026-05-09 10:21:32 +01:00
parent a48eb945e0
commit fe46cb3379
30 changed files with 4075 additions and 2610 deletions

View file

@ -0,0 +1,225 @@
import { Suspense, type MutableRefObject, type ReactNode } from 'react';
import type { FeatureFilters, FeatureMeta, POI, PostcodeGeometry, ViewState } from '../../../types';
import type { useMapData } from '../../../hooks/useMapData';
import type { useTutorial } from '../../../hooks/useTutorial';
import type { TravelTimeEntry } from '../../../hooks/useTravelTime';
import type { getTutorialStyles } from '../../../lib/tutorial-styles';
import type { SearchedLocation } from '../LocationSearch';
import { MapPinIcon } from '../../ui/icons/MapPinIcon';
import type { MapFlyTo, PaneResizeHandlers } from './types';
import { MapFallback, PaneFallback } from './Fallbacks';
import { LoadingOverlay } from './LoadingOverlay';
import { Joyride, Map, MapPageSelectionPane } from './lazyComponents';
type MapData = ReturnType<typeof useMapData>;
type Tutorial = ReturnType<typeof useTutorial>;
type TutorialTheme = ReturnType<typeof getTutorialStyles>;
type RightPaneTab = 'properties' | 'area';
interface DesktopMapPageProps {
initialLoading: boolean;
tutorial: Tutorial;
tutorialTheme: TutorialTheme;
leftPaneWidth: number;
leftPaneHandlers: PaneResizeHandlers;
filtersPane: ReactNode;
mapData: MapData;
pois: POI[];
mapViewFeature: string | null;
filterRange: [number, number] | null;
viewSource: 'drag' | 'eye' | null;
onCancelPin: () => void;
features: FeatureMeta[];
selectedHexagonId: string | null;
hoveredHexagonId: string | null;
onHexagonClick: (id: string, isPostcode?: boolean, geometry?: PostcodeGeometry) => void;
onHexagonHover: (h3: string | null, x?: number, y?: number) => void;
initialViewState: ViewState;
flyToRef: MutableRefObject<MapFlyTo | null>;
theme: 'light' | 'dark';
filters: FeatureFilters;
selectedPostcodeGeometry: PostcodeGeometry | null;
onLocationSearched: (location: SearchedLocation | null) => void;
onCurrentLocationFound: (lat: number, lng: number) => void;
currentLocation: { lat: number; lng: number } | null;
travelTimeEntries: TravelTimeEntry[];
densityLabel: string;
totalCount?: number;
poiPaneOpen: boolean;
onTogglePoiPane: () => void;
poiPane: ReactNode;
showSelectionPane: boolean;
rightPaneWidth: number;
rightPaneHandlers: PaneResizeHandlers;
rightPaneTab: RightPaneTab;
onAreaTabClick: () => void;
onPropertiesTabClick: () => void;
onCloseSelection: () => void;
renderAreaPane: () => ReactNode;
renderPropertiesPane: () => ReactNode;
toasts: ReactNode;
upgradeModal: ReactNode;
}
export function DesktopMapPage({
initialLoading,
tutorial,
tutorialTheme,
leftPaneWidth,
leftPaneHandlers,
filtersPane,
mapData,
pois,
mapViewFeature,
filterRange,
viewSource,
onCancelPin,
features,
selectedHexagonId,
hoveredHexagonId,
onHexagonClick,
onHexagonHover,
initialViewState,
flyToRef,
theme,
filters,
selectedPostcodeGeometry,
onLocationSearched,
onCurrentLocationFound,
currentLocation,
travelTimeEntries,
densityLabel,
totalCount,
poiPaneOpen,
onTogglePoiPane,
poiPane,
showSelectionPane,
rightPaneWidth,
rightPaneHandlers,
rightPaneTab,
onAreaTabClick,
onPropertiesTabClick,
onCloseSelection,
renderAreaPane,
renderPropertiesPane,
toasts,
upgradeModal,
}: DesktopMapPageProps) {
return (
<div className="flex-1 flex overflow-hidden relative">
<LoadingOverlay show={initialLoading} />
{tutorial.run && (
<Suspense fallback={null}>
<Joyride
steps={tutorial.steps}
run={tutorial.run}
continuous
onEvent={tutorial.handleCallback}
styles={tutorialTheme.styles}
options={{
...tutorialTheme.options,
buttons: ['back', 'close', 'primary', 'skip'],
showProgress: true,
skipScroll: true,
}}
locale={{ last: 'Finish' }}
/>
</Suspense>
)}
<div
data-tutorial="filters"
className="flex bg-white dark:bg-navy-950 shadow-lg overflow-hidden"
style={{ width: leftPaneWidth }}
>
<div className="flex-1 flex flex-col overflow-hidden">{filtersPane}</div>
<div
className="w-3 cursor-col-resize flex items-center justify-center group bg-warm-100 dark:bg-navy-800 hover:bg-warm-200 dark:hover:bg-navy-700 border-x border-warm-200 dark:border-navy-700"
{...leftPaneHandlers}
>
<div className="flex flex-col gap-1.5">
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
<div className="w-1 h-1 rounded-full bg-warm-300 dark:bg-navy-600 group-hover:bg-warm-400 dark:group-hover:bg-navy-500" />
</div>
</div>
</div>
<div data-tutorial="map" className="flex-1 relative">
{tutorial.run && (
<div
data-tutorial="map-anchor"
aria-hidden="true"
className="pointer-events-none absolute left-1/2 top-1/2 z-20 h-4 w-4 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white bg-teal-500 shadow-lg ring-4 ring-teal-500/30 dark:border-navy-950"
/>
)}
<Suspense fallback={<MapFallback />}>
<Map
data={mapData.data}
postcodeData={mapData.postcodeData}
usePostcodeView={mapData.usePostcodeView}
pois={pois}
onViewChange={mapData.handleViewChange}
viewFeature={mapViewFeature}
colorRange={mapData.colorRange}
filterRange={filterRange}
viewSource={viewSource}
onCancelPin={onCancelPin}
onResetPreviewScale={mapData.handleResetPreviewScale}
canResetPreviewScale={mapData.canResetPreviewScale}
features={features}
selectedHexagonId={selectedHexagonId}
hoveredHexagonId={hoveredHexagonId}
onHexagonClick={onHexagonClick}
onHexagonHover={onHexagonHover}
initialViewState={initialViewState}
flyToRef={flyToRef}
theme={theme}
filters={filters}
selectedPostcodeGeometry={selectedPostcodeGeometry}
onLocationSearched={onLocationSearched}
onCurrentLocationFound={onCurrentLocationFound}
currentLocation={currentLocation}
bounds={mapData.bounds}
travelTimeEntries={travelTimeEntries}
densityLabel={densityLabel}
totalCount={totalCount}
/>
</Suspense>
<button
data-tutorial="poi-button"
onClick={onTogglePoiPane}
className={`absolute bottom-4 right-4 z-10 px-3 py-2 rounded-lg shadow-lg bg-white dark:bg-warm-800 flex items-center gap-2 ${poiPaneOpen ? 'text-teal-600 dark:text-teal-400' : 'text-warm-500 dark:text-warm-400 hover:text-teal-600 dark:hover:text-teal-400'}`}
>
<MapPinIcon className="w-5 h-5" />
<span className="text-sm font-medium">Points of interest</span>
</button>
{poiPaneOpen && (
<div className="absolute bottom-14 right-4 z-10 flex h-[60vh] min-h-0 w-80 flex-col overflow-hidden rounded-lg border border-warm-200 bg-white shadow-xl dark:border-warm-700 dark:bg-warm-900">
{poiPane}
</div>
)}
</div>
{showSelectionPane && (
<Suspense fallback={<PaneFallback />}>
<MapPageSelectionPane
width={rightPaneWidth}
resizeHandlers={rightPaneHandlers}
tab={rightPaneTab}
onAreaTabClick={onAreaTabClick}
onPropertiesTabClick={onPropertiesTabClick}
onClose={onCloseSelection}
renderAreaPane={renderAreaPane}
renderPropertiesPane={renderPropertiesPane}
/>
</Suspense>
)}
{toasts}
{upgradeModal}
</div>
);
}