Google Maps not working on page refresh React

318 Views Asked by At

I have a Google Maps component from react-google-maps API that works just fine on accessing the page the first time. However, it fails to display the map upon refreshing the page, and there are no errors on the browser console to reflect this behavior.

I have tried to utilize the isLoaded variable from both useJsApiLoader and useLoadScript to render the map component conditionally but it fails to display upon refreshing the page even when isLoaded is true.

Code:

fetch location data in LocationContext.tsx:

"use client";

import { useRestaurantMenuContext } from "./RestaurantMenuContext";
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useState,
} from "react";
import { useJsApiLoader, useLoadScript } from "@react-google-maps/api";
import {
  LocationContextValue,
  LocationProviderProps,
  Restaurant,
} from "types/types";

const libraries: (
  | "places"
  | "drawing"
  | "geometry"
  | "localContext"
  | "visualization"
)[] = ["places", "geometry"];

const LocationContext = createContext<LocationContextValue | undefined>(
  undefined
);

export const useLocationContext = (): LocationContextValue => {
  const context = useContext(LocationContext);
  if (!context) {
    throw new Error(
      "useLocationContext must be used within a LocationProvider"
    );
  }
  return context;
};

function calculateDistanceBetweenLocations(
  location1: google.maps.LatLng,
  location2: google.maps.LatLng
) {
  const distanceInMeters =
    window.google.maps.geometry.spherical.computeDistanceBetween(
      location1,
      location2
    );
  return distanceInMeters / 1000;
}

export const LocationProvider: React.FC<LocationProviderProps> = ({
  children,
}) => {
  const { isLoaded } = useJsApiLoader({
    googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY as string,
    libraries,
  });

  const { restaurantData, setRestaurantData, isLoading } =
    useRestaurantMenuContext();
  const [currentLocation, setCurrentLocation] =
    useState<null | google.maps.LatLng>(null);
  const [distance, setDistance] = useState<number | null>(null);
  //   const [selectedMarker, setSelectedMarker] =
  //     useState<null | google.maps.Marker>(null);
  const [restaurantLocation, setRestaurantLocation] =
    useState<google.maps.LatLngLiteral | null>(null);
  const [selectedRestaurant, setSelectedRestaurant] =
    useState<Restaurant | null>(null);

  console.log(`restaurant data: ${JSON.stringify(restaurantData)}`);
  // console.log(`current location`, currentLocation);

  // useEffect(() => {
  //   if (selectedRestaurant) {
  //     const { latitude, longitude } = selectedRestaurant;
  //     setRestaurantLocation({ lat: latitude, lng: longitude });
  //     console.log(
  //       `restaurant location: latitude: ${latitude}, longitude: ${longitude}`
  //     );
  //   }

  const updatedRestaurantData = useCallback(
    (location: google.maps.LatLng) => {
      const updatedData = restaurantData.map((restaurant) => {
        const restaurantLocation = new window.google.maps.LatLng(
          restaurant.latitude,
          restaurant.longitude
        );
        const distanceInKm = calculateDistanceBetweenLocations(
          location,
          restaurantLocation
        );
        return { ...restaurant, distance: Number(distanceInKm.toFixed(2)) };
      });
      setRestaurantData(updatedData);
    },
    [restaurantData, setRestaurantData]
  );

  useEffect(() => {
    if (selectedRestaurant) {
      const { latitude, longitude } = selectedRestaurant;
      setRestaurantLocation({ lat: latitude, lng: longitude });
    }
  }, [selectedRestaurant]);

  // const { isLoaded } = useLoadScript({
  //   googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_KEY as string,
  //   libraries,
  // });

  console.log(`isLoaded:`, isLoaded);
  useEffect(() => {
    let watchId: number;

    if (isLoaded && window.google) {
      watchId = navigator.geolocation.watchPosition(
        (position) => {
          const { longitude, latitude } = position.coords;
          //   console.log(
          //     `current location: latitude: ${latitude}, longitude: ${longitude}`
          //   );
          setCurrentLocation(
            new window.google.maps.LatLng(latitude, longitude)
          );
        },
        (error) => {
          console.error(error);
        }
      );
    }

    return () => {
      if (watchId) {
        navigator.geolocation.clearWatch(watchId);
      }
    };
  }, [isLoaded]);

  useEffect(() => {
    if (currentLocation && window.google) {
      if (!isLoading) {
        updatedRestaurantData(currentLocation);
      }
    }
  }, [currentLocation, isLoading]);

  return (
    <LocationContext.Provider
      value={{
        restaurantLocation,
        currentLocation,
        distance,
        selectedRestaurant,
        setSelectedRestaurant,
        isLoaded,
        restaurantData,
      }}
    >
      {children}
    </LocationContext.Provider>
  );
};

