React Google Map Component with Custom Markers
This code snippet showcases a React component for rendering a Google Map with custom markers, utilizing hooks for state management and context. It includes error handling and user location tracking, making it a useful component for location-based applications.unknown
typescript
a year ago
9.4 kB
12
Indexable
'use client';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { GoogleMap, MarkerF, InfoWindow } from '@react-google-maps/api';
import { mapStyles } from '@/lib/mapStyles';
import { useSearchContext } from '@/context/SearchContext';
import { useSearchParams } from 'next/navigation';
import useMarkerStore from '@/store/markers-store';
import { useMap } from '@/context/MapContext';
import BeeSpinner from '../BeeSpinner';
import { Button } from '@/components/ui/button';
import ModernInfoWindow from '../ModernInfoWindow';
interface Marker {
id: string;
name: string;
address: string;
lat?: number;
lng?: number;
referral_code: string;
}
interface GoogleMapViewProps {
errorMessage?: string | null;
}
function GoogleMapView({ errorMessage }: GoogleMapViewProps) {
const { isLoaded, loadError } = useMap();
const [userLocation, setUserLocation] = useState<google.maps.LatLngLiteral | null>(null);
const { center, setCenter } = useSearchContext();
const { users } = useMarkerStore();
const [markers, setMarkers] = useState<Marker[]>([]);
const [selectedMarker, setSelectedMarker] = useState<Marker | null>(null);
const searchParams = useSearchParams();
const lat = searchParams.get('lat');
const lng = searchParams.get('lng');
const mapOptions = useMemo(() => ({
styles: mapStyles,
language: 'it',
region: 'IT',
clickableIcons: false,
zoomControl: true,
streetViewControl: false,
fullscreenControl: false,
mapTypeControl: false,
gestureHandling: 'greedy',
}), []);
useEffect(() => {
if (lat && lng) {
setCenter({ lat: parseFloat(lat), lng: parseFloat(lng) });
} else if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
const location = {
lat: position.coords.latitude,
lng: position.coords.longitude,
};
setCenter(location);
setUserLocation(location);
},
(error) => {
console.error("Error getting user's location:", error);
}
);
}
}, [lat, lng, setCenter]);
useEffect(() => {
if (isLoaded && users.length > 0) {
const geocoder = new google.maps.Geocoder();
const markersWithCoordinates = users.map((user, index) =>
new Promise<Marker>((resolve) => {
geocoder.geocode({ address: user.fullAddress }, (results, status) => {
if (status === 'OK' && results && results[0]) {
resolve({
id: `${user.referral_code}_${user.address}_${user.cap}_${index}`,
name: user.company_name,
address: user.fullAddress,
lat: results[0].geometry.location.lat(),
lng: results[0].geometry.location.lng(),
referral_code: user.referral_code
});
} else {
console.error('Geocode was not successful for the following reason:', status);
resolve({
id: `${user.referral_code}_${user.address}_${user.cap}_${index}`,
name: user.company_name,
address: user.fullAddress,
referral_code: user.referral_code
});
}
});
})
);
Promise.all(markersWithCoordinates).then((updatedMarkers) => {
setMarkers(updatedMarkers.filter(marker => marker.lat && marker.lng));
});
} else {
setMarkers([]);
}
}, [isLoaded, users]);
const handleMarkerClick = useCallback((marker: Marker) => {
setSelectedMarker(marker);
}, []);
const calculateDistance = useCallback((lat1: number, lon1: number, lat2: number, lon2: number) => {
const R = 6371;
const dLat = deg2rad(lat2 - lat1);
const dLon = deg2rad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c;
return d;
}, []);
const deg2rad = (deg: number) => {
return deg * (Math.PI / 180);
};
useEffect(() => {
if (isLoaded) {
const removeInfoWindowPadding = () => {
const infoWindows = document.querySelectorAll('.gm-style-iw-c');
infoWindows.forEach((iw) => {
(iw as HTMLElement).style.padding = '0';
(iw as HTMLElement).style.maxWidth = 'none';
(iw as HTMLElement).style.maxHeight = 'none';
(iw as HTMLElement).style.overflow = 'visible';
(iw as HTMLElement).style.background = 'none';
(iw as HTMLElement).style.boxShadow = 'none';
});
const infoWindowContents = document.querySelectorAll('.gm-style-iw-d');
infoWindowContents.forEach((iwc) => {
(iwc as HTMLElement).style.padding = '0';
(iwc as HTMLElement).style.maxWidth = 'none';
(iwc as HTMLElement).style.maxHeight = 'none';
(iwc as HTMLElement).style.overflow = 'visible';
});
const infoWindowBackground = document.querySelector('.gm-style-iw-t');
if (infoWindowBackground) {
(infoWindowBackground as HTMLElement).style.background = 'none';
}
// Remove the default close button
const closeButtons = document.querySelectorAll('.gm-ui-hover-effect');
closeButtons.forEach((button) => {
(button as HTMLElement).style.display = 'none';
});
// Remove the arrow
const arrows = document.querySelectorAll('.gm-style-iw-tc');
arrows.forEach((arrow) => {
(arrow as HTMLElement).style.display = 'none';
});
};
removeInfoWindowPadding();
const observer = new MutationObserver(removeInfoWindowPadding);
observer.observe(document.body, { childList: true, subtree: true });
return () => observer.disconnect();
}
}, [isLoaded, selectedMarker]);
if (loadError) return <div>Errore nel caricamento di Maps. Ricarica la pagina.</div>;
if (!isLoaded) return <div className='flex items-center justify-center h-[calc(100dvh-80px)] w-full'><BeeSpinner /></div>;
let infoWindowOptions = {};
if (isLoaded) {
infoWindowOptions = {
pixelOffset: new google.maps.Size(0, -30),
maxWidth: 256,
disableAutoPan: false,
};
}
const defaultIcon = {
url: '/assets/icons/Mapmarkerviola.svg',
scaledSize: new google.maps.Size(50, 50),
};
const selectedIcon = {
url: '/assets/icons/Mapmarkergiallo.svg',
scaledSize: new google.maps.Size(55, 55),
};
return (
<div className="flex flex-col h-full w-full relative">
<GoogleMap
mapContainerClassName="flex-1 lg:rounded-[1rem] lg:h-full rounded-none h-[calc(100vh-4rem)]"
center={center}
zoom={14}
options={mapOptions}
>
{userLocation && (
<MarkerF
position={userLocation}
title="Your Location"
/>
)}
{markers.map((marker) => (
marker.lat && marker.lng && (
<MarkerF
key={marker.id}
position={{ lat: marker.lat, lng: marker.lng }}
title={`${marker.name} - ${marker.address}`}
icon={selectedMarker?.id === marker.id ? selectedIcon : defaultIcon}
onClick={() => handleMarkerClick(marker)}
/>
)
))}
{selectedMarker && selectedMarker.lat && selectedMarker.lng && (
<InfoWindow
position={{ lat: selectedMarker.lat, lng: selectedMarker.lng }}
onCloseClick={() => setSelectedMarker(null)}
options={infoWindowOptions}
>
<div className="w-64 h-80 overflow-hidden rounded-lg">
<ModernInfoWindow
name={selectedMarker.name}
address={selectedMarker.address}
onClose={() => setSelectedMarker(null)}
distance={userLocation ? calculateDistance(
userLocation.lat,
userLocation.lng,
selectedMarker.lat,
selectedMarker.lng
) : null}
referralCode={selectedMarker.referral_code}
/>
</div>
</InfoWindow>
)}
</GoogleMap>
{errorMessage && (
<div className="absolute inset-0 z-[10] flex items-center justify-center bg-black/75 backdrop-blur-sm lg:rounded-[1rem]">
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md text-center">
<p className="text-red-600 font-semibold text-xl mb-2">Ops! Qualcosa è andato storto.</p>
<p className="mb-4">Errore nel caricamento dei marker. Ricarica la pagina.</p>
<Button
variant="nodropButton"
onClick={() => window.location.reload()}
className="text-white px-6 py-2 rounded-[10px] transition-colors"
>
Ricarica la pagina
</Button>
</div>
</div>
)}
</div>
);
}
export default GoogleMapView;Editor is loading...
Leave a Comment