- Enhanced readability by restructuring code formatting in various components, including API documentation, user management, and sales modules. - Standardized table and object definitions for better clarity in API documentation. - Improved layout and indentation in React components for better maintainability. - Updated unit tests to reflect changes in formatting and ensure consistency. These changes enhance the overall code quality and maintainability of the project, making it easier for developers to navigate and understand the codebase.
152 lines
4.1 KiB
TypeScript
152 lines
4.1 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, type RefObject } from 'react';
|
|
import { CircleMarker, MapContainer, TileLayer, useMap, useMapEvents } from 'react-leaflet';
|
|
import { Box, Text } from '@mantine/core';
|
|
import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm';
|
|
import {
|
|
DEFAULT_MAP_CENTER,
|
|
DEFAULT_MAP_ZOOM,
|
|
locationFromLatLng,
|
|
SELECTED_MAP_ZOOM,
|
|
toLocationLatLng,
|
|
} from './location-point';
|
|
import 'leaflet/dist/leaflet.css';
|
|
import './leaflet-stacking.css';
|
|
|
|
export interface LocationMapProps {
|
|
latitude?: unknown;
|
|
longitude?: unknown;
|
|
onChange?: (point: { latitude: number; longitude: number }) => void;
|
|
height?: number;
|
|
emptyLabel?: string;
|
|
helperLabel?: string;
|
|
defaultCenter?: [number, number];
|
|
defaultZoom?: number;
|
|
tileUrl?: string;
|
|
attribution?: string;
|
|
}
|
|
|
|
function SyncMapView({
|
|
latitude,
|
|
longitude,
|
|
lastPickedRef,
|
|
}: {
|
|
latitude: unknown;
|
|
longitude: unknown;
|
|
lastPickedRef: RefObject<{ latitude: number; longitude: number } | null>;
|
|
}) {
|
|
const map = useMap();
|
|
const point = toLocationLatLng(latitude, longitude);
|
|
const lat = point?.[0] ?? null;
|
|
const lng = point?.[1] ?? null;
|
|
const hasCentered = useRef(false);
|
|
|
|
useEffect(() => {
|
|
const id = window.setTimeout(() => map.invalidateSize(), 0);
|
|
return () => window.clearTimeout(id);
|
|
}, [map]);
|
|
|
|
useEffect(() => {
|
|
if (lat === null || lng === null) return;
|
|
const picked = lastPickedRef.current;
|
|
if (picked && picked.latitude === lat && picked.longitude === lng) {
|
|
return;
|
|
}
|
|
if (!hasCentered.current) {
|
|
hasCentered.current = true;
|
|
map.setView([lat, lng], SELECTED_MAP_ZOOM);
|
|
return;
|
|
}
|
|
map.panTo([lat, lng]);
|
|
}, [map, lat, lng, lastPickedRef]);
|
|
|
|
return null;
|
|
}
|
|
|
|
function MapClickPicker({ onPick }: { onPick: (lat: number, lng: number) => void }) {
|
|
const onPickRef = useRef(onPick);
|
|
onPickRef.current = onPick;
|
|
const handlers = useMemo(
|
|
() => ({
|
|
click(event: { latlng: { lat: number; lng: number } }) {
|
|
onPickRef.current(event.latlng.lat, event.latlng.lng);
|
|
},
|
|
}),
|
|
[],
|
|
);
|
|
useMapEvents(handlers);
|
|
return null;
|
|
}
|
|
|
|
export function LocationMap({
|
|
latitude,
|
|
longitude,
|
|
onChange,
|
|
height = 280,
|
|
emptyLabel = 'No location',
|
|
helperLabel,
|
|
defaultCenter = DEFAULT_MAP_CENTER,
|
|
defaultZoom = DEFAULT_MAP_ZOOM,
|
|
tileUrl = OSM_TILE_URL,
|
|
attribution = OSM_ATTRIBUTION,
|
|
}: LocationMapProps) {
|
|
const interactive = Boolean(onChange);
|
|
const position = toLocationLatLng(latitude, longitude);
|
|
const lastPickedRef = useRef<{ latitude: number; longitude: number } | null>(null);
|
|
const center = position ?? defaultCenter;
|
|
const zoom = position ? SELECTED_MAP_ZOOM : defaultZoom;
|
|
|
|
const handlePick = useCallback(
|
|
(lat: number, lng: number) => {
|
|
const point = locationFromLatLng(lat, lng);
|
|
lastPickedRef.current = point;
|
|
onChange?.(point);
|
|
},
|
|
[onChange],
|
|
);
|
|
|
|
if (!interactive && !position) {
|
|
return (
|
|
<Box h={height} bdrs="md" bd="1px solid var(--mantine-color-default-border)" p="md">
|
|
<Text size="sm" c="dimmed">
|
|
{emptyLabel}
|
|
</Text>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box>
|
|
<Box
|
|
h={height}
|
|
bdrs="md"
|
|
bd="1px solid var(--mantine-color-default-border)"
|
|
className="tg-map-viewport"
|
|
style={{ cursor: interactive ? 'crosshair' : undefined }}
|
|
>
|
|
<MapContainer
|
|
center={center}
|
|
zoom={zoom}
|
|
style={{ height: '100%', width: '100%' }}
|
|
scrollWheelZoom={interactive}
|
|
>
|
|
<TileLayer attribution={attribution} url={tileUrl} />
|
|
{position ? (
|
|
<CircleMarker
|
|
center={position}
|
|
radius={10}
|
|
pathOptions={{ color: 'var(--mantine-color-blue-8)', fillOpacity: 0.9 }}
|
|
/>
|
|
) : null}
|
|
<SyncMapView latitude={latitude} longitude={longitude} lastPickedRef={lastPickedRef} />
|
|
{onChange ? <MapClickPicker onPick={handlePick} /> : null}
|
|
</MapContainer>
|
|
</Box>
|
|
{interactive && helperLabel ? (
|
|
<Text size="xs" c="dimmed" mt="xs">
|
|
{helperLabel}
|
|
</Text>
|
|
) : null}
|
|
</Box>
|
|
);
|
|
}
|