display map component in fetchDisplayMap.tsx:

import {
  GoogleMap,
  MarkerF,
  InfoWindow,
  useLoadScript,
} from "@react-google-maps/api";
import { DisplayMapProps } from "types/types";
import { useLocationContext } from "app/context/LocationContext";
import { useEffect, useState } from "react";

const DisplayMap: React.FC<DisplayMapProps > = () => {
  const { restaurantLocation, currentLocation, selectedRestaurant, isLoaded } =
    useLocationContext();
  console.log(
    `restaurant location: `,
    restaurantLocation,
    `current location: `,
    currentLocation
  );
  console.log(`selected restaurant: `, selectedRestaurant);

  const [selectedMarker, setSelectedMarker] =
    useState<null | google.maps.Marker>(null);
  const [zoomLevel, setZoomLevel] = useState(14); // Initial zoom level

  const distance = selectedRestaurant?.distance;

  useEffect(() => {
    if (distance && distance > 5) {
      setZoomLevel(10); // Set lower zoom level if distance is greater than 5km
    } else if (distance && distance > 10) {
      setZoomLevel(4);
    } else {
      setZoomLevel(14);
    }
  }, [selectedRestaurant, distance]);

  const handleCloseInfoWindow = () => {
    setSelectedMarker(null);
  };

  useEffect(() => {
    if (isLoaded) {
      console.log("Google Maps library is loaded");
    }
  });

  if (!isLoaded || !restaurantLocation || !currentLocation) {
    console.log("Map not ready yet");
    return null;
  }

  console.log("Rendering map");

  return (
    <GoogleMap
      zoom={zoomLevel}
      center={currentLocation}
      mapContainerStyle={{ width: "100vw", height: "208px" }}
    >
      {currentLocation && <MarkerF position={currentLocation} />}
      {restaurantLocation && <MarkerF position={restaurantLocation} />}
      {restaurantLocation && (
        <InfoWindow
          position={restaurantLocation}
          onCloseClick={handleCloseInfoWindow}
        >
          <div>
            <div>
              {distance
                ? `${distance.toFixed(2)} km away`
                : "Calculating distance..."}
            </div>
          </div>
        </InfoWindow>
      )}
    </GoogleMap>
  );
};

export default DisplayMap;

fetch Restaurant Data in fetchDisplayRestaurants.tsx:

"use client";

import { RestaurantMenuContextValue, Restaurant, Menu } from "types/types";
import { createContext, useContext, useEffect, useState } from "react";
import supabase from "supabase/supabase";

const RestaurantMenuContext = createContext<
  RestaurantMenuContextValue | undefined
>(undefined);

export const useRestaurantMenuContext = (): RestaurantMenuContextValue => {
  const context = useContext(RestaurantMenuContext);

  if (!context) {
    throw new Error(
      "useRestaurantMenuContext must be used within a RestaurantMenuProvider"
    );
  }
  return context;
};

export const RestaurantMenuProvider: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  const [restaurantData, setRestaurantData] = useState<Restaurant[]>([]);
  const [menuData, setMenuData] = useState<Menu[]>([]);
  const [isLoading, setIsLoading] = useState<boolean>(true);

  useEffect(() => {
    async function fetchRestaurantData() {
      try {
        const { data: restaurantData, error: restaurantDataError } =
          await supabase
            .from("Restaurant")
            .select("id, name, photoUrl, location, latitude, longitude");
        console.log(`restaurantData: `, restaurantData);

        const { data: menuData, error: menuDataError } = await supabase
          .from("Menu")
          .select("*");

        if (restaurantDataError || menuDataError) {
          console.error(restaurantDataError);
          console.error(menuDataError);
        } else {
          setRestaurantData(restaurantData ?? []);
          setMenuData(menuData as Menu[]);
          // setIsLoading(false);
        }
      } catch (error) {
        console.error(error);
      } finally {
        setIsLoading(false);
      }
    }

    fetchRestaurantData();
  }, []);

  return (
    <RestaurantMenuContext.Provider
      value={{ restaurantData, setRestaurantData, menuData, isLoading }}
    >
      {children}
    </RestaurantMenuContext.Provider>
  );
};

layout.tsx:

