perfect-postcode/frontend/src/components/map/map-page/effects.ts
2026-05-14 20:42:48 +01:00

182 lines
5.4 KiB
TypeScript

import { useEffect } from 'react';
import type { MutableRefObject } from 'react';
import type { PostcodeGeometry, ViewState } from '../../../types';
import type { useMapData } from '../../../hooks/useMapData';
import { authHeaders } from '../../../lib/api';
import { POSTCODE_SEARCH_ZOOM } from '../../../lib/consts';
import { canWheelScrollInsideTarget } from '../../../lib/dom-scroll';
import type { MapFlyTo } from './types';
type MapData = ReturnType<typeof useMapData>;
type RightPaneTab = 'properties' | 'area';
const SCREENSHOT_MAP_IDLE_FALLBACK_MS = 1000;
export function useInitialMapPageView(
mapData: MapData,
initialViewState: ViewState,
initialTab: RightPaneTab,
setRightPaneTab: (tab: RightPaneTab) => void
) {
useEffect(() => {
mapData.setInitialView(initialViewState);
setRightPaneTab(initialTab);
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}
interface UseInitialPostcodeSelectionOptions {
initialPostcode?: string;
isMobile: boolean;
flyTo: MutableRefObject<MapFlyTo | null>;
onLocationSearch: (
postcode: string,
geometry: PostcodeGeometry,
lat?: number,
lng?: number
) => void;
onOpenMobileDrawer: (target: { lat: number; lng: number; zoom: number }) => void;
}
export function useInitialPostcodeSelection({
initialPostcode,
isMobile,
flyTo,
onLocationSearch,
onOpenMobileDrawer,
}: UseInitialPostcodeSelectionOptions) {
useEffect(() => {
if (!initialPostcode) return;
const params = new URLSearchParams(window.location.search);
params.delete('pc');
const newUrl = params.toString() ? `/dashboard?${params}` : '/dashboard';
window.history.replaceState(window.history.state, '', newUrl);
fetch(`/api/postcode/${encodeURIComponent(initialPostcode)}`, authHeaders())
.then((res) => {
if (!res.ok) throw new Error('Postcode not found');
return res.json();
})
.then(
(data: {
postcode: string;
latitude: number;
longitude: number;
geometry: PostcodeGeometry;
}) => {
flyTo.current?.(data.latitude, data.longitude, POSTCODE_SEARCH_ZOOM);
onLocationSearch(data.postcode, data.geometry, data.latitude, data.longitude);
if (isMobile) {
onOpenMobileDrawer({
lat: data.latitude,
lng: data.longitude,
zoom: POSTCODE_SEARCH_ZOOM,
});
}
}
)
.catch(() => {
// Silently fail because the postcode might no longer exist.
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
}
export function useHorizontalSwipeNavigationGuard() {
useEffect(() => {
const handleWheel = (e: WheelEvent) => {
if (
Math.abs(e.deltaX) > Math.abs(e.deltaY) &&
!canWheelScrollInsideTarget(e.target, e.deltaX, e.deltaY)
) {
e.preventDefault();
}
};
document.addEventListener('wheel', handleWheel, { passive: false });
return () => document.removeEventListener('wheel', handleWheel);
}, []);
}
export function useMobileBackNavigationGuard(isMobile: boolean) {
useEffect(() => {
if (!isMobile) return;
window.history.pushState({ dashboardGuard: true }, '');
const handlePopState = () => {
window.history.pushState({ dashboardGuard: true }, '');
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [isMobile]);
}
interface UseScreenshotReadySignalOptions {
screenshotMode?: boolean;
loading: boolean;
boundsReady: boolean;
dataLength: number;
postcodeDataLength: number;
usePostcodeView: boolean;
licenseRequired: boolean;
}
export function useScreenshotReadySignal({
screenshotMode,
loading,
boundsReady,
dataLength,
postcodeDataLength,
usePostcodeView,
licenseRequired,
}: UseScreenshotReadySignalOptions) {
useEffect(() => {
if (!screenshotMode || loading || !boundsReady) return;
const hasData = usePostcodeView ? postcodeDataLength > 0 : dataLength > 0;
if (!hasData && !licenseRequired) return;
let cancelled = false;
let signalled = false;
let frameId: number | null = null;
let timeoutId: number | null = null;
const signalReady = () => {
if (cancelled || signalled) return;
signalled = true;
if (timeoutId != null) window.clearTimeout(timeoutId);
if (frameId != null) window.cancelAnimationFrame(frameId);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (!cancelled) window.__screenshot_ready = true;
});
});
};
const waitAndSignal = () => {
if (window.__map_idle) {
signalReady();
} else {
frameId = requestAnimationFrame(waitAndSignal);
}
};
// In webpack dev mode MapLibre's idle event can be delayed by the dev
// client/HMR churn even after data has rendered. Keep production-quality
// waiting when idle fires, but avoid forcing the screenshot service to hit
// its much longer timeout in local development.
timeoutId = window.setTimeout(signalReady, SCREENSHOT_MAP_IDLE_FALLBACK_MS);
waitAndSignal();
return () => {
cancelled = true;
if (timeoutId != null) window.clearTimeout(timeoutId);
if (frameId != null) window.cancelAnimationFrame(frameId);
};
}, [
screenshotMode,
loading,
boundsReady,
dataLength,
postcodeDataLength,
usePostcodeView,
licenseRequired,
]);
}