MapAddress

 avatar
unknown
javascript
a month ago
12 kB
2
Indexable
import {
  useState,
  useCallback,
  useEffect,
  useRef,
  useReducer,
  useLayoutEffect,
} from "react";

import BillingDetailsForm from "../address/BilllingDetailsForm";
import Loader from "../loader/Loader";

import { Sheet } from "react-modal-sheet";

import {
  GoogleMap,
  useJsApiLoader,
  Marker,
  Autocomplete,
} from "@react-google-maps/api";
import "../map/style.scss";
import { Skeleton, Stack } from "@mui/material";

import { TbCurrentLocation } from "react-icons/tb";
import { IoIosClose } from "react-icons/io";
import { IoChevronBackCircleSharp } from "react-icons/io5";
import { GrLocationPin } from "react-icons/gr";

import { removePlusCode } from "../../utils/helper";

const initialState = {
  loading: false,
  erroMsg: "",
  userCurrentPosition: null,
  userSearchedPosition: null,
  markerPosition: null,
  mapInstance: null,
  userSearchedAddress: "",
  userCurrentAddress: "",
  inputFieldValue: "",
  isBillingDetailsFormOpen: false,
};

function reducer(state, action) {
  switch (action.type) {
    case "userCurrentPosition/updated":
      return {
        ...state,
        userCurrentPosition: { ...action.payload.userCurrentPosition },
        markerPosition: { ...action.payload.userCurrentPosition },
        userCurrentAddress: action.payload.userCurrentAddress,
      };

    case "userSearchedPosition/updated":
      return {
        ...state,
        userSearchedPosition: { ...action.payload.userSearchedPosition },
        markerPosition: { ...action.payload.userSearchedPosition },
        userSearchedAddress: action.payload.userSearchedAddress,
        inputFieldValue: action.payload.userSearchedAddress,
      };

    case "userSearchedAddress/cleared":
      return { ...state, userSearchedAddress: "", inputFieldValue: "" };

    case "inputFieldValue/update":
      return { ...state, inputFieldValue: action.payload };

    case "loading/set":
      return { ...state, loading: action.payload };

    case "geolocation/error":
      return { ...state, erroMsg: action.payload, loading: false };

    case "mapInstance/centered":
      return { ...state, mapInstance: action.payload };

    case "billingDetailsForm/toggled":
      return { ...state, isBillingDetailsFormOpen: action.payload };

    default:
      throw new Error("Unknow action at MapAddress");
  }
}