import { Metadata } from "next";
import React from "react";
import "./globals.css";
import "./Home.module.css";
import SupabaseListener from "./components/supabase-listener";
import { RestaurantMenuProvider } from "./context/RestaurantMenuContext";
import { DeliveryProvider } from "./context/DeliveryContext";
import { LocationProvider } from "./context/LocationContext";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body suppressHydrationWarning={true}>
        {/* @ts-expect-error next version of TS will fix this */}
        <SupabaseListener />
        <RestaurantMenuProvider>
          <LocationProvider>
            <DeliveryProvider>
              <div className="mt-16">{children}</div>
            </DeliveryProvider>
          </LocationProvider>
        </RestaurantMenuProvider>
      </body>
    </html>
  );
}

export const metadata: Metadata = {
  title: "Order from your favorite restaurant | Maui",
  description: "",
};

fetchDisplayMaps is displayed in Pickup.tsx:

// import { LocationProvider } from "app/context/LocationContext";
import DisplayMap from "app/lib/fetchDisplayMap";
import { useRouter } from "next/navigation";
import { DisplayMapProps } from "types/types";
import { useState, useEffect, useRef } from "react";
import { BsArrowLeftCircleFill } from "react-icons/bs";
import { IoMdArrowBack } from "react-icons/io";

export default function Pickup({}: DisplayMapProps) {
  const router = useRouter();
  const [searchTerm, setSearchTerm] = useState("");
  const [showSuggestions, setShowSuggestions] = useState(false);
  const [suggestions, setSuggestions] = useState<string[]>([]);
  const searchRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    // Fetch suggestions based on the search term
    // Replace this with your own API call or search logic
    const fetchSuggestions = async () => {
      try {
        // Simulating an API call delay
        await new Promise((resolve) => setTimeout(resolve, 500));
        // Mocking suggestions
        const mockSuggestions = [
          "Quick Fries",
          "Fast Food",
          "Burgers",
          "Chicken Wings",
        ];
        setSuggestions(mockSuggestions);
      } catch (error) {
        console.error("Error fetching suggestions:", error);
      }
    };

    if (searchTerm) {
      fetchSuggestions();
      setShowSuggestions(true);
    } else {
      setShowSuggestions(false);
      setSuggestions([]);
    }
  }, [searchTerm]);

  const handleSearch = () => {
    // Perform search logic here
    console.log("Searching for:", searchTerm);
    // Redirect or update UI with search results
  };

  const handleSuggestionClick = (suggestion: string) => {
    setSearchTerm(suggestion);
    setShowSuggestions(false);
  };

  const handleClickOutside = (event: MouseEvent) => {
    if (
      searchRef.current &&
      !searchRef.current.contains(event.target as Node)
    ) {
      setShowSuggestions(false);
    }
  };

  useEffect(() => {
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      document.removeEventListener("mousedown", handleClickOutside);
    };
  }, []);

  return (
    <div className="h-52 flex relative bg-black">
      <BsArrowLeftCircleFill />
      <button
        type="button"
        className="z-10 absolute top-3 left-3 bg-white rounded-full p-2"
        onClick={() => router.back()}
      >
        <IoMdArrowBack className="rounded-full h-6 w-6" />
      </button>
      <div className="z-10 absolute top-3 right-3 flex">
        <div
          ref={searchRef}
          className={`relative transition-all duration-300 ${
            showSuggestions ? "w-64" : "w-20"
          }`}
        >
          <input
            type="text"
            placeholder={`Search${showSuggestions ? ` locations` : "..."}`}
            value={searchTerm}
            onChange={(e) => setSearchTerm(e.target.value)}
            onFocus={() => setShowSuggestions(true)}
            className="rounded-full h-9 w-full p-2 bg-white text-black transition-all duration-300 outline-none focus:ring-0"
          />
          <button
            type="button"
            className={`absolute top-1/2 right-2 transofrm -translate-y-1/2 transition-all duration-300 ${
              showSuggestions ? "opacity-0" : "opacity-100"
            }`}
            onClick={handleSearch}
          >
            {/* <BiSearch className="h-6 w-6" /> */}
          </button>
          {showSuggestions && (
            <ul className="absolute w-full bg-white rounded-b-xl mt-1 overflow-hidden shadow-md">
              {suggestions.map((suggestion, index) => (
                <li
                  key={index}
                  className="px-4 py-2 cursor-pointer hover:bg-gray-200"
                  onClick={() => handleSuggestionClick(suggestion)}
                >
                  {suggestion}
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
      <DisplayMap />
    </div>
  );
}

Pickup.tsx is displayed in app/restaurants/[slug]/page.tsx:

"use client";

import { useState, useEffect } from "react";
import { Toggle } from "components/Toggle";
import Pickup from "components/Pickup";
import Delivery from "components/Delivery";
import RestaurantMenu from "components/RestaurantMenu";
import { useParams } from "next/navigation";
import { useDeliveryContext } from "../../context/DeliveryContext";
import { useRestaurantMenuContext } from "app/context/RestaurantMenuContext";
import { Restaurant } from "types/types";

const DeliveryOrPickup: React.FC<{ restaurant: Restaurant }> = ({
  restaurant,
}) => {
  const { isDelivery } = useDeliveryContext();

  if (isDelivery) {
    return <Delivery restaurant={restaurant} />;
  } else {
    return <Pickup />;
  }
};

export default function RestaurantDetailsPage() {
  const { restaurantData, menuData, isLoading } = useRestaurantMenuContext();

  const { toggleIsDelivery } = useDeliveryContext();

  const [error, setError] = useState<boolean>(false);

  // useEffect(() => {
  //   if (isLoading) {
  //     // Data not loaded yet, check for error after delay
  //     const timeout = setTimeout(() => {
  //       setError(true);
  //     }, 15000);

  //     return () => clearTimeout(timeout);
  //   }
  // }, [isLoading]);

  const handleRetry = () => {
    setError(false);
  };

  if (error) {
    return (
      <div>
        Error occurred while loading data.{" "}
        <button onClick={handleRetry}>Retry</button>
      </div>
    );
  }

  // if (isLoading) {
  //   return <div>Loading...</div>;
  // }

  console.log("menuData", menuData);
  const params = useParams();
  const { slug } = params;
  const str = slug?.toString().replace(/-/g, " ");
  console.log(`str: ${str}`);
  const capitalizedSlug = str
    ?.toLowerCase()
    .replace(/(^|\s)\S/g, (match) => match.toUpperCase());

  const restaurant = restaurantData.find(
    (item) => item.name === capitalizedSlug
  );
  const menu = menuData.filter(
    (item) => item.restaurantName === capitalizedSlug
  );

  return (
    <div>
      {restaurant && (
        <div>
          <DeliveryOrPickup restaurant={restaurant} />
          <div className="pl-2 mb-8 mt-6">
            <div className="text-3xl font-semibold">{restaurant?.name}</div>
            <div className="font-semibold">4.5</div>
            <div>Open until ...</div>
            <div>Tap for hours, info and more</div>
          </div>
          <Toggle onToggle={toggleIsDelivery} />
          <div className="mb-3 flex justify-around">
            <div>See similar</div>
            <div>Group order</div>
          </div>
          {restaurant && <RestaurantMenu restaurant={restaurant} menu={menu} />}
        </div>
      )}
    </div>
  );
}

Package.json:

{
  "name": "maui",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@googlemaps/google-maps-services-js": "^3.3.29",
    "@headlessui/react": "^1.7.15",
    "@heroicons/react": "^2.0.18",
    "@hookform/resolvers": "^3.1.0",
    "@mantine/carousel": "^5.10.0",
    "@prisma/client": "^4.12.0",
    "@react-google-maps/api": "^2.18.1",
    "@supabase/auth-helpers-nextjs": "^0.7.1",
    "@supabase/supabase-js": "^2.12.0",
    "@types/react": "18.0.26",
    "@types/react-dom": "18.0.10",
    "@vercel/analytics": "^1.0.0",
    "embla-carousel-autoplay": "^7.0.5",
    "embla-carousel-react": "^7.0.5",
    "encoding": "^0.1.13",
    "eslint": "8.30.0",
    "eslint-config-next": "13.1.1",
    "next": "^13.4.8",
    "react": "^18.2.0",
    "react-content-loader": "^6.2.1",
    "react-dom": "^18.2.0",
    "react-hook-form": "^7.44.3",
    "react-icons": "^4.10.1",
    "react-loading-skeleton": "^3.3.1",
    "react-spinners": "^0.13.8",
    "slugify": "^1.6.6",
    "swr": "^2.0.0",
    "typescript": "4.9.4",
    "zod": "^3.21.4",
    "zustand": "^4.3.8"
  },
  "devDependencies": {
    "@tailwindcss/forms": "^0.5.3",
    "@types/next-auth": "^3.15.0",
    "@types/node": "^20.2.6",
    "@types/uuid": "^9.0.2",
    "autoprefixer": "^10.4.13",
    "dotenv": "^16.0.3",
    "postcss": "^8.4.20",
    "prisma": "^4.12.0",
    "tailwindcss": "^3.2.4"
  }
}
0

There are 0 best solutions below