182 lines
5.4 KiB
TypeScript
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,
|
|
]);
|
|
}
|