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; 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; 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, ]); }