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.
 avatar
unknown
typescript
5 months ago
9.4 kB
6
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