function MapAddress({
  onShowAddressDetailForm,
  isSmallScreen = false,
  showAddressDetailForm,
  phoneNumberInput,
  addressFromMap,
  onSaveUserEnteredAddressDetails,
}) {
  const [
    {
      userCurrentPosition,
      userSearchedPosition,
      markerPosition,
      loading,
      erroMsg,
      mapInstance,
      userSearchedAddress,
      userCurrentAddress,
      inputFieldValue,
      isBillingDetailsFormOpen,
    },
    dispatch,
  ] = useReducer(reducer, initialState);

  const autoCompleteRef = useRef(null);
  const inputRef = useRef(null);

  const { isLoaded } = useJsApiLoader({
    id: "google-map-script",
    googleMapsApiKey: import.meta.env.VITE_GM_API_KEY,
    libraries: ["places"],
  });

  // console.log(isLoaded);
  // console.log("userCurrentPosition: ", userCurrentPosition);
  // console.log("userSearchedPosition: ", userSearchedPosition);
  // console.log(import.meta.env.VITE_GM_API_KEY);
  // console.log("Searched Address : ", userSearchedAddress);
  // console.log("Current Address : ", userCurrentAddress);

  // EFFECT to fetch the user latitude and longitude on initail Load
  useEffect(function () {
    // console.log("EEFECT EXECUTED");

    dispatch({ type: "loading/set", payload: true });
    if (!navigator.geolocation) {
      dispatch({
        type: "geolocation/error",
        payload: "Your browser does not support geolocation",
      });
      return;
    }

    navigator.geolocation.getCurrentPosition(
      // SUCCESS Callback
      (success) => {
        // console.log("success.coords.latitude : ", success.coords.latitude);

        const { latitude, longitude } = success.coords;

        // Fetch User Address using lat and lng with Geocoder API

        const geocoder = new window.google.maps.Geocoder();
        geocoder.geocode(
          { location: { lat: latitude, lng: longitude } },
          (results, status) => {
            if (status === "OK" && results.at(0)) {
              // console.log(results.at(0));
              const address = results.at(0)?.formatted_address;
              // console.log(address);
              dispatch({
                type: "userCurrentPosition/updated",
                payload: {
                  userCurrentPosition: { lat: latitude, lng: longitude },
                  userCurrentAddress: address,
                },
              });
            } else {
              console.error("Geocoder failed due to: " + status);
            }
          },
        );
      },
      // ERROR Callback
      (error) => {
        console.log(error.message);
        dispatch({
          type: "geolocation/error",
          payload: error.message,
        });
      },
    );
  }, []);

  // Handle place selection from suggestions list of google-api from Autocomplete Component
  function handlePlaceSelect() {
    if (autoCompleteRef.current) {
      const place = autoCompleteRef.current.getPlace();

      if (place && place.geometry) {
        const location = place.geometry.location;
        const address = place.formatted_address;

        console.log("address : ", address);

        dispatch({
          type: "userSearchedPosition/updated",
          payload: {
            userSearchedPosition: { lat: location.lat(), lng: location.lng() },
            userSearchedAddress: address,
          },
        });

        mapInstance?.panTo({ lat: location.lat(), lng: location.lng() });
      }
    }
  }

  // Handle input field focus on clicking the "Change" button

  function handleChangeAddress() {
    if (inputRef.current) inputRef.current.focus();
  }

  if (!isLoaded)
    return (
      <Stack spacing={0.5}>
        <Skeleton
          variant="rectangular"
          sx={{
            width: "100%",
            height: 420,
            bgcolor: "grey.400",
            border: "none",
          }}
        />
        <Skeleton variant="text" sx={{ fontSize: "2rem" }} />
        <Skeleton variant="text" sx={{ fontSize: "4rem" }} />
      </Stack>
    );

  return (
    <>
      <div className="tw-relative">
        {/* Input Field Wrapped with Autocomplete for typed place suggestion list */}
        <Autocomplete
          onLoad={(autocomplete) => (autoCompleteRef.current = autocomplete)}
          onPlaceChanged={handlePlaceSelect}
        >
          <form
            action=""
            onSubmit={(e) => {
              e.preventDefault();
              console.log("SUBMITTED");
            }}
            className="tw-absolute tw-left-1/2 tw-top-4 tw-z-10 tw-w-10/12 -tw-translate-x-1/2 tw-transform"
          >
            <input
              ref={inputRef}
              type="text"
              placeholder="&#128269;  Enter your address"
              className="tw-rounded-xl tw-p-1.5 tw-pl-3 placeholder:tw-pl-2 placeholder:tw-font-light"
              value={inputFieldValue}
              onChange={(e) =>
                dispatch({
                  type: "inputFieldValue/update",
                  payload: e.target.value,
                })
              }
            />
          </form>
        </Autocomplete>
        {/* GOOGLE MAP BOX */}
        <GoogleMap
          mapContainerStyle={{
            width: "100%", // Make the map responsive
            height: isSmallScreen ? "420px" : "400px",
          }}
          center={userCurrentPosition}
          zoom={18}
          options={{
            mapTypeControl: false,
            streetViewControl: false,
            fullscreenControl: false,
            zoomControl: false,
            // keyboardShortcuts: false,
          }}
          onLoad={
            (map) => dispatch({ type: "mapInstance/centered", payload: map })
            // This map parameter is a fully functional Google Maps object, providing methods and properties to interact with the map (e.g., panning, zooming, event listeners).
          }
        >
          {/* CURRENT LOACTION MARKER BOX  */}
          <Marker
            icon={{
              url: "/svg/location-pin.svg",
              scaledSize: new window.google.maps.Size(40, 40),
              origin: new window.google.maps.Point(0, 0),
              anchor: new window.google.maps.Point(20, 40),
            }}
            key={crypto.randomUUID()}
            position={markerPosition}
          />
        </GoogleMap>

        {/* Current loaction Button */}
        <button
          onClick={() => {
            mapInstance?.panTo(userCurrentPosition);
            // Clear user searched address details when user moves back to currect loaction
            dispatch({ type: "userSearchedAddress/cleared" });
          }}
          className="tw-absolute tw-bottom-4 tw-left-1/2 tw-flex -tw-translate-x-1/2 tw-transform tw-items-center tw-justify-center tw-gap-2 tw-whitespace-nowrap tw-rounded-md tw-border-[1px] tw-border-zcGreen tw-bg-white tw-px-2 tw-py-1.5 tw-text-xs sm:tw-w-fit"
        >
          <TbCurrentLocation className="tw-text-lg tw-text-zcGreen" />
          <span className="tw-font-semibold">Current Location</span>
        </button>
      </div>

      <div className="tw-mt-7">
        {/* Displaying Address and Change Button */}
        <div className="tw-flex tw-items-center tw-justify-between tw-gap-4 tw-px-3 tw-py-5">
          <p className="tw-flex tw-overflow-x-scroll tw-whitespace-nowrap tw-text-sm">
            <span>
              <GrLocationPin className="tw-text-4xl tw-text-zcGreen" />
            </span>
            <span className="tw-self-center">
              {removePlusCode(userSearchedAddress) ||
                removePlusCode(userCurrentAddress)}
            </span>
          </p>
          <button
            onClick={handleChangeAddress}
            className="tw-border tw-border-zcGreen/60 tw-px-2.5 tw-py-2 tw-text-xs tw-font-semibold"
          >
            Change
          </button>
        </div>

        {/* Confirm location and Proceed */}
        <button
          onClick={() =>
            // onShowAddressDetailForm(userSearchedAddress || userCurrentAddress)
            dispatch({ type: "billingDetailsForm/toggled", payload: true })
          }
          className="tw-mx-auto tw-mt-3 tw-block tw-w-11/12 tw-bg-zcGreen tw-px-4 tw-py-2 tw-font-semibold tw-text-stone-700"
        >
          Confirm location and Proceed
        </button>
      </div>

      {/* TESTING */}

      {/* {showAddressDetailForm && (
        <BillingDetailsForm
          phoneNumberInput={phoneNumberInput}
          addressFromMap={addressFromMap}
          onSaveUserEnteredAddressDetails={onSaveUserEnteredAddressDetails}
        />
      )} */}

      {/*  <Sheet
              //* Open sheet only when window width is <= 480 and the isMobileSheetOpen is set to true
              isOpen={isSmallScreen && isModalSheetOpen}
              // isOpen={false}
              onClose={() => dispatch({ type: "sheetModal/close" })}
              detent="content-height"
            > */}

      <Sheet
        isOpen={isSmallScreen && isBillingDetailsFormOpen}
        onClose={() =>
          dispatch({ type: "billingDetailsForm/toggled", payload: false })
        }
        detent="content-height"
      >
        <Sheet.Container>
          <Sheet.Header></Sheet.Header>
          <Sheet.Content>
            <BillingDetailsForm
              phoneNumberInput={phoneNumberInput}
              addressFromMap={addressFromMap}
              onSaveUserEnteredAddressDetails={onSaveUserEnteredAddressDetails}
            />
          </Sheet.Content>
        </Sheet.Container>
      </Sheet>
    </>
  );
}

export default MapAddress;
Leave a Comment