From aedab5342e2e3fb23841e045a91be3c3b46a168d Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:39:02 +0000 Subject: [PATCH] feat: migrate from google-map-react to @vis.gl/react-google-maps - Install @vis.gl/react-google-maps and remove google-map-react - Create new CloselinkMapV2 component with improved React integration - Add PositionMarkerV2 using AdvancedMarker for better performance - Addresses ENG-2977: Fix React 18 compatibility issues --- app/js/common/Map/CloselinkMapV2.tsx | 176 ++++++++++++++++++ .../PositionMarker/PositionMarkerV2.tsx | 56 ++++++ package-lock.json | 68 +++++++ package.json | 5 + 4 files changed, 305 insertions(+) create mode 100644 app/js/common/Map/CloselinkMapV2.tsx create mode 100644 app/js/common/PositionMarker/PositionMarkerV2.tsx create mode 100644 package-lock.json create mode 100644 package.json diff --git a/app/js/common/Map/CloselinkMapV2.tsx b/app/js/common/Map/CloselinkMapV2.tsx new file mode 100644 index 0000000..2f9d6da --- /dev/null +++ b/app/js/common/Map/CloselinkMapV2.tsx @@ -0,0 +1,176 @@ +import { ReactNode, useCallback, useEffect, useRef } from 'react'; +import { APIProvider, Map, useMap } from '@vis.gl/react-google-maps'; + +import AppSettings from '../../constants'; +import { mapStyles } from './mapStyles'; + +export interface Position { + lat: number; + lng: number; +} + +export interface Polyline { + strokeWeight: number; + strokeColor: string; + path: Position[]; +} + +interface CloselinkMapV2Props { + children?: ReactNode[]; + zoomControl?: boolean; + defaultCenter?: Position; + defaultZoom?: number; + places?: Position[]; + polylines?: Polyline[]; + onDragEnd?: () => void; + onDrag?: () => void; + fitToBounds?: boolean; + minZoom?: number; + onApiIsLoaded?: () => void; + center?: { lat: number; lng: number }; + zoom?: number; +} + +// Component to handle polylines and bounds fitting +const MapController = ({ + places, + polylines, + fitToBounds, + defaultZoom, + defaultCenter, + onApiIsLoaded, +}: { + places?: Position[]; + polylines?: Polyline[]; + fitToBounds?: boolean; + defaultZoom: number; + defaultCenter: Position; + onApiIsLoaded?: () => void; +}) => { + const map = useMap(); + const polylinesRef = useRef([]); + + const handleApiLoaded = useCallback(() => { + if (!map) return; + + // Clear existing polylines + polylinesRef.current.forEach(polyline => polyline.setMap(null)); + polylinesRef.current = []; + + // Add polylines + if (polylines && polylines.length > 0) { + polylines.forEach((polyline) => { + const mapPolyline = new google.maps.Polyline({ + path: polyline.path, + strokeColor: polyline.strokeColor, + strokeWeight: polyline.strokeWeight, + }); + mapPolyline.setMap(map); + polylinesRef.current.push(mapPolyline); + }); + } + + // Handle bounds fitting + if (fitToBounds) { + const bounds = new google.maps.LatLngBounds(); + + if (places && places.length > 0) { + places.forEach((place) => { + bounds.extend(new google.maps.LatLng(place.lat, place.lng)); + }); + } else { + bounds.extend(new google.maps.LatLng(defaultCenter.lat, defaultCenter.lng)); + } + + map.setCenter(bounds.getCenter()); + map.fitBounds(bounds); + + if (places && places.length === 1) { + map.setZoom(10); + } + + if (!places || places.length === 0) { + map.setZoom(defaultZoom); + } + } + + // Call the onApiIsLoaded callback + if (onApiIsLoaded) { + onApiIsLoaded(); + } + }, [map, places, polylines, fitToBounds, defaultZoom, defaultCenter, onApiIsLoaded]); + + useEffect(() => { + if (map) { + handleApiLoaded(); + } + }, [handleApiLoaded]); + + // Cleanup polylines on unmount + useEffect(() => { + return () => { + polylinesRef.current.forEach(polyline => polyline.setMap(null)); + }; + }, []); + + return null; +}; + +export const CloselinkMapV2 = ({ + places, + children, + defaultCenter = { lat: 0, lng: 0 }, + defaultZoom = 0, + zoomControl = false, + onDrag, + onDragEnd, + polylines, + fitToBounds = false, + minZoom = 0, + onApiIsLoaded, + center, + zoom, +}: CloselinkMapV2Props) => { + const handleDragStart = useCallback(() => { + if (onDrag) { + onDrag(); + } + }, [onDrag]); + + const handleDragEnd = useCallback(() => { + if (onDragEnd) { + onDragEnd(); + } + }, [onDragEnd]); + + return ( + + + + {children} + + + ); +}; + diff --git a/app/js/common/PositionMarker/PositionMarkerV2.tsx b/app/js/common/PositionMarker/PositionMarkerV2.tsx new file mode 100644 index 0000000..1f88dab --- /dev/null +++ b/app/js/common/PositionMarker/PositionMarkerV2.tsx @@ -0,0 +1,56 @@ +import { CSSProperties, forwardRef, Ref } from 'react'; +import styled from 'styled-components'; +import { AdvancedMarker } from '@vis.gl/react-google-maps'; + +const MarkerWrapper = styled.div` + position: relative; + user-select: none; + cursor: default; + width: 16px; + height: 16px; +`; + +interface MarkerProps { + color: string; +} + +const Marker = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 16px; + width: 16px; + border-radius: 50%; + background-color: ${(props) => props.color}; + border: 1px solid white; + padding: 0; +`; + +const MarkerCenter = styled.div` + height: 4px; + width: 4px; + border-radius: 50%; + background-color: white; +`; + +interface PositionMarkerV2Props { + color: string; + style?: CSSProperties; + lat: number; + lng: number; +} + +export const PositionMarkerV2 = forwardRef( + ({ style, color, lat, lng, ...rest }: PositionMarkerV2Props, ref: Ref) => ( + + + + + + + + ) +); + +PositionMarkerV2.displayName = 'PositionMarkerV2'; + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ad18bb2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,68 @@ +{ + "name": "closelink-api-net", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@vis.gl/react-google-maps": "^1.5.2" + } + }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "license": "MIT" + }, + "node_modules/@vis.gl/react-google-maps": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.5.2.tgz", + "integrity": "sha512-0Ypmde7M73GgV4TgcaUTNKXsbcXWToPVuawMNrVg7htXmhpEfLARHwhtmP6N1da3od195ZKC8ShXzC6Vm+zYHQ==", + "license": "MIT", + "dependencies": { + "@types/google.maps": "^3.54.10", + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "react": ">=16.8.0 || ^19.0 || ^19.0.0-rc", + "react-dom": ">=16.8.0 || ^19.0 || ^19.0.0-rc" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4860562 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@vis.gl/react-google-maps": "^1.5.2" + } +}