157 lines
4.2 KiB
TypeScript
157 lines
4.2 KiB
TypeScript
import React, { useMemo, useEffect, useRef } from "react";
|
|
import { Loader } from "@googlemaps/js-api-loader";
|
|
|
|
interface GoogleMapWithRadiusProps {
|
|
latitude?: number | string;
|
|
longitude?: number | string;
|
|
mapOptions?: {
|
|
zoom?: number;
|
|
};
|
|
}
|
|
|
|
// Utility function to safely convert coordinates to numbers
|
|
const safeParseNumber = (value: number | string | undefined): number | null => {
|
|
if (value === undefined || value === null || value === "") return null;
|
|
const num = typeof value === "string" ? parseFloat(value) : value;
|
|
return !isNaN(num) && isFinite(num) ? num : null;
|
|
};
|
|
|
|
// 2 miles in meters for radius circle
|
|
const RADIUS_METERS = 2 * 1609.34;
|
|
|
|
const GoogleMapWithRadius: React.FC<GoogleMapWithRadiusProps> = ({
|
|
latitude: rawLatitude,
|
|
longitude: rawLongitude,
|
|
mapOptions = {},
|
|
}) => {
|
|
// Convert coordinates to numbers safely
|
|
const latitude = safeParseNumber(rawLatitude);
|
|
const longitude = safeParseNumber(rawLongitude);
|
|
|
|
// Destructure mapOptions to create stable references
|
|
const { zoom = 12 } = mapOptions;
|
|
|
|
// Get API key and Map ID from environment
|
|
const apiKey = process.env.REACT_APP_GOOGLE_MAPS_PUBLIC_API_KEY;
|
|
const mapId = process.env.REACT_APP_GOOGLE_MAPS_MAP_ID;
|
|
|
|
// Refs for map container and instances
|
|
const mapRef = useRef<HTMLDivElement>(null);
|
|
const mapInstanceRef = useRef<google.maps.Map | null>(null);
|
|
const circleRef = useRef<google.maps.Circle | null>(null);
|
|
|
|
// Memoize map center
|
|
const mapCenter = useMemo(() => {
|
|
if (latitude === null || longitude === null) return null;
|
|
return { lat: latitude, lng: longitude };
|
|
}, [latitude, longitude]);
|
|
|
|
// Initialize map
|
|
useEffect(() => {
|
|
if (!apiKey || !mapRef.current || !mapCenter) return;
|
|
|
|
const initializeMap = async () => {
|
|
const loader = new Loader({
|
|
apiKey,
|
|
version: "weekly",
|
|
});
|
|
|
|
try {
|
|
await loader.importLibrary("maps");
|
|
|
|
if (!mapRef.current) return;
|
|
|
|
// Create map configuration
|
|
const mapConfig: google.maps.MapOptions = {
|
|
mapId: mapId,
|
|
center: mapCenter,
|
|
zoom: zoom,
|
|
maxZoom: 15, // Prevent users from zooming too close
|
|
zoomControl: true,
|
|
mapTypeControl: false,
|
|
scaleControl: true,
|
|
streetViewControl: false,
|
|
rotateControl: false,
|
|
fullscreenControl: false,
|
|
};
|
|
|
|
const map = new google.maps.Map(mapRef.current, mapConfig);
|
|
|
|
mapInstanceRef.current = map;
|
|
|
|
// Create circle overlay
|
|
const circle = new google.maps.Circle({
|
|
center: mapCenter,
|
|
radius: RADIUS_METERS,
|
|
fillColor: "#6c757d",
|
|
fillOpacity: 0.2,
|
|
strokeColor: "#6c757d",
|
|
strokeOpacity: 0.8,
|
|
strokeWeight: 2,
|
|
map: map,
|
|
});
|
|
|
|
circleRef.current = circle;
|
|
} catch (error) {
|
|
console.error("Failed to load Google Maps:", error);
|
|
}
|
|
};
|
|
|
|
initializeMap();
|
|
|
|
// Cleanup function
|
|
return () => {
|
|
if (circleRef.current) {
|
|
circleRef.current.setMap(null);
|
|
}
|
|
mapInstanceRef.current = null;
|
|
};
|
|
}, [apiKey, mapId, mapCenter, zoom]);
|
|
|
|
// Update map center and circle when coordinates change
|
|
useEffect(() => {
|
|
if (!mapInstanceRef.current || !circleRef.current || !mapCenter) return;
|
|
|
|
mapInstanceRef.current.setCenter(mapCenter);
|
|
circleRef.current.setCenter(mapCenter);
|
|
}, [mapCenter]);
|
|
|
|
// Handle case where no coordinates are available
|
|
if (latitude === null || longitude === null || mapCenter === null) {
|
|
return (
|
|
<div className="mb-4">
|
|
<h5>Location</h5>
|
|
<div
|
|
className="d-flex align-items-center justify-content-center"
|
|
style={{
|
|
height: "300px",
|
|
backgroundColor: "#f8f9fa",
|
|
borderRadius: "8px",
|
|
}}
|
|
>
|
|
<div className="text-center">
|
|
<p className="text-muted small">Map unavailable</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="mb-4">
|
|
<h5>Location</h5>
|
|
<div
|
|
ref={mapRef}
|
|
style={{
|
|
height: "300px",
|
|
borderRadius: "8px",
|
|
backgroundColor: "#f8f9fa",
|
|
width: "100%",
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default GoogleMapWithRadius;
|